PageSourceSearch

https://us.watch/_next/static/media/marker-solver.worker.35_r-c42sifpq.js

js us.watch collected 2026-10-07 07:19:45 UTC 23,612 bytes, 727 lines download raw bytes

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.