The office learns where it stands, and the sky stops being a backdrop
**Aircraft actually move now, and the reason they did not is the headline.**
`HttpFlights` holds a frozen snapshot between network refreshes and is
polled at 1 Hz, so a live feed handed the layer the same position five to
fifteen times and then jumped. `span` therefore measured the poll interval
rather than the gap between the two positions that differ, the teleport
test saw an airliner covering 36 units in a "second" against a ceiling of
8, and **every live track's history was wiped on every refresh** — so no
aircraft on the deployed site could ever grow a trail, however long
TRAIL_POINTS was set. Skipping the repeat fixes the motion and the trail
at once. Trails then go to 72 points / 240 s, which is about seventy
seconds of flying.
Three more defects in the same file, found while looking: trail
truncation dropped the segments nearest the aircraft (leaving a streak
with no aeroplane attached), MAX_TRACKS was declared and never enforced,
and one missing target deleted its whole trail. The buffer now uploads
only what it wrote, rather than 46 MB/s of untouched array.
**You can get above the constellation.** Dome to 1.05 board *radii* and
the orbit to 2.0 spans. Radii, not spans: scene space is centred on the
city and the Bay Area board runs forty kilometres down the peninsula, so
the furthest corner is 0.94 spans out where the half-diagonal is 0.65 —
sized off the half-diagonal the dome sat inside its own city. The far
plane goes to 4 spans to stop clipping the sky from off-centre chapters,
and `PointsMaterial` defaults `fog: true`, which was quietly dimming the
whole constellation with the city's haze.
**An office can say where it stands.** `Office.site` — lat, lng, height
above the ground outside, and the compass bearing the pack's −Z points
along — and with one it gets the same sun the city does, a sky, and a
horizon at `-elevation`. CONTRACT §4 reserved this as "a later
refinement"; it is taken up rather than overturned, and `daylight.ts`
computes no light of its own. It does the two things a room needs that a
map does not: turn the sun into the building's frame, and move the fog
outdoors before it greys out the far wall.
Two buildings now, and they are deliberately unalike: Lumbridge HQ 188 m
up a Transbay tower facing 205°, and **Frontier Valley**, a startup in a
hangar at Alameda Point — one room, 54 x 30 m, nine metres to the
trusses, four metres above reclaimed ground.
Floor-to-floor in the reference pack is now 16.8 m: the interstitial is
ten times a real one, so the space between the slabs is somewhere things
can hang. It is frankly not architecture, `PLENUM` is the one number to
change, and the file says so.
Also fixed, all found by review rather than by looking at the screen:
- `sun.shadow.camera.updateProjectionMatrix()` was never called, so
three's default ±5 unit box has been in force this whole time and
every `shadowExtent` this repo passes — including the city's ±752 —
has been silently ignored.
- A missing aircraft was kept alive by the new grace period and *drawn*,
so it froze in mid-air at full opacity for 32 s.
- Frontier Valley's mezzanine was a `Room`, which carries no height: its
slab lay on the concrete, its chairs floated 4.4 m over it, and its
balustrade fenced off a patch of ground floor. It is a `Level`.
- Overlapping floor slabs z-fought. The format permits overlap and
resolves later-first, so `shell.ts` now lifts a slab a hair per
earlier slab it overlaps — and by nothing at all in a pack, like the
reference office, whose rooms only ever abut.
- `switchOffice` bypassed the `entering` guard (leaking a whole scene
per double-click) and tore down the old room before knowing the new
one would load, with no way back.
Known and not fixed: raising MAX_SPAN to 30 s doubles the worst-case
re-base snap when a feed's gap shortens. It is bounded, pre-existing in
kind, and the fix wants carrying the live head into the next leg.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Turning the sun into a building's frame.
|
||||
*
|
||||
* This is eleven lines of trigonometry with four chances to get a sign wrong,
|
||||
* and every one of them produces a scene that renders perfectly and is lit from
|
||||
* the wrong side. There is no visual tell: an office lit from the east at
|
||||
* sunset looks exactly as plausible as one lit from the west, unless you happen
|
||||
* to know which wall the pack put its windows in.
|
||||
*
|
||||
* So the cases below are all stated as compass facts — "the sun is due east, the
|
||||
* building faces east, therefore the sun is straight ahead" — rather than as
|
||||
* expected numbers, because a number copied out of a failing run is not a test.
|
||||
*
|
||||
* The frame, for reading these: **−Z is north, +X is east**, in both the world
|
||||
* and the building. `heading` is the bearing the building's −Z points along.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { intoBuildingFrame, officeDaylight } from "../interiors/daylight.ts";
|
||||
import type { LightingState } from "../engine/types.ts";
|
||||
import type { OfficeSite } from "../interiors/types.ts";
|
||||
|
||||
/** Unit vector pointing at a compass bearing, in the frame described above. */
|
||||
function bearing(deg: number): [number, number, number] {
|
||||
const r = (deg * Math.PI) / 180;
|
||||
return [Math.sin(r), 0, -Math.cos(r)];
|
||||
}
|
||||
|
||||
/** The bearing a frame-vector points at, back out again. */
|
||||
function bearingOf([x, , z]: [number, number, number]): number {
|
||||
return (((Math.atan2(x, -z) * 180) / Math.PI) + 360) % 360;
|
||||
}
|
||||
|
||||
function close(actual: number, expected: number, message: string) {
|
||||
const delta = Math.abs(((actual - expected + 540) % 360) - 180);
|
||||
assert.ok(delta < 1e-6, `${message}: got ${actual}, expected ${expected}`);
|
||||
}
|
||||
|
||||
describe("rotating the sun into the building's frame", () => {
|
||||
it("changes nothing for a building whose north really is north", () => {
|
||||
for (const deg of [0, 45, 90, 180, 270]) {
|
||||
const v = bearing(deg);
|
||||
assert.deepEqual(intoBuildingFrame(v, 0), v);
|
||||
}
|
||||
});
|
||||
|
||||
it("puts a sun dead ahead when the building faces it", () => {
|
||||
// Building faces east; sun is due east. In the building's own frame that is
|
||||
// straight out of the front, which is bearing 0 — its own "north".
|
||||
close(bearingOf(intoBuildingFrame(bearing(90), 90)), 0, "sun should be ahead");
|
||||
});
|
||||
|
||||
it("puts a sun behind when the building faces away from it", () => {
|
||||
// Building faces north, sun due south.
|
||||
close(bearingOf(intoBuildingFrame(bearing(180), 0)), 180, "sun should be behind");
|
||||
// Building faces south, sun due north — the same physical arrangement.
|
||||
close(bearingOf(intoBuildingFrame(bearing(0), 180)), 180, "sun should be behind");
|
||||
});
|
||||
|
||||
it("subtracts the heading, rather than adding it", () => {
|
||||
// The sign error that produces a plausible-looking, mirrored building. A sun
|
||||
// at 90 seen from a building facing 30 is 60 off its nose, not 120.
|
||||
close(bearingOf(intoBuildingFrame(bearing(90), 30)), 60, "heading should subtract");
|
||||
});
|
||||
|
||||
it("leaves the sun's height alone", () => {
|
||||
// Rotating about the vertical cannot change how high the sun is. A rig that
|
||||
// got this wrong would have the sun rise and set as the building turned.
|
||||
const up: [number, number, number] = [0.3, 0.9, 0.31];
|
||||
for (const heading of [0, 37, 90, 205, 359]) {
|
||||
assert.equal(intoBuildingFrame(up, heading)[1], 0.9);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves length, so the direction stays a direction", () => {
|
||||
const v: [number, number, number] = [0.48, 0.72, -0.5];
|
||||
const length = Math.hypot(...v);
|
||||
for (const heading of [17, 205, 300]) {
|
||||
const out = intoBuildingFrame(v, heading);
|
||||
assert.ok(Math.abs(Math.hypot(...out) - length) < 1e-12);
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips: rotating by h and then by -h is the identity", () => {
|
||||
const v: [number, number, number] = [0.2, 0.83, -0.52];
|
||||
const there = intoBuildingFrame(v, 205);
|
||||
const back = intoBuildingFrame(there, -205);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
assert.ok(Math.abs((back[i] as number) - (v[i] as number)) < 1e-12);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("moving the weather outdoors", () => {
|
||||
const site: OfficeSite = { lat: 37.79, lng: -122.4, elevation: 188, heading: 0 };
|
||||
|
||||
const cityState: LightingState = {
|
||||
sun: { direction: [0.3, 0.9, 0.31], color: 0xffffff, intensity: 1 },
|
||||
hemisphere: { sky: 0x8899aa, ground: 0x404040, intensity: 1 },
|
||||
ambient: { color: 0xffffff, intensity: 0.3 },
|
||||
sky: { top: 0x223344, horizon: 0x99aabb },
|
||||
// A city fog, in city units: this would sit well inside a 50 m room.
|
||||
fog: { color: 0xaabbcc, near: 1150, far: 2800 },
|
||||
};
|
||||
|
||||
/**
|
||||
* The failure this guards is not subtle once you see it and is invisible until
|
||||
* you do: an office rendered with a fog that starts 1,150 *metres* away is
|
||||
* fine, and one rendered with a fog that starts at 1,150 *scene units* of a
|
||||
* city is fine too — but an office is 1 unit to the metre, so a city fog
|
||||
* dropped into one greys out the far wall and everybody standing at it.
|
||||
*/
|
||||
it("starts the fog outside the building", () => {
|
||||
const out = officeDaylight(cityState, site);
|
||||
assert.ok(out.fog, "an office with a site keeps its fog");
|
||||
assert.ok(out.fog.near > 60, `fog starts at ${out.fog.near} m, inside the building`);
|
||||
assert.ok(out.fog.far > out.fog.near);
|
||||
});
|
||||
|
||||
it("keeps the colour the atmosphere chose", () => {
|
||||
assert.equal(officeDaylight(cityState, site).fog?.color, 0xaabbcc);
|
||||
});
|
||||
|
||||
/**
|
||||
* The sky is a screen-space gradient and the ground plane converges on the fog
|
||||
* colour, so the two meet at whatever screen row the world horizon lands on —
|
||||
* which moves as the camera orbits. Unless they meet with the *same* colour,
|
||||
* that line is a visible step across the frame.
|
||||
*/
|
||||
it("pins the sky's horizon to the fog colour, so the join is invisible", () => {
|
||||
const out = officeDaylight(cityState, site);
|
||||
assert.equal(out.sky?.horizon, cityState.fog?.color);
|
||||
});
|
||||
|
||||
it("leaves the top of the sky to the atmosphere", () => {
|
||||
assert.equal(officeDaylight(cityState, site).sky?.top, cityState.sky?.top);
|
||||
});
|
||||
|
||||
it("keeps a skyless state skyless", () => {
|
||||
assert.equal(officeDaylight({ ...cityState, sky: null }, site).sky, null);
|
||||
});
|
||||
|
||||
it("keeps a fogless state fogless", () => {
|
||||
const clear = officeDaylight({ ...cityState, fog: null }, site);
|
||||
assert.equal(clear.fog, null);
|
||||
});
|
||||
|
||||
it("does not mutate what it was given", () => {
|
||||
const before = JSON.stringify(cityState);
|
||||
officeDaylight(cityState, { ...site, heading: 205 });
|
||||
assert.equal(JSON.stringify(cityState), before);
|
||||
});
|
||||
});
|
||||
+197
-17
@@ -16,7 +16,7 @@
|
||||
* room behind it is sealed, and the only evidence is a line in
|
||||
* `plan.problems` that nothing reads. That exact bug happened once while the
|
||||
* wing was being written.
|
||||
* - **The balustrade.** The gallery is 4.2 m above terrazzo. `Plan` derives
|
||||
* - **The balustrade.** The gallery is 16.8 m above terrazzo. `Plan` derives
|
||||
* the walk collider from the wall list, so the difference between a
|
||||
* balustrade and a decorative rail is whether it is in that list — and both
|
||||
* look identical in a screenshot.
|
||||
@@ -26,6 +26,7 @@ import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { Plan } from "../interiors/plan.ts";
|
||||
import LUMBRIDGE_HQ from "../offices/lumbridge-hq.ts";
|
||||
import FRONTIER_VALLEY from "../offices/frontier-valley.ts";
|
||||
|
||||
const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
|
||||
|
||||
@@ -38,13 +39,28 @@ describe("the reference pack resolves cleanly", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("has both storeys, at floor-to-floor and not floor-to-ceiling", () => {
|
||||
assert.deepEqual(
|
||||
plan.levels.map((l) => [l.id, l.floorY]),
|
||||
[
|
||||
["level-1", 0],
|
||||
["level-2", 4.2],
|
||||
],
|
||||
/**
|
||||
* The relationship, not the number.
|
||||
*
|
||||
* Floor-to-floor is deliberately exaggerated in this pack — see `PLENUM` in
|
||||
* `lumbridge-hq.ts` — and an assertion on the literal would have to be edited
|
||||
* every time somebody dials it, which makes it a change-detector rather than a
|
||||
* test. What must stay true is that level 2 sits a clear interstitial *above*
|
||||
* level 1's ceiling, and never at or below it: `elevation` is floor-to-floor,
|
||||
* and setting it to the ceiling height is the classic way to bury one storey's
|
||||
* slab inside the one below.
|
||||
*/
|
||||
it("stacks the storeys floor-to-floor, not floor-to-ceiling", () => {
|
||||
const [first, second] = plan.levels;
|
||||
assert.ok(first && second, "both storeys should resolve");
|
||||
assert.equal(first.id, "level-1");
|
||||
assert.equal(second.id, "level-2");
|
||||
assert.equal(first.floorY, 0);
|
||||
|
||||
const ceiling = LUMBRIDGE_HQ.levels[0]?.wallHeight ?? 0;
|
||||
assert.ok(
|
||||
second.floorY > ceiling,
|
||||
`level 2's slab at ${second.floorY} m is inside level 1, whose ceiling is at ${ceiling} m`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -188,7 +204,7 @@ describe("the gallery balustrade", () => {
|
||||
for (const [name, from, to] of edges) {
|
||||
assert.ok(
|
||||
plan.blocked("level-2", from, to, 0.3),
|
||||
`the ${name} edge of the gallery lets a walker off a 4.2 m drop`,
|
||||
`the ${name} edge of the gallery lets a walker off a 16.8 m drop`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -218,20 +234,184 @@ describe("the commons is open to both storeys", () => {
|
||||
});
|
||||
|
||||
it("is enclosed to two storeys rather than one", () => {
|
||||
const level1 = plan.levels.find((l) => l.id === "level-1");
|
||||
assert.ok(level1);
|
||||
// 7.0 m: one storey of 4.2 plus one room of 2.8. Asserted against the walls
|
||||
// as resolved, so a level default leaking through would be caught.
|
||||
//
|
||||
const [level1, level2] = plan.levels;
|
||||
assert.ok(level1 && level2);
|
||||
|
||||
// The height the void *should* be, derived rather than typed: everything up
|
||||
// to level 2's slab, plus level 2's own room. That is what "open to both
|
||||
// storeys" means, and it stays true whatever the interstitial is set to.
|
||||
const ceiling = LUMBRIDGE_HQ.levels[1]?.wallHeight ?? 0;
|
||||
const expected = level2.floorY + ceiling;
|
||||
|
||||
// `solid` only. The splitting pass emits a run per piece, so a wall with a
|
||||
// door in it also yields a `lintel` over the opening — 4.6 m of wall above a
|
||||
// 2.4 m head, which is correct and is not the wall's height.
|
||||
// door in it also yields a `lintel` over the opening — correct, and not the
|
||||
// wall's height.
|
||||
const wingWalls = level1.runs.filter(
|
||||
(run) => run.wallId.startsWith("wing-") && run.role === "solid",
|
||||
);
|
||||
assert.ok(wingWalls.length > 0, "the wing has no walls");
|
||||
for (const run of wingWalls) {
|
||||
assert.equal(run.top - run.bottom, 7, `${run.wallId} is ${run.top - run.bottom} m tall`);
|
||||
assert.equal(
|
||||
run.top - run.bottom,
|
||||
expected,
|
||||
`${run.wallId} is ${run.top - run.bottom} m tall, not the ${expected} m the void needs`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The second pack, held to the same bar as the first.
|
||||
*
|
||||
* A shipped pack that nothing asserts on is a pack that quietly stops resolving
|
||||
* the first time somebody edits a shared constant. It gets the two checks that
|
||||
* catch that — clean resolution and a walkable building — plus the one thing
|
||||
* that is specific to it: it is a *hangar*, and the point of shipping it is that
|
||||
* the format describes one room fifty-four metres across as readily as it
|
||||
* describes a corridor with fifteen doors off it.
|
||||
*/
|
||||
describe("the Frontier Valley pack", () => {
|
||||
const fv = new Plan(FRONTIER_VALLEY, { warn: false });
|
||||
|
||||
it("resolves with no problems", () => {
|
||||
assert.deepEqual(
|
||||
fv.problems.map((p) => `${p.where}: ${p.message} (${p.action})`),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* The deck is a level, and that is the fix for a real defect rather than a
|
||||
* modelling preference: authored as a `Room` it had no floor height, so its
|
||||
* slab lay on the concrete while its furniture floated 4.4 m over it.
|
||||
*/
|
||||
it("puts the mezzanine on its own level, at deck height", () => {
|
||||
assert.deepEqual(
|
||||
fv.levels.map((l) => [l.id, l.floorY]),
|
||||
[
|
||||
["level-1", 0],
|
||||
["level-mezz", 4.4],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it("has unique seat ids, which must not collide with the other pack's", () => {
|
||||
const mine = fv.allSeats().map((s) => s.id);
|
||||
assert.equal(new Set(mine).size, mine.length);
|
||||
// Not a hard requirement of the format — ids are unique per *building* — but
|
||||
// a deployment serving both from one presence API would find out the hard
|
||||
// way, and the two packs cost nothing by staying disjoint.
|
||||
const theirs = new Set(plan.allSeats().map((s) => s.id));
|
||||
const shared = mine.filter((id) => theirs.has(id));
|
||||
assert.deepEqual(shared, [], "seat ids shared between the two shipped packs");
|
||||
});
|
||||
|
||||
it("can be walked from the personnel door to every room", () => {
|
||||
const STEP = 0.1;
|
||||
const RADIUS = 0.3;
|
||||
const b = fv.bounds;
|
||||
const cols = Math.ceil(b.width / STEP);
|
||||
const rows = Math.ceil(b.depth / STEP);
|
||||
const at = (c: number, r: number) => ({ x: b.minX + c * STEP, z: b.minZ + r * STEP });
|
||||
|
||||
const room: (string | null)[] = new Array(cols * rows).fill(null);
|
||||
for (let r = 0; r < rows; r += 1) {
|
||||
for (let c = 0; c < cols; c += 1) {
|
||||
room[r * cols + c] = fv.roomAt("level-1", at(c, r))?.id ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// Just inside the west gable's personnel door.
|
||||
const sc = Math.round((1.0 - b.minX) / STEP);
|
||||
const sr = Math.round((14.4 - b.minZ) / STEP);
|
||||
assert.ok(room[sr * cols + sc], "the way in is not inside a room");
|
||||
|
||||
const seen = new Uint8Array(cols * rows);
|
||||
const queue: [number, number][] = [[sc, sr]];
|
||||
seen[sr * cols + sc] = 1;
|
||||
while (queue.length > 0) {
|
||||
const [c, r] = queue.pop() as [number, number];
|
||||
for (const [dc, dr] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
const nc = c + (dc as number);
|
||||
const nr = r + (dr as number);
|
||||
if (nc < 0 || nr < 0 || nc >= cols || nr >= rows) continue;
|
||||
const i = nr * cols + nc;
|
||||
if (seen[i] === 1 || room[i] === null) continue;
|
||||
if (fv.blocked("level-1", at(c, r), at(nc, nr), RADIUS)) continue;
|
||||
seen[i] = 1;
|
||||
queue.push([nc, nr]);
|
||||
}
|
||||
}
|
||||
|
||||
const reached = new Set<string>();
|
||||
for (let i = 0; i < seen.length; i += 1) {
|
||||
const id = room[i];
|
||||
if (seen[i] === 1 && id != null) reached.add(id);
|
||||
}
|
||||
// Level 1 only. The mezzanine is a level of its own now and is reached by a
|
||||
// stair this pack does not model, so a flood fill across the slab cannot and
|
||||
// should not walk onto it.
|
||||
const sealed = fv.levels[0]!.rooms.map((r) => r.id).filter((id) => !reached.has(id));
|
||||
assert.deepEqual(sealed, [], "sealed rooms in the hangar");
|
||||
});
|
||||
|
||||
it("guards the mezzanine edge, which is a 4.4 m drop onto concrete", () => {
|
||||
// On the deck's own level — the balustrade belongs to it, not to the slab
|
||||
// below, which is the whole point of the deck being a level.
|
||||
assert.ok(fv.blocked("level-mezz", { x: 39.0, z: 25.0 }, { x: 37.0, z: 25.0 }, 0.3));
|
||||
assert.ok(fv.blocked("level-mezz", { x: 45.0, z: 21.0 }, { x: 45.0, z: 19.0 }, 0.3));
|
||||
});
|
||||
|
||||
/**
|
||||
* And the deck is genuinely up there. `Plan` resolves a seat's `y` from its
|
||||
* level's floor, so this is the assertion that would have caught the original
|
||||
* defect: authored as a ground-floor room, every one of these was at y = 0
|
||||
* with a chair drawn 4.4 m above it.
|
||||
*/
|
||||
it("stands its occupants on the deck rather than on the concrete", () => {
|
||||
for (const id of ["mezz-01", "mezz-02", "mezz-03"]) {
|
||||
assert.equal(fv.seat(id)?.y, 4.4, `${id} is not on the deck`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Both shipped packs declare where they stand, and the two are deliberately
|
||||
* nothing alike — which is the entire argument for the field existing.
|
||||
*/
|
||||
describe("the sites", () => {
|
||||
it("are both declared", () => {
|
||||
assert.ok(LUMBRIDGE_HQ.site, "Lumbridge HQ has no site");
|
||||
assert.ok(FRONTIER_VALLEY.site, "Frontier Valley has no site");
|
||||
});
|
||||
|
||||
it("put one high in the air and one on the ground", () => {
|
||||
assert.ok(
|
||||
(LUMBRIDGE_HQ.site?.elevation ?? 0) > 100,
|
||||
"the tower office should be a long way up",
|
||||
);
|
||||
assert.ok(
|
||||
(FRONTIER_VALLEY.site?.elevation ?? 999) < 20,
|
||||
"the hangar should be near the ground",
|
||||
);
|
||||
});
|
||||
|
||||
it("carry headings inside the compass", () => {
|
||||
for (const pack of [LUMBRIDGE_HQ, FRONTIER_VALLEY]) {
|
||||
const h = pack.site?.heading ?? 0;
|
||||
assert.ok(h >= 0 && h < 360, `${pack.id} has a heading of ${h}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("are both on the board the city view draws", () => {
|
||||
// Not a format requirement — an office may stand anywhere — but both of
|
||||
// these are meant to be places in *this* product's San Francisco, and a
|
||||
// coordinate typo that put one in Nevada would otherwise render fine.
|
||||
for (const pack of [LUMBRIDGE_HQ, FRONTIER_VALLEY]) {
|
||||
const site = pack.site;
|
||||
assert.ok(site);
|
||||
assert.ok(site.lat > 37.2 && site.lat < 38.2, `${pack.id} latitude ${site.lat}`);
|
||||
assert.ok(site.lng > -122.8 && site.lng < -121.8, `${pack.id} longitude ${site.lng}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user