@jgengine/shell 0.13.0 → 0.15.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +529 -1
- package/dist/CombatPresentation.js +2 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +16 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +15 -1
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -0
- package/dist/devtools/collisionDebugMath.js +14 -5
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +38 -5
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/postfx/postfxOverlay.d.ts +24 -0
- package/dist/postfx/postfxOverlay.js +35 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EntityPreview.d.ts +53 -0
- package/dist/render/EntityPreview.js +23 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +52 -10
- package/dist/render/measureBounds.js +4 -0
- package/dist/render/measureCollisionMesh.d.ts +26 -0
- package/dist/render/measureCollisionMesh.js +108 -0
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +8 -3
- package/skills/jgengine-ui/api.md +732 -23
- package/skills/jgengine-ui/capabilities.md +371 -2
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { SkyDome } from "./Daylight.js";
|
|
6
|
+
import { daylightStateAt } from "./daylightCycle.js";
|
|
7
|
+
const HEMI_GROUND = "#4c6b34";
|
|
8
|
+
/**
|
|
9
|
+
* Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of
|
|
10
|
+
* lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's
|
|
11
|
+
* top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt`
|
|
12
|
+
* geometry. This is the turnkey presentation seam — a game mounts this one component in its
|
|
13
|
+
* scene and gets a moving, color-graded day-night sky from the serializable model, with no
|
|
14
|
+
* hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are
|
|
15
|
+
* free-form and never interpreted here.
|
|
16
|
+
*
|
|
17
|
+
* @capability day-night-sky drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
|
|
18
|
+
*/
|
|
19
|
+
export function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }) {
|
|
20
|
+
const skyMaterialRef = useRef(null);
|
|
21
|
+
const sunRef = useRef(null);
|
|
22
|
+
const hemiRef = useRef(null);
|
|
23
|
+
const initial = useMemo(() => cycle.sample(), [cycle]);
|
|
24
|
+
const initialSun = useMemo(() => daylightStateAt(initial.dayFraction).sunPosition, [initial.dayFraction]);
|
|
25
|
+
useFrame(() => {
|
|
26
|
+
const sample = cycle.sample();
|
|
27
|
+
const geometry = daylightStateAt(sample.dayFraction);
|
|
28
|
+
const material = skyMaterialRef.current;
|
|
29
|
+
if (material !== null) {
|
|
30
|
+
material.uniforms.topColor.value.set(sample.lightColor);
|
|
31
|
+
material.uniforms.bottomColor.value.set(sample.color);
|
|
32
|
+
material.uniforms.uSunColor.value.set(sample.lightColor);
|
|
33
|
+
material.uniforms.uSunDirection.value
|
|
34
|
+
.set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2])
|
|
35
|
+
.normalize();
|
|
36
|
+
material.uniforms.uSunIntensity.value = sample.intensity;
|
|
37
|
+
}
|
|
38
|
+
const sun = sunRef.current;
|
|
39
|
+
if (sun !== null) {
|
|
40
|
+
sun.position.set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2]);
|
|
41
|
+
sun.intensity = sample.intensity * keyLightIntensity;
|
|
42
|
+
sun.color.set(sample.lightColor);
|
|
43
|
+
}
|
|
44
|
+
const hemi = hemiRef.current;
|
|
45
|
+
if (hemi !== null) {
|
|
46
|
+
hemi.intensity = sample.intensity * ambientIntensity;
|
|
47
|
+
hemi.color.set(sample.lightColor);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.lightColor, horizonColor: initial.color, sunDirection: initialSun, sunColor: initial.lightColor, sunIntensity: initial.intensity, materialRef: skyMaterialRef, ...(radius === undefined ? {} : { radius }), ...(hazeStrength === undefined ? {} : { hazeStrength }), ...(sunGlowStrength === undefined ? {} : { sunGlowStrength }) }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [initial.lightColor, HEMI_GROUND, initial.intensity * ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initialSun, intensity: initial.intensity * keyLightIntensity, color: initial.lightColor, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 })] })) : null] }));
|
|
51
|
+
}
|
|
@@ -16,13 +16,15 @@ export interface SkyDomeProps {
|
|
|
16
16
|
sunIntensity?: number;
|
|
17
17
|
/** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
|
|
18
18
|
hazeStrength?: number;
|
|
19
|
+
/** Procedural cloud-band opacity: 0 is a clear sky, 1 is heavy overcast. Default 0.35. */
|
|
20
|
+
cloudiness?: number;
|
|
19
21
|
/** Sun-glow brightness multiplier around the sun disc. Default 1. */
|
|
20
22
|
sunGlowStrength?: number;
|
|
21
23
|
/** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
|
|
22
24
|
materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
|
|
23
25
|
}
|
|
24
26
|
/** @internal */
|
|
25
|
-
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
27
|
+
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, cloudiness, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
26
28
|
export interface DaylightProps {
|
|
27
29
|
sky?: SkyDomeProps | false;
|
|
28
30
|
fog?: {
|
|
@@ -36,7 +36,7 @@ function ShadowCastingSun({ position, intensity, color, }) {
|
|
|
36
36
|
return (_jsx("directionalLight", { ref: ref, position: [position[0], position[1], position[2]], intensity: intensity, color: color, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 }));
|
|
37
37
|
}
|
|
38
38
|
/** @internal */
|
|
39
|
-
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, materialRef, } = {}) {
|
|
39
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, cloudiness = 0.35, materialRef, } = {}) {
|
|
40
40
|
const meshRef = useRef(null);
|
|
41
41
|
const material = useMemo(() => {
|
|
42
42
|
return new THREE.ShaderMaterial({
|
|
@@ -50,6 +50,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
50
50
|
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
51
51
|
uHazeStrength: { value: hazeStrength },
|
|
52
52
|
uSunGlow: { value: sunGlowStrength },
|
|
53
|
+
uCloudiness: { value: cloudiness },
|
|
53
54
|
},
|
|
54
55
|
vertexShader: `
|
|
55
56
|
varying vec3 vWorldPosition;
|
|
@@ -67,6 +68,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
67
68
|
uniform float uSunIntensity;
|
|
68
69
|
uniform float uHazeStrength;
|
|
69
70
|
uniform float uSunGlow;
|
|
71
|
+
uniform float uCloudiness;
|
|
70
72
|
uniform float offset;
|
|
71
73
|
uniform float exponent;
|
|
72
74
|
varying vec3 vWorldPosition;
|
|
@@ -86,7 +88,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
86
88
|
vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
|
|
87
89
|
float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
|
|
88
90
|
float band = smoothstep(0.08, 0.35, dir.y) * smoothstep(1.0, 0.35, dir.y);
|
|
89
|
-
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band *
|
|
91
|
+
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * uCloudiness);
|
|
90
92
|
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
91
93
|
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
92
94
|
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
@@ -97,7 +99,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
97
99
|
depthWrite: false,
|
|
98
100
|
fog: false,
|
|
99
101
|
});
|
|
100
|
-
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength]);
|
|
102
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength, cloudiness]);
|
|
101
103
|
useEffect(() => {
|
|
102
104
|
if (materialRef !== undefined)
|
|
103
105
|
materialRef.current = material;
|
|
@@ -139,6 +141,8 @@ function skyDomeShape(sky) {
|
|
|
139
141
|
...(sky.radius === undefined ? {} : { radius: sky.radius }),
|
|
140
142
|
...(sky.hazeStrength === undefined ? {} : { hazeStrength: sky.hazeStrength }),
|
|
141
143
|
...(sky.sunGlowStrength === undefined ? {} : { sunGlowStrength: sky.sunGlowStrength }),
|
|
144
|
+
...(sky.gradientExponent === undefined ? {} : { exponent: sky.gradientExponent }),
|
|
145
|
+
...(sky.cloudiness === undefined ? {} : { cloudiness: sky.cloudiness }),
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
148
|
function StaticSkyDaylight({ sky, lights }) {
|
|
@@ -6,11 +6,12 @@ import * as THREE from "three";
|
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
8
8
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
9
|
-
import { findRoadJunctions
|
|
9
|
+
import { findRoadJunctions } from "@jgengine/core/world/streets";
|
|
10
10
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
11
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
11
12
|
import { GroundPad } from "./GroundPad.js";
|
|
12
13
|
import { RoadJunctions } from "./RoadJunctions.js";
|
|
13
|
-
import { RoadRibbons } from "./RoadRibbons.js";
|
|
14
|
+
import { buildAuthoredRoadNetwork, RoadRibbons } from "./RoadRibbons.js";
|
|
14
15
|
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
15
16
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
16
17
|
import { GrassField } from "../terrain/GrassField.js";
|
|
@@ -36,8 +37,7 @@ function TexturedTerrainGround({ detail, layout, }) {
|
|
|
36
37
|
texture.needsUpdate = true;
|
|
37
38
|
}
|
|
38
39
|
}, [textures]);
|
|
39
|
-
const material =
|
|
40
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
40
|
+
const material = useDisposable(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
|
|
41
41
|
return (_jsx(CarvedTerrain, { field: layout.field, size: layout.size, segments: layout.segments, colors: layout.colors, heightRange: layout.heightRange, paletteAt: layout.paletteAt, center: layout.center, roughness: 0.94, surfaceMaterial: material }));
|
|
42
42
|
}
|
|
43
43
|
function TerrainGround({ terrain, field, center, }) {
|
|
@@ -158,6 +158,6 @@ export function EnvironmentScene({ feature }) {
|
|
|
158
158
|
const islands = feature.islands ?? [];
|
|
159
159
|
const roads = feature.roads ?? [];
|
|
160
160
|
const junctions = useMemo(() => findRoadJunctions(roads), [roads]);
|
|
161
|
-
const
|
|
162
|
-
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field,
|
|
161
|
+
const roadNetwork = useMemo(() => buildAuthoredRoadNetwork(roads, junctions, (x, z) => field.sampleHeight(x, z)), [roads, junctions, field]);
|
|
162
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field, paths: roadNetwork.roadPaths[index] ?? [] }, `road-${index}`))), _jsx(RoadJunctions, { surfaces: roadNetwork.junctionSurfaces, junctions: junctions, field: field }), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field, groundColor: vegetationGroundColor(feature.terrain, field, grass) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
163
163
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type RoadJunction } from "@jgengine/core/world/streets";
|
|
2
2
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import type { JunctionSurface } from "./RoadRibbons.js";
|
|
3
4
|
/**
|
|
4
|
-
* Renders welded road intersections: one asphalt
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Renders welded road intersections: one seam-shared asphalt surface per junction (built by
|
|
6
|
+
* {@link buildAuthoredRoadNetwork}, its boundary vertices welded onto the trimmed ribbon ends so
|
|
7
|
+
* there is no overlap and no floating disc), plus white stop lines and crosswalk bars at each
|
|
8
|
+
* approach. Markings ride the {@link GROUND_DECAL_LAYERS} marking layer above the surface, with
|
|
9
|
+
* `polygonOffset`/`renderOrder` so they win the depth test on sloped ground.
|
|
7
10
|
* @internal
|
|
8
11
|
*/
|
|
9
|
-
export declare function RoadJunctions({ junctions, field, }: {
|
|
12
|
+
export declare function RoadJunctions({ surfaces, junctions, field, }: {
|
|
13
|
+
surfaces: readonly JunctionSurface[];
|
|
10
14
|
junctions: readonly RoadJunction[];
|
|
11
15
|
field: TerrainField;
|
|
12
16
|
}): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { BufferAttribute, BufferGeometry, DoubleSide } from "three";
|
|
4
|
-
import {
|
|
4
|
+
import { buildRoadRibbon, GROUND_DECAL_LAYERS } from "@jgengine/core/world/roads";
|
|
5
5
|
import { junctionMarkings } from "@jgengine/core/world/streets";
|
|
6
|
+
/** Lane paint sits the table's road→marking gap above the welded junction surface (also polygonOffset). */
|
|
7
|
+
const MARKING_LIFT = GROUND_DECAL_LAYERS.marking - GROUND_DECAL_LAYERS.road;
|
|
6
8
|
function toGeometry(positions, indices) {
|
|
7
9
|
if (positions.length === 0)
|
|
8
10
|
return null;
|
|
@@ -34,18 +36,17 @@ function mergedBars(bars, width, field, elevation) {
|
|
|
34
36
|
return toGeometry(positions, indices);
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
|
-
* Renders welded road intersections: one asphalt
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Renders welded road intersections: one seam-shared asphalt surface per junction (built by
|
|
40
|
+
* {@link buildAuthoredRoadNetwork}, its boundary vertices welded onto the trimmed ribbon ends so
|
|
41
|
+
* there is no overlap and no floating disc), plus white stop lines and crosswalk bars at each
|
|
42
|
+
* approach. Markings ride the {@link GROUND_DECAL_LAYERS} marking layer above the surface, with
|
|
43
|
+
* `polygonOffset`/`renderOrder` so they win the depth test on sloped ground.
|
|
40
44
|
* @internal
|
|
41
45
|
*/
|
|
42
|
-
export function RoadJunctions({ junctions, field, }) {
|
|
43
|
-
const patches = useMemo(() =>
|
|
44
|
-
.map((
|
|
45
|
-
|
|
46
|
-
return { color: junction.color, geometry: toGeometry(patch.positions, patch.indices) };
|
|
47
|
-
})
|
|
48
|
-
.filter((entry) => entry.geometry !== null), [junctions, field]);
|
|
46
|
+
export function RoadJunctions({ surfaces, junctions, field, }) {
|
|
47
|
+
const patches = useMemo(() => surfaces
|
|
48
|
+
.map((surface) => ({ color: surface.color, geometry: toGeometry(surface.ribbon.positions, surface.ribbon.indices) }))
|
|
49
|
+
.filter((entry) => entry.geometry !== null), [surfaces]);
|
|
49
50
|
const markings = useMemo(() => {
|
|
50
51
|
const stopBars = [];
|
|
51
52
|
const crossBars = [];
|
|
@@ -59,11 +60,11 @@ export function RoadJunctions({ junctions, field, }) {
|
|
|
59
60
|
crossBars.push(bar);
|
|
60
61
|
}
|
|
61
62
|
return {
|
|
62
|
-
stops: mergedBars(stopBars, 0.5, field, elevation +
|
|
63
|
-
crosswalks: mergedBars(crossBars, 0.55, field, elevation +
|
|
63
|
+
stops: mergedBars(stopBars, 0.5, field, elevation + MARKING_LIFT),
|
|
64
|
+
crosswalks: mergedBars(crossBars, 0.55, field, elevation + MARKING_LIFT),
|
|
64
65
|
};
|
|
65
66
|
}, [junctions, field]);
|
|
66
|
-
if (patches.length === 0)
|
|
67
|
+
if (patches.length === 0 && markings.stops === null && markings.crosswalks === null)
|
|
67
68
|
return null;
|
|
68
|
-
return (_jsxs("group", { children: [patches.map((patch, index) => (_jsx("mesh", { geometry: patch.geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: patch.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }, `junction-${index}`))), markings.stops !== null ? (_jsx("mesh", { geometry: markings.stops, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide }) })) : null, markings.crosswalks !== null ? (_jsx("mesh", { geometry: markings.crosswalks, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide }) })) : null] }));
|
|
69
|
+
return (_jsxs("group", { children: [patches.map((patch, index) => (_jsx("mesh", { geometry: patch.geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: patch.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }, `junction-${index}`))), markings.stops !== null ? (_jsx("mesh", { geometry: markings.stops, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, markings.crosswalks !== null ? (_jsx("mesh", { geometry: markings.crosswalks, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null] }));
|
|
69
70
|
}
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import type { RoadEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
|
-
import { type
|
|
2
|
+
import { type RoadJunctionInput, type RoadPoint, type RoadRibbon } from "@jgengine/core/world/roads";
|
|
3
|
+
import { type RoadJunction } from "@jgengine/core/world/streets";
|
|
3
4
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
5
|
+
/** Convert a detected {@link RoadJunction} into the structural {@link RoadJunctionInput} the trimmer wants. @internal */
|
|
6
|
+
export declare function roadJunctionInput(junction: RoadJunction): RoadJunctionInput;
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Split every path segment at the junction centers that fall on it, inserting the junction point as
|
|
9
|
+
* a real vertex so {@link trimPathAtJunctions} (which matches junctions to path vertices) can cut the
|
|
10
|
+
* ribbon back there. Authored road crossings from `findRoadJunctions` land mid-segment, not on the
|
|
11
|
+
* hand-authored vertices, so without this the trimmer would never see them.
|
|
12
|
+
* @internal
|
|
8
13
|
*/
|
|
9
|
-
export declare function
|
|
14
|
+
export declare function insertJunctionNodes(path: readonly RoadPoint[], junctions: readonly RoadJunctionInput[], tolerance?: number): RoadPoint[];
|
|
15
|
+
/** One welded asphalt junction surface plus the color it should render in. */
|
|
16
|
+
export interface JunctionSurface {
|
|
17
|
+
color: string;
|
|
18
|
+
ribbon: RoadRibbon;
|
|
19
|
+
}
|
|
20
|
+
/** Trimmed ribbons for every road plus the welded surfaces that fill the crossings between them. */
|
|
21
|
+
export interface AuthoredRoadNetwork {
|
|
22
|
+
/** Trimmed centerline sub-paths per input road (parallel to the `roads` array; empty for degenerate roads). */
|
|
23
|
+
roadPaths: RoadPoint[][][];
|
|
24
|
+
/** One welded surface per junction that at least one trimmed ribbon actually ends at. */
|
|
25
|
+
junctionSurfaces: JunctionSurface[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Trim every authored road back to its junction boundaries and weld one surface into each crossing —
|
|
29
|
+
* the drop-in replacement for the old "full ribbons + floating disc" model. Each road's path gets its
|
|
30
|
+
* junction crossings inserted as vertices, is trimmed by {@link trimPathAtJunctions} (a through-road
|
|
31
|
+
* splits in two), and the terminal cross-section corners feed {@link buildJunctionSurface}. Ribbon
|
|
32
|
+
* corners drape at the road's own authored `elevation`, so each welded corner meets the ribbon end at
|
|
33
|
+
* the exact same height (seam-shared, per {@link GROUND_DECAL_LAYERS}); the surface interior drapes at
|
|
34
|
+
* the junction's representative elevation. Pure geometry, so the shell just meshes the result.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildAuthoredRoadNetwork(roads: readonly RoadEnvironmentDescriptor[], junctions: readonly RoadJunction[], sampleHeight: (x: number, z: number) => number): AuthoredRoadNetwork;
|
|
38
|
+
/**
|
|
39
|
+
* Renders one `road()` descriptor from its trimmed centerline sub-paths (cut back at every junction
|
|
40
|
+
* by {@link buildAuthoredRoadNetwork}): an asphalt ribbon, a curb/edge strip along each border, an
|
|
41
|
+
* optional dashed centerline, and optional sidewalks. Because the sub-paths already stop at the
|
|
42
|
+
* junction boundary, every layer naturally ends there — no ribbon ploughs through the crossing and
|
|
43
|
+
* the dashed line no longer needs exclusion circles to skip intersections.
|
|
44
|
+
*/
|
|
45
|
+
export declare function RoadRibbons({ road, field, paths, }: {
|
|
10
46
|
road: RoadEnvironmentDescriptor;
|
|
11
47
|
field: TerrainField;
|
|
12
|
-
|
|
48
|
+
paths: readonly (readonly RoadPoint[])[];
|
|
13
49
|
}): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { BufferAttribute, BufferGeometry, Color, DoubleSide } from "three";
|
|
4
|
-
import { buildRoadRibbon, dashSegments } from "@jgengine/core/world/roads";
|
|
5
|
-
import {
|
|
4
|
+
import { buildJunctionSurface, buildRoadRibbon, dashSegments, GROUND_DECAL_LAYERS, trimPathAtJunctions, } from "@jgengine/core/world/roads";
|
|
5
|
+
import { offsetPath, sidewalkWidthOf } from "@jgengine/core/world/streets";
|
|
6
|
+
/**
|
|
7
|
+
* Decal sub-layers as small lifts above each road's authored base `elevation`, spaced by the
|
|
8
|
+
* single-owner {@link GROUND_DECAL_LAYERS} table so the edge strip and lane paint never z-fight the
|
|
9
|
+
* asphalt they sit on. The asphalt ribbon and the welded junction surface stay on the authored base
|
|
10
|
+
* (they are seam-welded — never stacked); markings lift by the table's road→marking gap and ALSO get
|
|
11
|
+
* `polygonOffset`/`renderOrder` on the material. Curbs and sidewalks are their own surfaces beside
|
|
12
|
+
* the road edge (not table decal layers), so they carry documented offsets rather than a table entry.
|
|
13
|
+
*/
|
|
14
|
+
const MARKING_LIFT = GROUND_DECAL_LAYERS.marking - GROUND_DECAL_LAYERS.road;
|
|
15
|
+
const CURB_LIFT = 0.02;
|
|
16
|
+
const SIDEWALK_LIFT = 0.06;
|
|
6
17
|
/** Darken an asphalt color toward black for a curb/edge strip (deterministic, no alloc churn). */
|
|
7
18
|
function darken(hex, amount) {
|
|
8
19
|
const c = new Color(hex);
|
|
@@ -18,13 +29,8 @@ function toGeometry(positions, indices) {
|
|
|
18
29
|
geometry.computeVertexNormals();
|
|
19
30
|
return geometry;
|
|
20
31
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return toGeometry(ribbon.positions, ribbon.indices);
|
|
24
|
-
}
|
|
25
|
-
function mergedDashGeometry(road, field, exclusions) {
|
|
26
|
-
const width = Math.max(0.25, road.width * 0.035);
|
|
27
|
-
const ribbons = dashSegments(road.path, 3.2, 4, exclusions).map((dash) => buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + 0.04 }));
|
|
32
|
+
/** Merge many ribbons into one buffer geometry (draws collapse to a single mesh). */
|
|
33
|
+
function mergeRibbons(ribbons) {
|
|
28
34
|
const vertexCount = ribbons.reduce((sum, r) => sum + r.positions.length, 0);
|
|
29
35
|
const indexCount = ribbons.reduce((sum, r) => sum + r.indices.length, 0);
|
|
30
36
|
if (vertexCount === 0)
|
|
@@ -36,40 +42,157 @@ function mergedDashGeometry(road, field, exclusions) {
|
|
|
36
42
|
for (const ribbon of ribbons) {
|
|
37
43
|
positions.set(ribbon.positions, vertexOffset);
|
|
38
44
|
const base = vertexOffset / 3;
|
|
39
|
-
for (let i = 0; i < ribbon.indices.length; i += 1)
|
|
45
|
+
for (let i = 0; i < ribbon.indices.length; i += 1)
|
|
40
46
|
indices[indexOffset + i] = ribbon.indices[i] + base;
|
|
41
|
-
}
|
|
42
47
|
vertexOffset += ribbon.positions.length;
|
|
43
48
|
indexOffset += ribbon.indices.length;
|
|
44
49
|
}
|
|
45
50
|
return toGeometry(positions, indices);
|
|
46
51
|
}
|
|
52
|
+
/** Convert a detected {@link RoadJunction} into the structural {@link RoadJunctionInput} the trimmer wants. @internal */
|
|
53
|
+
export function roadJunctionInput(junction) {
|
|
54
|
+
return {
|
|
55
|
+
x: junction.center[0],
|
|
56
|
+
z: junction.center[1],
|
|
57
|
+
// Arm direction `[dx, dz]` → angle `atan2(dx, dz)`, matching the generator convention the core uses.
|
|
58
|
+
arms: junction.approaches.map((approach) => ({
|
|
59
|
+
angle: Math.atan2(approach.direction[0], approach.direction[1]),
|
|
60
|
+
width: approach.width,
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Split every path segment at the junction centers that fall on it, inserting the junction point as
|
|
66
|
+
* a real vertex so {@link trimPathAtJunctions} (which matches junctions to path vertices) can cut the
|
|
67
|
+
* ribbon back there. Authored road crossings from `findRoadJunctions` land mid-segment, not on the
|
|
68
|
+
* hand-authored vertices, so without this the trimmer would never see them.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function insertJunctionNodes(path, junctions, tolerance = 1e-3) {
|
|
72
|
+
if (path.length < 2)
|
|
73
|
+
return path.map((p) => [p[0], p[1]]);
|
|
74
|
+
const out = [[path[0][0], path[0][1]]];
|
|
75
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
76
|
+
const a = path[i];
|
|
77
|
+
const b = path[i + 1];
|
|
78
|
+
const abx = b[0] - a[0];
|
|
79
|
+
const abz = b[1] - a[1];
|
|
80
|
+
const lengthSq = abx * abx + abz * abz;
|
|
81
|
+
if (lengthSq > 1e-12) {
|
|
82
|
+
const hits = [];
|
|
83
|
+
for (const j of junctions) {
|
|
84
|
+
const t = ((j.x - a[0]) * abx + (j.z - a[1]) * abz) / lengthSq;
|
|
85
|
+
if (t <= 1e-6 || t >= 1 - 1e-6)
|
|
86
|
+
continue; // coincident with an existing vertex — already covered
|
|
87
|
+
const px = a[0] + abx * t;
|
|
88
|
+
const pz = a[1] + abz * t;
|
|
89
|
+
if (Math.hypot(j.x - px, j.z - pz) <= tolerance)
|
|
90
|
+
hits.push({ t, x: j.x, z: j.z });
|
|
91
|
+
}
|
|
92
|
+
hits.sort((u, v) => u.t - v.t);
|
|
93
|
+
for (const hit of hits) {
|
|
94
|
+
const prev = out[out.length - 1];
|
|
95
|
+
if (Math.hypot(prev[0] - hit.x, prev[1] - hit.z) > 1e-6)
|
|
96
|
+
out.push([hit.x, hit.z]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.push([b[0], b[1]]);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
47
103
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
104
|
+
* Trim every authored road back to its junction boundaries and weld one surface into each crossing —
|
|
105
|
+
* the drop-in replacement for the old "full ribbons + floating disc" model. Each road's path gets its
|
|
106
|
+
* junction crossings inserted as vertices, is trimmed by {@link trimPathAtJunctions} (a through-road
|
|
107
|
+
* splits in two), and the terminal cross-section corners feed {@link buildJunctionSurface}. Ribbon
|
|
108
|
+
* corners drape at the road's own authored `elevation`, so each welded corner meets the ribbon end at
|
|
109
|
+
* the exact same height (seam-shared, per {@link GROUND_DECAL_LAYERS}); the surface interior drapes at
|
|
110
|
+
* the junction's representative elevation. Pure geometry, so the shell just meshes the result.
|
|
111
|
+
* @internal
|
|
51
112
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
54
|
-
const
|
|
113
|
+
export function buildAuthoredRoadNetwork(roads, junctions, sampleHeight) {
|
|
114
|
+
const inputs = junctions.map(roadJunctionInput);
|
|
115
|
+
const roadPaths = [];
|
|
116
|
+
const approachesByJunction = new Map();
|
|
117
|
+
for (const road of roads) {
|
|
118
|
+
if (road.path.length < 2 || road.width <= 0) {
|
|
119
|
+
roadPaths.push([]);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const noded = insertJunctionNodes(road.path, inputs);
|
|
123
|
+
const subs = trimPathAtJunctions(noded, road.width, inputs);
|
|
124
|
+
roadPaths.push(subs.map((sub) => sub.path));
|
|
125
|
+
for (const sub of subs) {
|
|
126
|
+
for (const cut of sub.cuts) {
|
|
127
|
+
const approach = {
|
|
128
|
+
center: cut.center,
|
|
129
|
+
left: [cut.left[0], sampleHeight(cut.left[0], cut.left[1]) + road.elevation, cut.left[1]],
|
|
130
|
+
right: [cut.right[0], sampleHeight(cut.right[0], cut.right[1]) + road.elevation, cut.right[1]],
|
|
131
|
+
};
|
|
132
|
+
const list = approachesByJunction.get(cut.junctionIndex);
|
|
133
|
+
if (list)
|
|
134
|
+
list.push(approach);
|
|
135
|
+
else
|
|
136
|
+
approachesByJunction.set(cut.junctionIndex, [approach]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const junctionSurfaces = [];
|
|
141
|
+
for (const ji of [...approachesByJunction.keys()].sort((p, q) => p - q)) {
|
|
142
|
+
const junction = junctions[ji];
|
|
143
|
+
const ribbon = buildJunctionSurface({ x: junction.center[0], z: junction.center[1] }, approachesByJunction.get(ji), sampleHeight, { elevation: junction.elevation });
|
|
144
|
+
junctionSurfaces.push({ color: junction.color, ribbon });
|
|
145
|
+
}
|
|
146
|
+
return { roadPaths, junctionSurfaces };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Renders one `road()` descriptor from its trimmed centerline sub-paths (cut back at every junction
|
|
150
|
+
* by {@link buildAuthoredRoadNetwork}): an asphalt ribbon, a curb/edge strip along each border, an
|
|
151
|
+
* optional dashed centerline, and optional sidewalks. Because the sub-paths already stop at the
|
|
152
|
+
* junction boundary, every layer naturally ends there — no ribbon ploughs through the crossing and
|
|
153
|
+
* the dashed line no longer needs exclusion circles to skip intersections.
|
|
154
|
+
*/
|
|
155
|
+
export function RoadRibbons({ road, field, paths, }) {
|
|
156
|
+
const asphalt = useMemo(() => mergeRibbons(paths.map((path) => buildRoadRibbon(path, road.width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation }))), [paths, road.width, road.elevation, field]);
|
|
157
|
+
const dashes = useMemo(() => {
|
|
158
|
+
if (!road.markings)
|
|
159
|
+
return null;
|
|
160
|
+
const width = Math.max(0.25, road.width * 0.035);
|
|
161
|
+
const ribbons = [];
|
|
162
|
+
for (const path of paths) {
|
|
163
|
+
for (const dash of dashSegments(path, 3.2, 4)) {
|
|
164
|
+
ribbons.push(buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + MARKING_LIFT }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return mergeRibbons(ribbons);
|
|
168
|
+
}, [paths, road.markings, road.width, road.elevation, field]);
|
|
55
169
|
const curbColor = useMemo(() => darken(road.color, 0.45), [road.color]);
|
|
56
170
|
const curbs = useMemo(() => {
|
|
57
171
|
// A thin strip straddling each asphalt edge, lifted just above the ribbon.
|
|
58
172
|
const strip = Math.max(0.4, road.width * 0.09);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
173
|
+
const edge = road.width / 2 - strip / 2;
|
|
174
|
+
const ribbons = [];
|
|
175
|
+
for (const path of paths) {
|
|
176
|
+
for (const off of [edge, -edge]) {
|
|
177
|
+
ribbons.push(buildRoadRibbon(offsetPath(path, off), strip, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + CURB_LIFT }));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return mergeRibbons(ribbons);
|
|
181
|
+
}, [paths, road.width, road.elevation, field]);
|
|
63
182
|
const sidewalks = useMemo(() => {
|
|
64
183
|
const width = sidewalkWidthOf(road);
|
|
65
184
|
if (width <= 0)
|
|
66
|
-
return
|
|
185
|
+
return null;
|
|
67
186
|
const offset = road.width / 2 + width / 2;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
187
|
+
const ribbons = [];
|
|
188
|
+
for (const path of paths) {
|
|
189
|
+
for (const off of [offset, -offset]) {
|
|
190
|
+
ribbons.push(buildRoadRibbon(offsetPath(path, off), width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + SIDEWALK_LIFT }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return mergeRibbons(ribbons);
|
|
194
|
+
}, [paths, road, field]);
|
|
72
195
|
if (asphalt === null)
|
|
73
196
|
return null;
|
|
74
|
-
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), curbs
|
|
197
|
+
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), curbs !== null ? (_jsx("mesh", { geometry: curbs, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: curbColor, roughness: 0.9, metalness: 0, side: DoubleSide }) })) : null, dashes !== null ? (_jsx("mesh", { geometry: dashes, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, sidewalks !== null ? (_jsx("mesh", { geometry: sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.sidewalk === false ? "#a7adb8" : road.sidewalk.color, roughness: 0.9, metalness: 0, side: DoubleSide }) })) : null] }));
|
|
75
198
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
|
-
import {
|
|
3
|
+
import { useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
/**
|
|
6
7
|
* Camera-centered proxy dome radius. Must sit inside the default camera far plane
|
|
7
8
|
* (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
|
|
@@ -161,7 +162,7 @@ void main() {
|
|
|
161
162
|
export function VolumetricClouds({ rules, sunDirection }) {
|
|
162
163
|
const meshRef = useRef(null);
|
|
163
164
|
const timeRef = useRef(0);
|
|
164
|
-
const material =
|
|
165
|
+
const material = useDisposable(() => {
|
|
165
166
|
return new THREE.ShaderMaterial({
|
|
166
167
|
uniforms: {
|
|
167
168
|
uCameraPos: { value: new THREE.Vector3() },
|
|
@@ -186,7 +187,6 @@ export function VolumetricClouds({ rules, sunDirection }) {
|
|
|
186
187
|
fog: false,
|
|
187
188
|
});
|
|
188
189
|
}, [rules, sunDirection]);
|
|
189
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
190
190
|
useFrame((state, delta) => {
|
|
191
191
|
timeRef.current += delta;
|
|
192
192
|
const mesh = meshRef.current;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
|
|
2
|
+
export { DayNightSky, type DayNightSkyProps } from "./DayNightSky.js";
|
|
2
3
|
export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
|
|
3
4
|
export { VolumetricClouds, type VolumetricCloudsProps } from "./VolumetricClouds.js";
|
|
4
5
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
|
|
2
|
+
export { DayNightSky } from "./DayNightSky.js";
|
|
2
3
|
export { EnvironmentScene } from "./EnvironmentScene.js";
|
|
3
4
|
export { VolumetricClouds } from "./VolumetricClouds.js";
|
|
4
5
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
|
package/dist/gameKit.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
-
* (`defineSystem`),
|
|
5
|
-
*
|
|
2
|
+
* **Start here for game code.** The sole happy-path import surface for a whole simple game:
|
|
3
|
+
* authoring (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), authored-scene helpers, HUD building blocks, and deterministic randomness.
|
|
5
|
+
* Reach for deep package paths only when a game outgrows the kit. Do not start from
|
|
6
|
+
* `@jgengine/core/authoring` — that module is a core-side helper barrel, not the game entrypoint.
|
|
6
7
|
*
|
|
7
8
|
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
9
|
*/
|
|
@@ -20,7 +21,9 @@ export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
|
20
21
|
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
21
22
|
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
22
23
|
export { environment } from "@jgengine/core/world/features";
|
|
24
|
+
export { grounded } from "@jgengine/core/world/grounded";
|
|
23
25
|
export type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
26
|
+
export { DEFAULT_WALK_CODES } from "./shellMovement.js";
|
|
24
27
|
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
28
|
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
29
|
export { seededRng } from "@jgengine/core/random/rng";
|
package/dist/gameKit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
-
* (`defineSystem`),
|
|
5
|
-
*
|
|
2
|
+
* **Start here for game code.** The sole happy-path import surface for a whole simple game:
|
|
3
|
+
* authoring (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), authored-scene helpers, HUD building blocks, and deterministic randomness.
|
|
5
|
+
* Reach for deep package paths only when a game outgrows the kit. Do not start from
|
|
6
|
+
* `@jgengine/core/authoring` — that module is a core-side helper barrel, not the game entrypoint.
|
|
6
7
|
*
|
|
7
8
|
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
9
|
*/
|
|
@@ -20,7 +21,9 @@ export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
|
20
21
|
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
21
22
|
// World + camera.
|
|
22
23
|
export { environment } from "@jgengine/core/world/features";
|
|
23
|
-
|
|
24
|
+
export { grounded } from "@jgengine/core/world/grounded";
|
|
25
|
+
export { DEFAULT_WALK_CODES } from "./shellMovement.js";
|
|
26
|
+
// HUD layout + drop-in building blocks — compose these, reskin with HudTheme; games own layout/terminology/art direction.
|
|
24
27
|
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
28
|
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
29
|
// Deterministic randomness (inject seeds; never Math.random in game logic).
|