mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge branch 'main' into contrib/gods-006-weather-missing-observations-20261011-reviewed
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
2befda5286
18 files changed
+192
-22
No files matched your search
@@ -28,6 +28,11 @@ export function resolveCameraMove(scene, shot) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Wrap an interpolated camera longitude to WGS84 without changing authored endpoints. */
|
||||
export function normalizeCameraLongitude(longitude) {
|
||||
return ((((longitude + 180) % 360) + 360) % 360) - 180;
|
||||
}
|
||||
|
||||
/** Sample the same authored curve for playback and seeking, taking the short longitude/angle arc. */
|
||||
export function sampleCameraMove(move, progress) {
|
||||
const t = Math.max(0, Math.min(1, Number(progress) || 0));
|
||||
@@ -45,7 +50,7 @@ export function sampleCameraMove(move, progress) {
|
||||
const { from, to } = move;
|
||||
return {
|
||||
lat: lerp(from.lat, to.lat),
|
||||
lon: ((((angle(from.lon, to.lon) + 180) % 360) + 360) % 360) - 180,
|
||||
lon: normalizeCameraLongitude(angle(from.lon, to.lon)),
|
||||
alt: lerp(from.alt, to.alt),
|
||||
heading: angle(from.heading, to.heading),
|
||||
pitch: lerp(from.pitch, to.pitch),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { cameraAtProgress } from './timeline.js';
|
||||
const camera = (lon) => ({
|
||||
lat: 10,
|
||||
lon,
|
||||
alt: 1000,
|
||||
heading: 0,
|
||||
pitch: -40,
|
||||
roll: 0,
|
||||
});
|
||||
test('director seek follows the short dateline arc in both directions', () => {
|
||||
for (const [a, b] of [
|
||||
[179, -179],
|
||||
[-179, 179],
|
||||
]) {
|
||||
assert.equal(
|
||||
Math.abs(cameraAtProgress(camera(a), camera(b), 0.5).lon),
|
||||
180,
|
||||
);
|
||||
assert.equal(cameraAtProgress(camera(a), camera(b), 0).lon, a);
|
||||
assert.equal(cameraAtProgress(camera(a), camera(b), 1).lon, b);
|
||||
assert.ok(Math.abs(cameraAtProgress(camera(a), camera(b), 0.25).lon) > 179);
|
||||
}
|
||||
assert.equal(cameraAtProgress(camera(20), camera(24), 0.5).lon, 22);
|
||||
});
|
||||
|
||||
test('director seek keeps exact ±180 endpoints and picks the short arc near the antipode', () => {
|
||||
for (const [a, b] of [
|
||||
[180, -180],
|
||||
[-180, 180],
|
||||
]) {
|
||||
assert.equal(cameraAtProgress(camera(a), camera(b), 0).lon, a);
|
||||
assert.equal(cameraAtProgress(camera(a), camera(b), 1).lon, b);
|
||||
assert.equal(
|
||||
Math.abs(cameraAtProgress(camera(a), camera(b), 0.5).lon),
|
||||
180,
|
||||
);
|
||||
}
|
||||
// 10°E to 169.9°W is 179.9° westward through Greenwich, not 180.1° east.
|
||||
const mid = cameraAtProgress(camera(10), camera(-169.9), 0.5).lon;
|
||||
assert.ok(mid > -81 && mid < -79, `short arc midpoint, got ${mid}`);
|
||||
});
|
||||
|
||||
test('director seek turns heading and roll the short way for any authored angles', () => {
|
||||
const pose = (heading, roll) => ({ ...camera(0), heading, roll });
|
||||
const mid = cameraAtProgress(pose(350, 350), pose(-350, -350), 0.5);
|
||||
// 350° and -350° are 20° apart through 0°/360°, not 340° through 180°.
|
||||
for (const value of [mid.heading, mid.roll]) {
|
||||
const wrapped = ((value % 360) + 360) % 360;
|
||||
assert.ok(wrapped < 1e-9 || 360 - wrapped < 1e-9, `got ${value}`);
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
resolveCameraPose,
|
||||
resolveCameraMove,
|
||||
sampleCameraMove,
|
||||
normalizeCameraLongitude,
|
||||
} from './camera.js';
|
||||
|
||||
const DEFAULT_SHOT_DURATION_SEC = 4;
|
||||
@@ -40,12 +41,19 @@ export function cameraAtProgress(fromCamera, toCamera, progress) {
|
||||
const lerp = (from, to) => Number(from) + (Number(to) - Number(from)) * eased;
|
||||
const lerpAngle = (from, to) => {
|
||||
const start = Number(from) || 0;
|
||||
const delta = ((Number(to) - start + 540) % 360) - 180;
|
||||
// Double modulo keeps the delta in [-180, 180) for any authored angles,
|
||||
// as sampleCameraMove does; a single % goes negative below -540.
|
||||
const delta = ((((Number(to) - start + 540) % 360) + 360) % 360) - 180;
|
||||
return start + delta * eased;
|
||||
};
|
||||
return {
|
||||
lat: lerp(source.lat, target.lat),
|
||||
lon: lerp(source.lon, target.lon),
|
||||
lon:
|
||||
t === 0
|
||||
? Number(source.lon)
|
||||
: t === 1
|
||||
? Number(target.lon)
|
||||
: normalizeCameraLongitude(lerpAngle(source.lon, target.lon)),
|
||||
alt: lerp(source.alt, target.alt),
|
||||
heading: lerpAngle(source.heading, target.heading),
|
||||
pitch: lerp(source.pitch, target.pitch),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MACOS_CHROME, chromeExecutablePath } from '../scripts/qa-chrome.mjs';
|
||||
|
||||
test('gates keep macOS Chrome where it is installed', () => {
|
||||
assert.equal(
|
||||
chromeExecutablePath({ env: {}, exists: (p) => p === MACOS_CHROME }),
|
||||
MACOS_CHROME,
|
||||
);
|
||||
});
|
||||
|
||||
test('gates fall back to Puppeteer’s browser without macOS Chrome', () => {
|
||||
assert.equal(
|
||||
chromeExecutablePath({ env: {}, exists: () => false }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test('an explicit executable path wins', () => {
|
||||
assert.equal(
|
||||
chromeExecutablePath({
|
||||
env: { PUPPETEER_EXECUTABLE_PATH: 'C:\\Chrome\\chrome.exe' },
|
||||
exists: () => true,
|
||||
}),
|
||||
'C:\\Chrome\\chrome.exe',
|
||||
);
|
||||
});
|
||||
+14
-4
@@ -106,10 +106,20 @@ export function validateValue(schema, value, path = 'arguments') {
|
||||
fail('must be a string');
|
||||
break;
|
||||
}
|
||||
if (schema.minLength != null && value.length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && value.length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
if (schema.minLength != null || schema.maxLength != null) {
|
||||
// Code points, as JSON Schema counts. Counting stops once both bounds
|
||||
// are decided, so a huge argument costs no more than its limit.
|
||||
const limit = Math.max(
|
||||
schema.minLength ?? 0,
|
||||
schema.maxLength != null ? schema.maxLength + 1 : 0,
|
||||
);
|
||||
let length = 0;
|
||||
for (const _ of value) if (++length >= limit) break;
|
||||
if (schema.minLength != null && length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
}
|
||||
if (
|
||||
schema.pattern !== undefined &&
|
||||
!new RegExp(schema.pattern, 'u').test(value)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validateValue } from './schema.js';
|
||||
test('tool string schemas count Unicode characters rather than UTF-16 units', () => {
|
||||
const value = '𠮷野家😀';
|
||||
assert.deepEqual(validateValue({ type: 'string', maxLength: 4 }, value), []);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', minLength: 5 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', maxLength: 3 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateValue({ type: 'string', minLength: 4, maxLength: 4 }, 'abcd'),
|
||||
[],
|
||||
);
|
||||
assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1);
|
||||
});
|
||||
|
||||
test('tool string bounds hold at their edges and on huge arguments', () => {
|
||||
const string = (bounds) => ({ type: 'string', ...bounds });
|
||||
assert.deepEqual(validateValue(string({ maxLength: 3 }), '😀😀😀'), []);
|
||||
assert.equal(validateValue(string({ maxLength: 3 }), '😀😀😀😀').length, 1);
|
||||
assert.deepEqual(validateValue(string({ minLength: 3 }), '😀😀😀'), []);
|
||||
assert.deepEqual(validateValue(string({}), 'x'.repeat(1e6)), []);
|
||||
// Counting all of it takes roughly 0.7 s; stopping at the limit takes none.
|
||||
const huge = 'x'.repeat(1e8);
|
||||
const started = performance.now();
|
||||
assert.equal(validateValue(string({ maxLength: 60 }), huge).length, 1);
|
||||
assert.ok(performance.now() - started < 200, 'stops counting past the limit');
|
||||
});
|
||||
Reference in new issue
Block a user