1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-08-09 20:59:23 +03:00

fix(globe3d=11): ortho hub land + affine flat wrap fallback

Restore sampleHub for reference N-S proportions; wrap via inverse
inner-layer affine on mercator-flat with seamless U bilinear.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-05 18:23:07 +02:00
parent 7b10062604
commit c0c99b7cce
2 changed files with 80 additions and 11 deletions

View File

@@ -11,7 +11,7 @@
* globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated). * globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated).
* globe3d=9: v4 sphere grid only (thin EW + NS) — proper rotation baseline. * globe3d=9: v4 sphere grid only (thin EW + NS) — proper rotation baseline.
* globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped. * globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped.
* globe3d=11: v10 grid + mercator-flat land wrap (lon center 12°, inverse Mercator Y). * globe3d=11: v10 grid + ortho hub land + affine flat-plate wrap fallback.
*/ */
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
@@ -20,8 +20,13 @@ const DRAG_ROTATION_PER_PX = 0.004;
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 }; const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
const FRONT_Z = 0.04; const FRONT_Z = 0.04;
const LIMB_Z = 0.002; const LIMB_Z = 0.002;
/** hub-logo-continents ortho / flat map center (extract_red_polygons.py). */ /** hub-logo-continents inner layer fit (continents-meta.json / extract_red_polygons.py). */
const MERCATOR_LON_CENTER = (12 * Math.PI) / 180; const CONTINENT_FLAT_W = 800;
const CONTINENT_FLAT_H = 519;
const CONTINENT_INNER_TX = 223.66088631984587;
const CONTINENT_INNER_TY = 238.0;
const CONTINENT_INNER_SX = 1.2408477842003853;
const CONTINENT_INNER_SY = 1.2408477842003853;
const RASTER_SCALE = 2; const RASTER_SCALE = 2;
const AXIS_STEPS = 160; const AXIS_STEPS = 160;
const SPHERE_AXIS_STEPS = 180; const SPHERE_AXIS_STEPS = 180;
@@ -135,6 +140,34 @@ function rasterizeImage(img, w, h, scale) {
return { data: ctx.getImageData(0, 0, rw, rh).data, w: rw, h: rh }; return { data: ctx.getImageData(0, 0, rw, rh).data, w: rw, h: rh };
} }
function sampleImageDataBilinearWrapU(imgData, w, h, fx, fy) {
const y = Math.max(0, Math.min(h - 1.001, fy));
let x = fx % w;
if (x < 0) {
x += w;
}
const x0 = x | 0;
const tx = x - x0;
const x1 = (x0 + 1) % w;
const y0 = y | 0;
const y1 = Math.min(h - 1, y0 + 1);
const ty = y - y0;
const i00 = (y0 * w + x0) * 4;
const i10 = (y0 * w + x1) * 4;
const i01 = (y1 * w + x0) * 4;
const i11 = (y1 * w + x1) * 4;
const out = [0, 0, 0, 0];
for (let c = 0; c < 4; c++) {
out[c] = Math.round(
imgData[i00 + c] * (1 - tx) * (1 - ty) +
imgData[i10 + c] * tx * (1 - ty) +
imgData[i01 + c] * (1 - tx) * ty +
imgData[i11 + c] * tx * ty
);
}
return out;
}
function sampleImageDataBilinear(imgData, w, h, fx, fy) { function sampleImageDataBilinear(imgData, w, h, fx, fy) {
const x = Math.max(0, Math.min(w - 1.001, fx)); const x = Math.max(0, Math.min(w - 1.001, fx));
const y = Math.max(0, Math.min(h - 1.001, fy)); const y = Math.max(0, Math.min(h - 1.001, fy));
@@ -900,13 +933,25 @@ function latToMercatorRow(lat, h) {
function sampleMercFlat(mercFlat, lon, lat) { function sampleMercFlat(mercFlat, lon, lat) {
const u = ((wrapLon(lon) + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1); const u = ((wrapLon(lon) + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1);
const v = latToMercatorRow(lat, mercFlat.h); const v = latToMercatorRow(lat, mercFlat.h);
return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, u, v); return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, u, v);
}
/** Inverse hub-disk → flat plate (same affine as globe SVG), X wraps 360°. */
function sampleFlatPlateFromHub(mercFlat, hpX, hpY) {
const fx = (hpX - CONTINENT_INNER_TX) / CONTINENT_INNER_SX;
const fy = (hpY - CONTINENT_INNER_TY) / CONTINENT_INNER_SY;
if (fy < 0 || fy > CONTINENT_FLAT_H - 1) {
return null;
}
const sx = (fx / CONTINENT_FLAT_W) * mercFlat.w;
const sy = (fy / CONTINENT_FLAT_H) * mercFlat.h;
return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, sx, sy);
} }
/** Sphere tex → mercator-flat plate (lon offset + inverse Mercator Y). */ /** Sphere tex → mercator-flat plate (lon offset + inverse Mercator Y). */
function sampleMercFlatFromTex(mercFlat, tex) { function sampleMercFlatFromTex(mercFlat, tex) {
const ll = texLonLat(tex); const ll = texLonLat(tex);
const lon = wrapLon(ll.lon + MERCATOR_LON_CENTER); const lon = wrapLon(ll.lon + (12 * Math.PI) / 180);
return sampleMercFlat(mercFlat, lon, ll.lat); return sampleMercFlat(mercFlat, lon, ll.lat);
} }
@@ -936,6 +981,24 @@ function hubContinentRgba(sample) {
return [sample[0], sample[1], sample[2], 255]; return [sample[0], sample[1], sample[2], 255];
} }
/** Solid fill from rasterized globe SVG — skip gradient fringe / drop-shadow. */
function isSolidContinentPixel(r, g, b, a) {
if (a < 72) {
return false;
}
if (r > 168 && g > 198 && b > 218) {
return false;
}
if (r < 58 && g < 68 && b < 95) {
return false;
}
const lum = r * 0.299 + g * 0.587 + b * 0.114;
if (lum < 102 || lum > 218) {
return false;
}
return b >= 105 && g >= 100 && r >= 72;
}
/** Light hub grid strokes baked into hub-logo-fragment-globe.svg (vector grid drawn separately). */ /** Light hub grid strokes baked into hub-logo-fragment-globe.svg (vector grid drawn separately). */
function isGridPixel(r, g, b, a) { function isGridPixel(r, g, b, a) {
if (a < 12) { if (a < 12) {
@@ -1000,13 +1063,19 @@ function sampleLandRgbaMercator(mercFlat, tex) {
return null; return null;
} }
/** globe3d=11 — visible hemisphere (z > LIMB_Z); mercator-flat wrap (single source, no ortho ghost). */ /** globe3d=11 — ortho hub (reference NS) + wrapped flat-plate fallback. */
function sampleLandVisibleHemisphere(mercFlat, tex) { function sampleLandVisibleHemisphere(continents, mercFlat, tex) {
if (tex.z <= LIMB_Z) { if (tex.z <= LIMB_Z) {
return null; return null;
} }
const flat = sampleMercFlatFromTex(mercFlat, tex); const ortho = sampleHub(continents, tex);
if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) { if (isSolidContinentPixel(ortho[0], ortho[1], ortho[2], ortho[3])) {
return hubContinentRgba(ortho);
}
const hpX = GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r;
const hpY = GLOBE_VIEW.cy - tex.y * GLOBE_VIEW.r;
const flat = sampleFlatPlateFromHub(mercFlat, hpX, hpY);
if (flat && isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) {
return hubContinentRgba(flat); return hubContinentRgba(flat);
} }
return null; return null;
@@ -1222,7 +1291,7 @@ export async function mountHubGlobe(stage, opts) {
cy, cy,
r, r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
const l = sampleLandVisibleHemisphere(mercFlat, tex); const l = sampleLandVisibleHemisphere(continents, mercFlat, tex);
return l || [0, 0, 0, 0]; return l || [0, 0, 0, 0];
}) })
); );

View File

@@ -187,7 +187,7 @@ function hub_h(string $s): string {
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover')); var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
var logoEnabled = !!(stage && supportsSvg && supportsLayout); var logoEnabled = !!(stage && supportsSvg && supportsLayout);
var logoBuild = '20260704globe15'; var logoBuild = '20260704globe16';
function hubAsset(rel) { function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]'); var link = document.querySelector('link[href*="hub.css"]');