wip(mobile): checkpoint the globe engine and vision mockup in progress

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
Claude committed 2026-10-19 02:49:15 +00:00
1 parent 8b7fcb09df
commit a644e797a7
36 files changed
+10296 -623

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
/**
* Pins for discourse and capture (DESIGN.md §11.3–11.4): posts, sealed
* witness captures, public camera eyes, live broadcasts and holograms. Eyes
* and live pins draw their view frustum as a translucent wedge on the ground
* along the camera heading. Pins are few (tens), so the wedge primitive is
* simply rebuilt on change.
*/
import * as Cesium from 'cesium';
import { glyph } from './glyphs.js';
import { offsetLonLat } from './geo.js';
export const PIN_KINDS = Object.freeze([
'post',
'witness',
'eye',
'live',
'hologram',
]);
const PIN_COLORS = Object.freeze({
post: '#E8EEF7',
witness: '#FFC857',
eye: '#FFD166',
live: '#FF4D6D',
hologram: '#C9A7FF',
});
const PIN_TITLES = Object.freeze({
post: 'Post',
witness: 'Sealed witness capture',
eye: 'Public camera',
live: 'Live broadcast',
hologram: 'Hologram',
});
export function createPins({ scene, requestRender }) {
const root = scene.primitives.add(new Cesium.PrimitiveCollection());
const billboards = root.add(new Cesium.BillboardCollection());
const edges = root.add(new Cesium.PolylineCollection());
let wedges = null;
const pins = new Map();
let pulse = 0;
function rebuildWedges() {
if (wedges) root.remove(wedges);
wedges = null;
edges.removeAll();
const instances = [];
for (const p of pins.values()) {
if (p.kind !== 'eye' && p.kind !== 'live') continue;
if (!Number.isFinite(p.headingDeg)) continue;
const half = (p.fovDeg ?? 60) / 2;
const ring = [[p.lon, p.lat]];
for (let a = -half; a <= half + 0.01; a += Math.max(2, half / 6))
ring.push(offsetLonLat(p.lon, p.lat, p.headingDeg + a, p.rangeM));
const css = p.color || PIN_COLORS[p.kind];
instances.push(
new Cesium.GeometryInstance({
id: { layer: 'pins', key: p.id },
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(
Cesium.Cartesian3.fromDegreesArray(ring.flat()),
),
height: (p.altM || 0) + 3,
vertexFormat: Cesium.PerInstanceColorAppearance.FLAT_VERTEX_FORMAT,
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.fromCssColorString(css).withAlpha(0.28),
),
},
}),
);
edges.add({
positions: Cesium.Cartesian3.fromDegreesArrayHeights(
[...ring, ring[0]].flatMap(([lo, la]) => [lo, la, (p.altM || 0) + 4]),
),
width: 1.5,
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.fromCssColorString(css).withAlpha(0.85),
}),
});
}
if (instances.length)
wedges = root.add(
new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: true,
}),
asynchronous: true,
}),
);
}
function rebuildBillboards() {
billboards.removeAll();
for (const p of pins.values()) {
const g = glyph(p.kind);
p._bb = billboards.add({
position: Cesium.Cartesian3.fromDegrees(
p.lon,
p.lat,
(p.altM || 0) + 8,
),
image: g.image,
imageId: g.imageId,
color: Cesium.Color.fromCssColorString(p.color || PIN_COLORS[p.kind]),
scale: 0.62,
scaleByDistance: new Cesium.NearFarScalar(2e3, 1.15, 8e6, 0.55),
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: 2e5,
id: { layer: 'pins', key: p.id },
});
}
}
function changed() {
rebuildBillboards();
rebuildWedges();
requestRender();
}
return {
add(pin) {
if (!pin || pin.id == null) throw new TypeError('addPin needs an id');
if (!Number.isFinite(pin.lon) || !Number.isFinite(pin.lat))
throw new TypeError('addPin needs lon and lat');
const kind = PIN_KINDS.includes(pin.kind) ? pin.kind : 'post';
const p = {
id: String(pin.id),
lon: pin.lon,
lat: pin.lat,
kind,
headingDeg: Number.isFinite(pin.headingDeg) ? pin.headingDeg : null,
fovDeg: Number.isFinite(pin.fovDeg) ? pin.fovDeg : 60,
rangeM: Number.isFinite(pin.rangeM) ? pin.rangeM : 350,
color: pin.color || null,
label: pin.label || '',
altM: Number.isFinite(pin.altM) ? pin.altM : 0,
};
pins.set(p.id, p);
changed();
return p.id;
},
remove(id) {
const ok = pins.delete(String(id));
if (ok) changed();
return ok;
},
list() {
return [...pins.values()].map(({ _bb, ...p }) => ({ ...p }));
},
locate(id) {
const p = pins.get(String(id));
return p ? [p.lon, p.lat, p.altM || 0] : null;
},
pick(id) {
const p = pins.get(String(id));
if (!p) return null;
return {
kind: 'pin',
pinKind: p.kind,
id: p.id,
title: p.label || PIN_TITLES[p.kind],
subtitle: PIN_TITLES[p.kind],
lon: p.lon,
lat: p.lat,
meta: {
layer: 'pins',
headingDeg: p.headingDeg,
fovDeg: p.fovDeg,
label: p.label,
},
};
},
/** Live pins breathe once a second; returns true when anything changed. */
tick() {
let any = false;
pulse = (pulse + 1) % 2;
for (const p of pins.values())
if (p.kind === 'live' && p._bb) {
p._bb.scale = pulse ? 0.74 : 0.6;
any = true;
}
return any;
},
get billboards() {
return billboards;
},
destroy() {
scene.primitives.remove(root);
},
};
}