1// src/app/[[...name]]/marker-solver.worker.js 2 3const COCO_CLASSES = [ 4 "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", 5 "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", 6 "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", 7 "backpack", "umbrella", "handbag", "tie", "suitcase", 8 "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", 9 "skateboard", "surfboard", "tennis racket", 10 "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", 11 "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", 12 "chair", "couch", "potted plant", "bed", "dining table", "toilet", 13 "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", 14 "microwave", "oven", "toaster", "sink", "refrigerator", 15 "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" 16]; 17 18const COCO_ICON_MAP = { 19 "person": "person", "bicycle": "pedal_bike", "car": "directions_car", "motorcycle": "two_wheeler", 20 "airplane": "flight", "bus": "directions_bus", "train": "train", "truck": "local_shipping", "boat": "directions_boat", 21 "traffic light": "traffic", "fire hydrant": "fire_hydrant_alt", "stop sign": "stop_circle", "parking meter": "local_parking", "bench": "park", 22 "bird": "emoji_nature", "cat": "pets", "dog": "pets", "horse": "agriculture", "sheep": "agriculture", "cow": "agriculture", "elephant": "pets", "bear": "pets", "zebra": "pets", "giraffe": "pets", 23 "backpack": "backpack", "umbrella": "umbrella", "handbag": "shopping_bag", "tie": "checkroom", "suitcase": "luggage", 24 "frisbee": "sports_handball", "skis": "downhill_skiing", "snowboard": "snowboarding", "sports ball": "sports_soccer", "kite": "toys", "baseball bat": "sports_baseball", "baseball glove": "sports_baseball", "skateboard": "skateboarding", "surfboard": "surfing", "tennis racket": "sports_tennis", 25 "bottle": "sports_bar", "wine glass": "wine_bar", "cup": "local_cafe", "fork": "restaurant", "knife": "restaurant", "spoon": "restaurant_menu", "bowl": "ramen_dining", 26 "banana": "spa", "apple": "spa", "sandwich": "lunch_dining", "orange": "spa", "broccoli": "spa", "carrot": "spa", "hot dog": "lunch_dining", "pizza": "local_pizza", "donut": "donut_small", "cake": "cake", 27 "chair": "chair", "couch": "weekend", "potted plant": "yard", "bed": "bed", "dining table": "dining", "toilet": "wc", 28 "tv": "tv", "laptop": "laptop_mac", "mouse": "mouse", "remote": "settings_remote", "keyboard": "keyboard", "cell phone": "smartphone", 29 "microwave": "microwave", "oven": "kitchen", "toaster": "kitchen", "sink": "countertops", "refrigerator": "kitchen", 30 "book": "menu_book", "clock": "schedule", "vase": "science", "scissors": "content_cut", "teddy bear": "pets", "hair drier": "air", "toothbrush": "brush" 31}; 32 33const COCO_CLASS_SET = new Set(COCO_CLASSES); 34 35function projectWorldToScreen(m, worldX, worldY, worldZ, viewport) { 36 const clipX = (m[0] * worldX) + (m[4] * worldY) + (m[8] * worldZ) + m[12]; 37 const clipY = (m[1] * worldX) + (m[5] * worldY) + (m[9] * worldZ) + m[13]; 38 const clipZ = (m[2] * worldX) + (m[6] * worldY) + (m[10] * worldZ) + m[14]; 39 const clipW = (m[3] * worldX) + (m[7] * worldY) + (m[11] * worldZ) + m[15]; 40 const invW = clipW !== 0 ? (1 / clipW) : 0; 41 42 const ndcX = clipX * invW; 43 const ndcY = clipY * invW; 44 const ndcZ = clipZ * invW; 45 46 const xView = (ndcX * 0.5 + 0.5) * viewport.vw + viewport.offX; 47 const yView = (-ndcY * 0.5 + 0.5) * viewport.vh + viewport.offY; 48 49 return { xView, yView, ndcZ }; 50} 51 52function solveUplight(item, viewport) { 53 const { xView, yView } = projectWorldToScreen( 54 viewport.viewProj, 55 item.worldX, 56 item.worldY, 57 item.worldZ, 58 viewport, 59 ); 60 61 const x = xView - viewport.rrLeft; 62 const y = yView - viewport.rrTop; 63 64 const blA = Math.atan2(viewport.bottom - y, viewport.left - x); 65 const tlA = Math.atan2(viewport.top - y, viewport.left - x); 66 const trA = Math.atan2(viewport.top - y, viewport.right - x); 67 68 const wrap = (t) => (t < 0 ? t + (2 * Math.PI) : t); 69 const shift = (t, base) => wrap(t - base); 70 71 const A = shift(tlA, blA); 72 const T = shift(tlA, blA); 73 let R = shift(trA, blA); 74 if (R < T) R += 2 * Math.PI; 75 76 const clamped = wrap(blA) + Math.max(0, Math.min(R, A)); 77 const dx = Math.cos(clamped); 78 const dy = Math.sin(clamped); 79 80 let tLength = (viewport.top - y) / dy; 81 if (!Number.isFinite(tLength) || tLength < 0) tLength = 0; 82 83 const xHit = x + tLength * dx; 84 if (xHit < viewport.left || xHit > viewport.right) { 85 const snapX = xHit < viewport.left ? viewport.left : viewport.right; 86 const tX = (snapX - x) / dx; 87 if (Number.isFinite(tX) && tX >= 0) tLength = Math.min(tLength, tX); 88 } 89 90 const rot = (clamped > Math.PI ? clamped - (2 * Math.PI) : clamped) + (Math.PI / 2); 91 const visW = (item.worldRad / viewport.mpp) * 2.0; 92 93 return { 94 id: item.id, 95 x, 96 y, 97 tLength, 98 rot, 99 visW, 100 }; 101} 102 103function buildBlockCandidate(item, viewport) { 104 if (item.mode === "reuse") { 105 return { 106 id: item.id, 107 x: item.x, 108 y: item.y, 109 w: item.w, 110 h: item.h, 111 clamp: item.clamp, 112 priority: item.priority, 113 }; 114 } 115 116 const projected = projectWorldToScreen( 117 viewport.viewProj, 118 item.worldX, 119 item.worldY, 120 item.worldZ, 121 viewport, 122 ); 123 124 if (projected.ndcZ <= -1.2 || projected.ndcZ >= 1.2) return null; 125 126 return { 127 id: item.id, 128 x: projected.xView - viewport.rrLeft, 129 y: projected.yView - viewport.rrTop, 130 w: item.w, 131 h: item.h, 132 clamp: item.clamp, 133 priority: item.priority, 134 }; 135} 136 137function solveBlocks(items, viewport) { 138 if (!Array.isArray(items) || items.length === 0) return []; 139 140 const candidates = []; 141 for (let i = 0; i < items.length; i++) { 142 const candidate = buildBlockCandidate(items[i], viewport); 143 if (candidate) candidates.push(candidate); 144 } 145 146 if (candidates.length <= 1) return []; 147 148 candidates.sort((a, b) => 149 (b.priority - a.priority) 150 || (a.y - b.y) 151 || (a.x - b.x) 152 ); 153 154 const placed = []; 155 const solved = []; 156 157 for (let i = 0; i < candidates.length; i++) { 158 const item = candidates[i]; 159 let targetClamp = item.clamp; 160 161 for (let j = 0; j < placed.length; j++) { 162 const p = placed[j]; 163 const needX = (item.w + p.w) * 0.62; 164 const needY = (item.h + p.h) * 0.58; 165 const dx = Math.abs(item.x - p.x); 166 const dy = Math.abs(item.y - p.y); 167 if (dx >= needX || dy >= needY) continue; 168 169 const xRatio = dx / Math.max(needX, 1);
170 const yRatio = dy / Math.max(needY, 1); 171 const conservative = Math.min(1, Math.max(0.42, Math.min(xRatio, yRatio) * 0.92)); 172 targetClamp = Math.min(targetClamp, item.clamp * conservative); 173 } 174 175 solved.push({ id: item.id, targetClamp }); 176 placed.push({ 177 x: item.x, 178 y: item.y, 179 w: item.w * Math.max(targetClamp, 0.01), 180 h: item.h * Math.max(targetClamp, 0.01), 181 }); 182 } 183 184 return solved; 185} 186 187function getDistanceMeters(lat1, lng1, lat2, lng2) { 188 const R = 6371e3; 189 const phi1 = lat1 * Math.PI / 180; 190 const phi2 = lat2 * Math.PI / 180; 191 const dPhi = (lat2 - lat1) * Math.PI / 180; 192 const dLambda = (lng2 - lng1) * Math.PI / 180; 193 const a = Math.sin(dPhi / 2) * Math.sin(dPhi / 2) 194 + Math.cos(phi1) * Math.cos(phi2) 195 * Math.sin(dLambda / 2) * Math.sin(dLambda / 2); 196 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 197 return R * c; 198} 199 200function isPointInClip(viewProj, worldX, worldY, worldZ) { 201 const clipX = (viewProj[0] * worldX) + (viewProj[4] * worldY) + (viewProj[8] * worldZ) + viewProj[12]; 202 const clipY = (viewProj[1] * worldX) + (viewProj[5] * worldY) + (viewProj[9] * worldZ) + viewProj[13]; 203 const clipZ = (viewProj[2] * worldX) + (viewProj[6] * worldY) + (viewProj[10] * worldZ) + viewProj[14]; 204 const clipW = (viewProj[3] * worldX) + (viewProj[7] * worldY) + (viewProj[11] * worldZ) + viewProj[15]; 205 if (clipW === 0) return false; 206 return ( 207 clipX >= -clipW && clipX <= clipW && 208 clipY >= -clipW && clipY <= clipW && 209 clipZ >= -clipW && clipZ <= clipW 210 ); 211} 212 213function rotateByQuaternion(x, y, z, qx, qy, qz, qw) { 214 const ix = (qw * x) + (qy * z) - (qz * y); 215 const iy = (qw * y) + (qz * x) - (qx * z); 216 const iz = (qw * z) + (qx * y) - (qy * x); 217 const iw = (-qx * x) - (qy * y) - (qz * z); 218 219 return { 220 x: (ix * qw) + (iw * -qx) + (iy * -qz) - (iz * -qy), 221 y: (iy * qw) + (iw * -qy) + (iz * -qx) - (ix * -qz), 222 z: (iz * qw) + (iw * -qz) + (ix * -qy) - (iy * -qx), 223 }; 224} 225 226function isStrictlyInsideViewport(item, viewProj, margin = 0) { 227 const corners = item?.corners; 228 const baseScale = item?.baseScale; 229 const position = item?.position; 230 const quaternion = item?.quaternion; 231 if (!Array.isArray(corners) || corners.length !== 24) return false; 232 if (!Array.isArray(baseScale) || baseScale.length !== 3) return false; 233 if (!Array.isArray(position) || position.length !== 3) return false; 234 if (!Array.isArray(quaternion) || quaternion.length !== 4) return false; 235 236 let minX = Infinity; 237 let minY = Infinity; 238 let minZ = Infinity; 239 let maxX = -Infinity; 240 let maxY = -Infinity; 241 let maxZ = -Infinity; 242 243 for (let i = 0; i < 24; i += 3) { 244 const scaledX = corners[i] * baseScale[0]; 245 const scaledY = corners[i + 1] * baseScale[1]; 246 const scaledZ = corners[i + 2] * baseScale[2]; 247 const rotated = rotateByQuaternion( 248 scaledX, scaledY, scaledZ, 249 quaternion[0], quaternion[1], quaternion[2], quaternion[3], 250 ); 251 252 const worldX = rotated.x + position[0]; 253 const worldY = rotated.y + position[1]; 254 const worldZ = rotated.z + position[2]; 255 256 const clipX = (viewProj[0] * worldX) + (viewProj[4] * worldY) + (viewProj[8] * worldZ) + viewProj[12]; 257 const clipY = (viewProj[1] * worldX) + (viewProj[5] * worldY) + (viewProj[9] * worldZ) + viewProj[13]; 258 const clipZ = (viewProj[2] * worldX) + (viewProj[6] * worldY) + (viewProj[10] * worldZ) + viewProj[14]; 259 const clipW = (viewProj[3] * worldX) + (viewProj[7] * worldY) + (viewProj[11] * worldZ) + viewProj[15]; 260 if (clipW === 0) return false; 261 262 const ndcX = clipX / clipW; 263 const ndcY = clipY / clipW; 264 const ndcZ = clipZ / clipW; 265 266 if (ndcX < minX) minX = ndcX; 267 if (ndcY < minY) minY = ndcY; 268 if (ndcZ < minZ) minZ = ndcZ; 269 if (ndcX > maxX) maxX = ndcX; 270 if (ndcY > maxY) maxY = ndcY; 271 if (ndcZ > maxZ) maxZ = ndcZ; 272 } 273 274 if (maxZ < -1 || minZ > 1) return false; 275 return ( 276 minX >= (-1 + margin) && maxX <= (1 - margin) && 277 minY >= (-1 + margin) && maxY <= (1 - margin) 278 ); 279} 280 281function solveDots(items, options) { 282 const isBuffer = items instanceof Float32Array; 283 const count = isBuffer ? items.length / 40 : items.length; 284 285 if (count === 0) { 286 return { visibility: [], targetId: null, minDist: Infinity, targetStrictInView: false }; 287 } 288 289 const visibility = []; 290 let targetId = null; 291 let minDist = Infinity; 292 let targetIdx = -1; 293 294 for (let i = 0; i < count; i++) { 295 let id, lat, lng, worldX, worldY, worldZ, breakLegIdx, flags; 296 let itemObj = null; 297 298 if (isBuffer) { 299 const base = i * 40; 300 id = items[base]; 301 lat = items[base + 1]; 302 lng = items[base + 2]; 303 worldX = items[base + 3]; 304 worldY = items[base + 4]; 305 worldZ = items[base + 5]; 306 breakLegIdx = items[base + 6]; 307 flags = items[base + 7]; 308 } else { 309 itemObj = items[i]; 310 id = itemObj.id; 311 lat = itemObj.lat; 312 lng = itemObj.lng; 313 worldX = itemObj.worldX; 314 worldY = itemObj.worldY; 315 worldZ = itemObj.worldZ; 316 breakLegIdx = itemObj.breakLegIdx; 317 flags = 0;
318 if (itemObj.dismissed) flags |= 1; 319 if (itemObj.keepVisible) flags |= 2; 320 if (itemObj.sightingActive) flags |= 4; 321 if (itemObj.floatAwayStarted) flags |= 8; 322 if (itemObj.disintegrating) flags |= 16; 323 if (itemObj.ignoreProximity) flags |= 32; 324 if (itemObj.isBreadcrumb) flags |= 64; 325 } 326 327 const dismissed = flags & 1; 328 const keepVisible = flags & 2; 329 const sightingActive = flags & 4; 330 const floatAwayStarted = flags & 8; 331 const disintegrating = flags & 16; 332 const ignoreProximity = flags & 32; 333 const isBreadcrumb = flags & 64; 334 335 if (dismissed && !keepVisible) { 336 visibility.push({ id, visible: false }); 337 continue; 338 } 339 340 const dist = getDistanceMeters(options.myLat, options.myLng, lat, lng); 341 let visible = false; 342 343 if (options.isOverview) { 344 visible = isPointInClip(options.viewProj, worldX, worldY, worldZ) || dist < options.cullRadius; 345 } else if (sightingActive || floatAwayStarted || disintegrating || keepVisible) { 346 visible = true; 347 } 348 349 visibility.push({ id, visible }); 350 351 // ð¡ï¸ Track proximity suppression by label ID (bit 8+ of flags) 352 const labelId = (flags >> 8) & 0xFF; 353 if (labelId > 0 && dist < 25) { 354 if (!options.suppressedLabelIds) options.suppressedLabelIds = new Set(); 355 options.suppressedLabelIds.add(labelId); 356 } 357 358 if (!ignoreProximity && (isBreadcrumb || breakLegIdx === options.curLeg)) { 359 if (dist <= 100 && dist < minDist) { 360 minDist = dist; 361 targetId = id; 362 targetIdx = i; 363 } 364 } 365 } 366 367 let targetStrictInView = false; 368 if (targetId != null) { 369 let targetObj = null; 370 if (isBuffer) { 371 const base = targetIdx * 40; 372 targetObj = { 373 worldX: items[base + 3], 374 worldY: items[base + 4], 375 worldZ: items[base + 5], 376 corners: items.slice(base + 8, base + 32), 377 quaternion: items.slice(base + 32, base + 36), 378 baseScale: items.slice(base + 36, base + 39), 379 position: [items[base + 3], items[base + 4], items[base + 5]], 380 }; 381 } else { 382 targetObj = items[targetIdx]; 383 } 384 385 if (targetObj) { 386 if (targetObj.corners) { 387 targetStrictInView = isStrictlyInsideViewport(targetObj, options.viewProj, options.margin || 0); 388 } else { 389 targetStrictInView = isPointInClip(options.viewProj, targetObj.worldX, targetObj.worldY, targetObj.worldZ); 390 } 391 } 392 } 393 394 return { visibility, targetId, minDist, targetStrictInView }; 395} 396 397function solveWatchFlare(data) { 398 const source = data?.source || null; 399 const rr = data?.rr || null; 400 const viewRect = data?.viewRect || null; 401 const flareSidesMax = Math.max(0, data?.flareSidesMax || 0); 402 if (!source || !rr || !viewRect || flareSidesMax <= 0) { 403 return { 404 active: false, 405 sourceId: source?.id ?? null, 406 hits: [], 407 visibleSides: [], 408 opacity: 1, 409 primarySide: null, 410 }; 411 } 412 413 const x = Number(source.x); 414 const y = Number(source.y); 415 const rot = Number(source.rot); 416 if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(rot)) { 417 return { 418 active: false, 419 sourceId: source?.id ?? null, 420 hits: [], 421 visibleSides: [], 422 opacity: 1, 423 primarySide: null, 424 }; 425 } 426 427 const inset = Number.isFinite(data?.inset) ? data.inset : 24; 428 const px = rr.left + x; 429 const py = rr.top + y; 430 const theta = rot - Math.PI / 2; 431 const vx = Math.cos(theta); 432 const vy = Math.sin(theta); 433 const bL = viewRect.left + inset; 434 const bR = viewRect.right - inset; 435 const bT = viewRect.top + inset; 436 const bB = viewRect.bottom - inset; 437 438 const hits = []; 439 if (Math.abs(vx) > 1e-4) { 440 const tR = (bR - px) / vx; 441 const yR = py + tR * vy; 442 if (tR >= 0 && yR >= bT && yR <= bB) { 443 hits.push({ side: "right", x: bR, y: yR, t: tR }); 444 } 445 } 446 if (Math.abs(vy) > 1e-4) { 447 const tB = (bB - py) / vy; 448 const xB = px + tB * vx; 449 if (tB >= 0 && xB >= bL && xB <= bR) { 450 hits.push({ side: "bottom", x: xB, y: bB, t: tB }); 451 } 452 } 453 454 if (!hits.length) { 455 return { 456 active: false, 457 sourceId: source?.id ?? null, 458 hits: [], 459 visibleSides: [], 460 opacity: Number.isFinite(source.opacity) ? source.opacity : 1, 461 primarySide: null, 462 }; 463 } 464 465 hits.sort((a, b) => a.t - b.t); 466 const selectedHits = hits.slice(0, flareSidesMax).map((hit) => ({ 467 side: hit.side, 468 x: hit.x - viewRect.left, 469 y: hit.y - viewRect.top, 470 t: hit.t, 471 })); 472 473 return { 474 active: true, 475 sourceId: source?.id ?? null, 476 hits: selectedHits, 477 visibleSides: selectedHits.map((hit) => hit.side), 478 opacity: Number.isFinite(source.opacity) ? source.opacity : 1, 479 primarySide: selectedHits[0]?.side || null, 480 }; 481} 482 483function normalizeLngDegrees(lng) { 484 return ((((lng + 540) % 360) + 360) % 360) - 180; 485} 486 487function computeOffsetLatLng(lat, lng, distanceMeters, headingDegrees) { 488 const earthRadiusM = 6378137; 489 const angularDistance = distanceMeters / earthRadiusM; 490 const heading = headingDegrees * Math.PI / 180; 491 const lat1 = lat * Math.PI / 180; 492 const lng1 = lng * Math.PI / 180; 493 const sinLat1 = Math.sin(lat1); 494 const cosLat1 = Math.cos(lat1); 495 const sinAd = Math.sin(angularDistance); 496 const cosAd = Math.cos(angularDistance); 497 const sinHeading = Math.sin(heading);
498 const cosHeading = Math.cos(heading); 499 500 const sinLat2 = (sinLat1 * cosAd) + (cosLat1 * sinAd * cosHeading); 501 const lat2 = Math.asin(Math.max(-1, Math.min(1, sinLat2))); 502 const lng2 = lng1 + Math.atan2( 503 sinHeading * sinAd * cosLat1, 504 cosAd - (sinLat1 * sinLat2), 505 ); 506 507 return { 508 lat: lat2 * 180 / Math.PI, 509 lng: normalizeLngDegrees(lng2 * 180 / Math.PI), 510 }; 511} 512 513function solveOverviewSweep(data) { 514 const centerLat = Number(data?.centerLat); 515 const centerLng = Number(data?.centerLng); 516 const ax = Number(data?.ax); 517 const ay = Number(data?.ay); 518 const elapsedMs = Number(data?.elapsedMs); 519 if ( 520 !Number.isFinite(centerLat) || 521 !Number.isFinite(centerLng) || 522 !Number.isFinite(ax) || 523 !Number.isFinite(ay) || 524 !Number.isFinite(elapsedMs) 525 ) { 526 return { lat: null, lng: null, offsetX: 0, offsetY: 0 }; 527 } 528 529 const omegaX = 2 * Math.PI / 9000; 530 const omegaY = 2 * Math.PI / 6000; 531 const phi = Math.PI / 3; 532 const offsetX = ax * Math.sin(omegaX * elapsedMs); 533 const offsetY = ay * Math.sin(omegaY * elapsedMs + phi); 534 535 const eastFirst = computeOffsetLatLng( 536 centerLat, 537 centerLng, 538 Math.abs(offsetX), 539 offsetX >= 0 ? 90 : 270, 540 ); 541 const sweep = computeOffsetLatLng( 542 eastFirst.lat, 543 eastFirst.lng, 544 Math.abs(offsetY), 545 offsetY >= 0 ? 0 : 180, 546 ); 547 548 return { 549 lat: sweep.lat, 550 lng: sweep.lng, 551 offsetX, 552 offsetY, 553 }; 554} 555function solveHudState(counts, suppressedLabelIds, fullLabels) { 556 if (!counts || typeof counts !== "object") return null; 557 558 const sSet = new Set(); 559 if (suppressedLabelIds && typeof suppressedLabelIds.forEach === "function") { 560 suppressedLabelIds.forEach(id => { 561 const label = COCO_CLASSES[id - 1]; 562 if (label) sSet.add(label); 563 }); 564 } 565 566 const fSet = new Set(fullLabels || []); 567 568 const extraNames = []; 569 for (const name in counts) { 570 if (!COCO_CLASS_SET.has(name)) extraNames.push(name); 571 } 572 extraNames.sort(); 573 574 const orderedNames = COCO_CLASSES.concat(extraNames); 575 const pieces = []; 576 const fullIcons = []; 577 let total = 0; 578 579 for (const name of orderedNames) { 580 const c = counts[name] || 0; 581 if (!c) continue; 582 583 total += c; 584 const iconName = COCO_ICON_MAP[name] || "trip_origin"; 585 const isFull = fSet.has(name); 586 587 if (isFull) { 588 fullIcons.push(`<span class="material-icons-outlined env-icon" data-state="full">${iconName}</span>`); 589 } else { 590 const isProxSuppressed = sSet.has(name); 591 const state = isProxSuppressed ? "suppressed" : "active"; 592 for (let i = 0; i < c; i++) { 593 pieces.push(`<span class="material-icons-outlined env-icon" data-state="${state}">${iconName}</span>`); 594 } 595 } 596 } 597 598 if (total === 0) return null; 599 600 return { 601 faceSummary: pieces.join(""), 602 objLabel: fullIcons.length > 0 ? "Max reached" : "Auto-detect", 603 objIconOverride: fullIcons.length > 0 ? fullIcons.join("") : null, 604 piecesCount: pieces.length, 605 fullCount: fullIcons.length, 606 }; 607} 608 609self.onmessage = (event) => { 610 const { type, data } = event.data || {}; 611 if ( 612 type !== "solve_marker_batch" && 613 type !== "solve_dot_batch" && 614 type !== "solve_watch_flare_batch" && 615 type !== "solve_overview_sweep" 616 ) return; 617 618 try { 619 if (type === "solve_dot_batch") { 620 const options = data?.options || {}; 621 options.viewProj = Array.isArray(data?.viewProj) 622 ? data.viewProj 623 : Array.from(data?.viewProj || []); 624 const result = solveDots(data?.dots || [], options); 625 626 // ð¡ï¸ Also solve HUD state if counts provided 627 const hudManifest = solveHudState(data?.counts, result.suppressedLabelIds, data?.fullLabels); 628 629 self.postMessage({ 630 type: "dot_solve_result", 631 frameId: data?.frameId || 0, 632 cycleStamp: data?.cycleStamp, 633 visibility: result.visibility, 634 targetId: result.targetId, 635 minDist: result.minDist, 636 targetStrictInView: !!result.targetStrictInView, 637 suppressedLabelIds: result.suppressedLabelIds, 638 hudManifest, 639 }); 640 return; 641 } 642 if (type === "solve_overview_sweep") { 643 const result = solveOverviewSweep(data); 644 self.postMessage({ 645 type: "overview_sweep_result", 646 frameId: data?.frameId || 0, 647 token: data?.token || 0, 648 lat: result.lat, 649 lng: result.lng, 650 offsetX: result.offsetX, 651 offsetY: result.offsetY, 652 }); 653 return; 654 } 655 if (type === "solve_watch_flare_batch") { 656 const result = solveWatchFlare(data); 657 self.postMessage({ 658 type: "watch_flare_result", 659 frameId: data?.frameId || 0, 660 ...result, 661 }); 662 return; 663 } 664 665 const viewport = data?.viewport || {}; 666 viewport.viewProj = Array.isArray(data?.viewProj) 667 ? data.viewProj 668 : Array.from(data?.viewProj || []); 669 670 const uplights = []; 671 if (data?.uplights && data.uplights instanceof Float32Array) { 672 const uBuf = data.uplights; 673 for (let i = 0; i < uBuf.length; i += 5) { 674 uplights.push(solveUplight({ 675 id: uBuf[i], 676 worldX: uBuf[i+1], 677 worldY: uBuf[i+2], 678 worldZ: uBuf[i+3], 679 worldRad: uBuf[i+4] 680 }, viewport)); 681 } 682 } else if (Array.isArray(data?.uplights)) {
683 data.uplights.forEach((item) => uplights.push(solveUplight(item, viewport))); 684 } 685 686 let blockItems = []; 687 if (data?.blocks && data.blocks instanceof Float32Array) { 688 const bBuf = data.blocks; 689 for (let i = 0; i < bBuf.length; i += 10) { 690 blockItems.push({ 691 id: bBuf[i], 692 mode: bBuf[i+1] === 1 ? "reuse" : "project", 693 worldX: bBuf[i+2], 694 worldY: bBuf[i+3], 695 worldZ: bBuf[i+4], 696 x: bBuf[i+2], // reuse sets worldX slot to x 697 y: bBuf[i+3], // reuse sets worldY slot to y 698 w: bBuf[i+5], 699 h: bBuf[i+6], 700 clamp: bBuf[i+7], 701 priority: bBuf[i+8] 702 }); 703 } 704 } else { 705 blockItems = data?.blocks || []; 706 } 707 const blocks = solveBlocks(blockItems, viewport); 708 709 self.postMessage({ 710 type: "marker_solve_result", 711 frameId: data?.frameId || 0, 712 uplights, 713 blocks, 714 }); 715 } catch (error) { 716 self.postMessage({ 717 type: type === "solve_dot_batch" 718 ? "dot_solve_error" 719 : (type === "solve_watch_flare_batch" 720 ? "watch_flare_error" 721 : (type === "solve_overview_sweep" ? "overview_sweep_error" : "marker_solve_error")), 722 frameId: data?.frameId || 0, 723 token: data?.token || 0, 724 error: String(error?.message || error || "marker solve failed"), 725 }); 726 } 727};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.