Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ jobs:
- run: bun run games:clone
env:
GAMES_CLONE_TOKEN: ${{ secrets.GAMES_CLONE_TOKEN }}
- run: bun test packages apps/dev scripts
# Browser and physics suites cold-start Chromium/WASM; on a loaded runner the first test in a
# file can pass Bun's 5 s default and cascade through the file. Tests with their own timeout keep it.
- run: bun test --timeout 20000 packages apps/dev scripts

web-build:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions changes/gravel-junctions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- City districts (`@jgengine/shell` `CityRenderer`) draw a crossing of gravel lanes as gravel instead of a dark asphalt slab; a crossing that touches any paved street stays asphalt.
5 changes: 4 additions & 1 deletion packages/shell/src/scene/CityRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { InstancedBuildings, type InstancedBuildingPlacement } from "../structur
import { getBuildingKit } from "../structures/buildingKitRegistry";
import { createCityWindowMaterial } from "./cityWindowMaterial";
import { registerSceneKindRenderer, type SceneKindRenderContext } from "./sceneKindRenderers";
import { junctionSurface } from "./cityJunctionSurface";

const ASPHALT_COLOR = "#3a3b3d";
const GRAVEL_COLOR = "#93815f";
Expand Down Expand Up @@ -825,7 +826,9 @@ function OneCity({ object, context }: { object: SceneKindObject; context: SceneK
const crossIndex = trimmed.junctionIndices[j]!;
const scuff = (hashString(`cross:${crossIndex}`) % 512) / 512;
wearColor.setRGB(0.76 + scuff * 0.18, 0.77 + scuff * 0.17, 0.8 + scuff * 0.16);
patches.addMesh(trimmed.junctions[j]!, wearColor);
const cross = resolved.intersections[crossIndex];
if (cross !== undefined && junctionSurface(cross, resolved.streets) === "gravel") gravel.addMesh(trimmed.junctions[j]!);
else patches.addMesh(trimmed.junctions[j]!, wearColor);
}
for (const paint of buildIntersectionMarkings(trimmed, deckSampler, {
mouthClearance: 1.25,
Expand Down
24 changes: 24 additions & 0 deletions packages/shell/src/scene/cityJunctionSurface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test";
import { junctionSurface } from "./cityJunctionSurface";

const street = (surface: "asphalt" | "gravel", points: [number, number][]) => ({ surface, width: 6, points });
const cross = { x: 0, z: 0, radius: 4 };

describe("junctionSurface", () => {
test("a crossing of gravel lanes stays gravel", () => {
expect(junctionSurface(cross, [street("gravel", [[-20, 0], [20, 0]]), street("gravel", [[0, -20], [0, 20]])])).toBe("gravel");
});

test("a gravel lane meeting a paved main is asphalt", () => {
expect(junctionSurface(cross, [street("asphalt", [[-20, 0], [20, 0]]), street("gravel", [[0, 0], [0, 20]])])).toBe("asphalt");
});

test("distant paved streets do not pave a gravel crossing", () => {
const far = street("asphalt", [[-20, 40], [20, 40]]);
expect(junctionSurface(cross, [street("gravel", [[-20, 0], [20, 0]]), far])).toBe("gravel");
});

test("a crossing no street reaches falls back to asphalt", () => {
expect(junctionSurface(cross, [street("gravel", [[-20, 50], [20, 50]])])).toBe("asphalt");
});
});
36 changes: 36 additions & 0 deletions packages/shell/src/scene/cityJunctionSurface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { CityIntersection, CityStreet } from "@jgengine/core/world/cityKind";

/**
* Surface for a crossing's welded patch: gravel only when every street running through the crossing
* is gravel, so an unpaved hamlet's junctions stay gravel while a lane meeting a paved main stays asphalt.
* @internal
*/
export function junctionSurface(
cross: Pick<CityIntersection, "x" | "z" | "radius">,
streets: readonly Pick<CityStreet, "points" | "width" | "surface">[],
): CityStreet["surface"] {
let touched = false;
for (const street of streets) {
const reach = cross.radius + street.width * 0.5 + 0.5;
if (distanceToPath(cross.x, cross.z, street.points) > reach) continue;
if (street.surface !== "gravel") return "asphalt";
touched = true;
}
return touched ? "gravel" : "asphalt";
}

function distanceToPath(x: number, z: number, points: CityStreet["points"]): number {
if (points.length === 0) return Infinity;
if (points.length === 1) return Math.hypot(x - points[0]![0], z - points[0]![1]);
let best = Infinity;
for (let i = 1; i < points.length; i += 1) {
const [ax, az] = points[i - 1]!;
const [bx, bz] = points[i]!;
const dx = bx - ax;
const dz = bz - az;
const len2 = dx * dx + dz * dz;
const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (z - az) * dz) / len2));
best = Math.min(best, Math.hypot(x - (ax + dx * t), z - (az + dz * t)));
}
return best;
}
1 change: 1 addition & 0 deletions scripts/export-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@
"./scene/StudioStage",
"./scene/WaterRenderer",
"./scene/builtinSceneKindRenderers",
"./scene/cityJunctionSurface",
"./scene/cityWindowMaterial",
"./scene/sceneKindRenderers",
"./settings/QuickControls",
Expand Down
Loading