@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +529 -1
  2. package/dist/CombatPresentation.js +2 -1
  3. package/dist/GamePlayerShell.js +25 -5
  4. package/dist/Shell3dPresentation.d.ts +6 -6
  5. package/dist/Shell3dPresentation.js +4 -4
  6. package/dist/camera/ControllerCameraShake.d.ts +31 -0
  7. package/dist/camera/ControllerCameraShake.js +34 -0
  8. package/dist/camera/GameCameraRig.js +10 -5
  9. package/dist/camera/GameOrbitCamera.js +1 -10
  10. package/dist/camera/cameraRigs.js +16 -4
  11. package/dist/camera/index.d.ts +1 -0
  12. package/dist/camera/index.js +1 -0
  13. package/dist/camera/rigMath.d.ts +2 -0
  14. package/dist/camera/rigMath.js +7 -0
  15. package/dist/devtools/CollisionDebugWorld.js +15 -1
  16. package/dist/devtools/DevtoolsOverlay.js +2 -0
  17. package/dist/devtools/collisionDebugMath.d.ts +8 -0
  18. package/dist/devtools/collisionDebugMath.js +14 -5
  19. package/dist/drivers/FrameDriver.d.ts +4 -1
  20. package/dist/drivers/FrameDriver.js +5 -3
  21. package/dist/environment/DayNightSky.d.ts +33 -0
  22. package/dist/environment/DayNightSky.js +51 -0
  23. package/dist/environment/Daylight.d.ts +3 -1
  24. package/dist/environment/Daylight.js +7 -3
  25. package/dist/environment/EnvironmentScene.js +6 -6
  26. package/dist/environment/RoadJunctions.d.ts +8 -4
  27. package/dist/environment/RoadJunctions.js +16 -15
  28. package/dist/environment/RoadRibbons.d.ts +42 -6
  29. package/dist/environment/RoadRibbons.js +150 -27
  30. package/dist/environment/VolumetricClouds.js +3 -3
  31. package/dist/environment/index.d.ts +1 -0
  32. package/dist/environment/index.js +1 -0
  33. package/dist/gameKit.d.ts +7 -4
  34. package/dist/gameKit.js +8 -5
  35. package/dist/inputSink.js +14 -0
  36. package/dist/materialOverride.js +30 -0
  37. package/dist/postfx/PostProcessing.d.ts +3 -2
  38. package/dist/postfx/PostProcessing.js +38 -5
  39. package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
  40. package/dist/postfx/ScreenEffectsOverlay.js +70 -0
  41. package/dist/postfx/postfxOverlay.d.ts +24 -0
  42. package/dist/postfx/postfxOverlay.js +35 -0
  43. package/dist/presentationResolve.d.ts +3 -2
  44. package/dist/presentationResolve.js +11 -6
  45. package/dist/render/CascadedShadows.d.ts +13 -0
  46. package/dist/render/CascadedShadows.js +96 -0
  47. package/dist/render/EntityPreview.d.ts +53 -0
  48. package/dist/render/EntityPreview.js +23 -0
  49. package/dist/render/EnvironmentLighting.d.ts +23 -7
  50. package/dist/render/EnvironmentLighting.js +69 -10
  51. package/dist/render/PartMotion.js +4 -4
  52. package/dist/render/SceneLighting.js +7 -0
  53. package/dist/render/SceneModels.d.ts +37 -1
  54. package/dist/render/SceneModels.js +52 -10
  55. package/dist/render/measureBounds.js +4 -0
  56. package/dist/render/measureCollisionMesh.d.ts +26 -0
  57. package/dist/render/measureCollisionMesh.js +108 -0
  58. package/dist/render/modelLoad.d.ts +41 -5
  59. package/dist/render/modelLoad.js +85 -18
  60. package/dist/render/sceneCapture.d.ts +27 -0
  61. package/dist/render/sceneCapture.js +43 -0
  62. package/dist/render/useDisposable.d.ts +16 -0
  63. package/dist/render/useDisposable.js +24 -0
  64. package/dist/render/useModelAnimation.js +47 -4
  65. package/dist/scene/AuthoredScene.js +14 -24
  66. package/dist/scene/CityRenderer.js +103 -36
  67. package/dist/scene/GrassFieldRenderer.js +4 -1
  68. package/dist/scene/PoleLineRenderer.js +7 -16
  69. package/dist/settings/QuickControls.js +5 -3
  70. package/dist/settings/SettingsMenu.js +6 -3
  71. package/dist/settings/appliedSettings.d.ts +3 -1
  72. package/dist/settings/appliedSettings.js +17 -5
  73. package/dist/settings/settingsController.js +11 -5
  74. package/dist/shellMovement.d.ts +15 -1
  75. package/dist/shellMovement.js +15 -1
  76. package/dist/structures/GeneratedBuilding.js +4 -5
  77. package/dist/structures/TransformGizmo.js +6 -5
  78. package/dist/terrain/CarvedTerrain.js +3 -7
  79. package/dist/terrain/GrassField.js +2 -2
  80. package/dist/terrain/ProceduralGround.js +3 -5
  81. package/dist/terrain/grassMaterial.d.ts +8 -2
  82. package/dist/terrain/grassMaterial.js +24 -5
  83. package/dist/terrain/index.d.ts +1 -1
  84. package/dist/terrain/terrainDetailMaterial.js +11 -2
  85. package/dist/terrain/terrainMath.d.ts +38 -1
  86. package/dist/terrain/terrainMath.js +86 -64
  87. package/dist/touch/TouchControlsOverlay.d.ts +6 -2
  88. package/dist/touch/TouchControlsOverlay.js +136 -17
  89. package/dist/vfx/ParticleField.d.ts +32 -0
  90. package/dist/vfx/ParticleField.js +91 -0
  91. package/dist/vfx/WorldFloatingText.d.ts +32 -0
  92. package/dist/vfx/WorldFloatingText.js +32 -0
  93. package/dist/water/Ocean.js +3 -6
  94. package/dist/weather/LightningStrike.js +3 -9
  95. package/dist/weather/RainField.js +3 -7
  96. package/dist/weather/SnowField.js +3 -7
  97. package/dist/world/DataObjects.js +3 -6
  98. package/dist/world/GridWorldScene.js +3 -6
  99. package/dist/world/InstancedBodies.js +3 -6
  100. package/dist/world/InstancedJoints.js +2 -2
  101. package/dist/world/SpriteBatch.js +3 -6
  102. package/dist/world/WorldEntityFrames.d.ts +47 -0
  103. package/dist/world/WorldEntityFrames.js +75 -0
  104. package/dist/world/WorldHud.js +7 -5
  105. package/dist/world/WorldPings.d.ts +26 -0
  106. package/dist/world/WorldPings.js +68 -0
  107. package/dist/world/WorldScene.js +9 -4
  108. package/dist/world/pingPulse.d.ts +21 -0
  109. package/dist/world/pingPulse.js +17 -0
  110. package/package.json +5 -5
  111. package/skills/ce-handoff/SKILL.md +149 -0
  112. package/skills/game-design/SKILL.md +2 -0
  113. package/skills/jgengine/SKILL.md +7 -3
  114. package/skills/jgengine/api.md +17 -7
  115. package/skills/jgengine/capabilities.md +12 -0
  116. package/skills/jgengine/recipes/minimal-game.md +7 -4
  117. package/skills/jgengine-ui/SKILL.md +8 -3
  118. package/skills/jgengine-ui/api.md +732 -23
  119. package/skills/jgengine-ui/capabilities.md +371 -2
  120. package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
  121. package/skills/jgengine-ui/reference.md +7 -4
  122. package/skills/jgengine-world/api.md +408 -12
  123. package/skills/jgengine-world/capabilities.md +127 -2
  124. package/skills/jgengine-world/reference.md +15 -1
@@ -1,17 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import {} from "@react-three/fiber";
3
- import { useEffect, useMemo } from "react";
4
3
  import * as THREE from "three";
4
+ import { useDisposable } from "../render/useDisposable.js";
5
5
  import { createProceduralGroundGeometry } from "./terrainMath.js";
6
6
  export function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }) {
7
- const geometry = useMemo(() => createProceduralGroundGeometry(terrain, colors), [colors, terrain]);
8
- const material = useMemo(() => new THREE.MeshStandardMaterial({
7
+ const geometry = useDisposable(() => createProceduralGroundGeometry(terrain, colors), [colors, terrain]);
8
+ const material = useDisposable(() => new THREE.MeshStandardMaterial({
9
9
  color: "#ffffff",
10
10
  roughness,
11
11
  metalness,
12
12
  vertexColors: true,
13
13
  }), [metalness, roughness]);
14
- useEffect(() => () => geometry.dispose(), [geometry]);
15
- useEffect(() => () => material.dispose(), [material]);
16
14
  return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
17
15
  }
@@ -5,6 +5,12 @@ export interface GrassWindOptions {
5
5
  speed?: number;
6
6
  gustScale?: number;
7
7
  flutter?: number;
8
+ /**
9
+ * Layered rolling gust field: three phase-offset directional waves (one carrying a cross-wind
10
+ * component) instead of a single sine, so visible wave fronts sweep the meadow. Default true.
11
+ * `false` restores the single-sine gust. Compile-time variation — folded into the program cache key.
12
+ */
13
+ layered?: boolean;
8
14
  }
9
15
  /** Camera-distance fade band: tufts thin out between `start` and `end` meters, so the instance budget spends where the camera lives. */
10
16
  export interface GrassDistanceFadeOptions {
@@ -18,7 +24,7 @@ export interface GrassMaterialOptions {
18
24
  colorGround?: THREE.ColorRepresentation;
19
25
  colorVariation?: number;
20
26
  wind?: GrassWindOptions | false;
21
- /** Distance thinning; `false` disables. Default fades between 35 m and 95 m. */
27
+ /** Distance thinning; `false` disables. Default fades between 55 m and 150 m so meadows read expansive. */
22
28
  distanceFade?: GrassDistanceFadeOptions | false;
23
29
  /** 0..1 blend of blade normals toward straight-up — lit like a soft carpet instead of dark individual planes. Default 0.6. */
24
30
  normalLift?: number;
@@ -43,7 +49,7 @@ export interface GrassMaterialHandle {
43
49
  uniforms: GrassShaderUniforms;
44
50
  }
45
51
  export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
46
- /** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
52
+ /** Default camera-distance fade band: tufts start thinning at 55 m and are gone by 150 m so the meadow reads deep. */
47
53
  export declare const DEFAULT_GRASS_DISTANCE_FADE: Required<GrassDistanceFadeOptions>;
48
54
  /** @internal */
49
55
  export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
@@ -5,11 +5,12 @@ export const DEFAULT_GRASS_WIND = {
5
5
  speed: 1.6,
6
6
  gustScale: 0.16,
7
7
  flutter: 0.08,
8
+ layered: true,
8
9
  };
9
- /** Default camera-distance fade band: tufts start thinning at 35 m and are gone by 95 m. */
10
+ /** Default camera-distance fade band: tufts start thinning at 55 m and are gone by 150 m so the meadow reads deep. */
10
11
  export const DEFAULT_GRASS_DISTANCE_FADE = {
11
- start: 35,
12
- end: 95,
12
+ start: 55,
13
+ end: 150,
13
14
  };
14
15
  function normalizeWindDirection(direction) {
15
16
  const vector = new THREE.Vector2(direction[0], direction[1]);
@@ -25,6 +26,7 @@ export function resolveGrassWind(wind) {
25
26
  speed: wind?.speed ?? DEFAULT_GRASS_WIND.speed,
26
27
  gustScale: wind?.gustScale ?? DEFAULT_GRASS_WIND.gustScale,
27
28
  flutter: wind?.flutter ?? DEFAULT_GRASS_WIND.flutter,
29
+ layered: wind?.layered ?? DEFAULT_GRASS_WIND.layered,
28
30
  };
29
31
  }
30
32
  /** @internal */
@@ -122,7 +124,15 @@ void jgComputeGrassBlade() {
122
124
  float st = sin(instanceYaw);
123
125
  vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
124
126
  float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
125
- float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
127
+ ${wind.layered
128
+ ? ` // Layered rolling gust field: three phase-offset directional waves, one carrying a cross-wind
129
+ // term, so coherent wave fronts sweep across the meadow instead of every tuft waving in place.
130
+ vec2 windPerp = vec2(-uWindDirection.y, uWindDirection.x);
131
+ float crossPhase = dot(instanceOffset.xz, windPerp) * uWindGustScale * 0.6;
132
+ float gust = sin(gustPhase) * 0.55
133
+ + sin(gustPhase * 0.53 + crossPhase + 2.1) * 0.32
134
+ + sin(gustPhase * 1.7 + crossPhase * 0.4 + 4.3) * 0.13;`
135
+ : ` float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;`}
126
136
  float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
127
137
  vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
128
138
  jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
@@ -156,8 +166,17 @@ grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
156
166
  grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
157
167
  grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
158
168
  diffuseColor.rgb = grassColor;
169
+ `)
170
+ .replace("#include <lights_fragment_end>", `
171
+ #include <lights_fragment_end>
172
+ // Warm tip sheen / backscatter: blade tips lift toward a warm glow, scaled by how strongly the
173
+ // fragment is directly lit — a cheap stand-in for sun shining through the canopy, no extra lights.
174
+ float jgTipMask = smoothstep(0.55, 1.0, vGrassBladeT);
175
+ float jgLit = clamp(dot(reflectedLight.directDiffuse, vec3(0.3333)) * 1.6, 0.0, 1.0);
176
+ reflectedLight.directDiffuse += vec3(0.34, 0.28, 0.12) * jgTipMask * jgLit * (1.0 - vGrassFade * 0.7);
159
177
  `);
160
178
  };
161
- material.customProgramCacheKey = () => `jgengine-grass-${wind.strength}-${wind.flutter}`;
179
+ // Only the layered-gust branch changes the compiled shader source, so it is the sole compile-time key.
180
+ material.customProgramCacheKey = () => `jgengine-grass-${wind.layered ? "layered" : "single"}`;
162
181
  return { material, uniforms };
163
182
  }
@@ -7,5 +7,5 @@ export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./Terrafor
7
7
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
8
8
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
9
9
  export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
10
- export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
10
+ export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainSurfaceColorOptions, type TerrainVertexColorOptions, } from "./terrainMath.js";
11
11
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
@@ -43,6 +43,7 @@ export function createTerrainDetailMaterial(detail, textures) {
43
43
  const hasTexture = textures !== undefined && detail.material !== undefined;
44
44
  const repeat = detail.material?.repeat ?? 4;
45
45
  const strength = detail.material?.strength ?? 1;
46
+ const tint = detail.material?.tint ?? 1;
46
47
  const uniforms = {
47
48
  uRockColor: { value: new THREE.Color(detail.rockColor) },
48
49
  uSandColor: { value: new THREE.Color(detail.sandColor) },
@@ -61,6 +62,7 @@ export function createTerrainDetailMaterial(detail, textures) {
61
62
  uMatAo: { value: textures.ao },
62
63
  uMatRepeat: { value: Math.max(0.01, repeat) },
63
64
  uMatStrength: { value: strength },
65
+ uMatTint: { value: tint },
64
66
  }
65
67
  : {}),
66
68
  };
@@ -87,13 +89,20 @@ uniform sampler2D uMatNormal;
87
89
  uniform sampler2D uMatRoughness;
88
90
  uniform sampler2D uMatAo;
89
91
  uniform float uMatRepeat;
90
- uniform float uMatStrength;`
92
+ uniform float uMatStrength;
93
+ uniform float uMatTint;`
91
94
  : "";
92
95
  const materialColorGlsl = hasTexture
93
96
  ? `
94
97
  vec2 jgMatUv = jgWp / uMatRepeat;
95
98
  vec3 jgMatColor = texture2D(uMatColor, jgMatUv).rgb;
96
- jgCol = mix(jgCol, jgMatColor, uMatStrength);`
99
+ // Two separable jobs. GRAIN: the map's luminance relative to its own mid-grey shades the authored
100
+ // colour, which is what makes ground read as a surface. TINT: how much of the map's own hue
101
+ // replaces that colour — at 1 the biome palette, macro sweeps, and slope rock underneath are simply
102
+ // painted over, and a whole region collapses to the texture's single hue.
103
+ float jgMatLum = dot(jgMatColor, vec3(0.2126, 0.7152, 0.0722));
104
+ jgCol *= mix(1.0, clamp(jgMatLum / 0.35, 0.55, 1.7), uMatStrength * (1.0 - uMatTint));
105
+ jgCol = mix(jgCol, jgMatColor, uMatStrength * uMatTint);`
97
106
  : "";
98
107
  const materialRoughnessGlsl = hasTexture
99
108
  ? `
@@ -27,6 +27,35 @@ export interface TerrainVertexColorOptions {
27
27
  waterline?: THREE.ColorRepresentation;
28
28
  waterlineHeight?: number;
29
29
  }
30
+ /**
31
+ * Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain
32
+ * instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is
33
+ * a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
34
+ */
35
+ export interface TerrainSurfaceColorOptions {
36
+ /** 0..1 darkening + desaturation on steep faces (slope sampled from neighbor heights). Default 0.5; 0 disables. */
37
+ slopeStrength?: number;
38
+ /** 0..1 amplitude of deterministic hash mottling (brightness jitter per patch). Default 0.14; 0 disables. */
39
+ mottleStrength?: number;
40
+ /** World-space frequency of the mottling lattice (cells per meter). Default 0.12. */
41
+ mottleScale?: number;
42
+ /** Seed for the mottling hash. Same seed → identical mottling. Default 1. */
43
+ seed?: TerrainSeed;
44
+ }
45
+ interface ResolvedTerrainSurfaceColorOptions {
46
+ slopeStrength: number;
47
+ mottleStrength: number;
48
+ mottleScale: number;
49
+ seed: TerrainSeed;
50
+ }
51
+ /** @internal */
52
+ export declare const DEFAULT_TERRAIN_SURFACE_COLOR: ResolvedTerrainSurfaceColorOptions;
53
+ /**
54
+ * Layer the slope darkening and hash mottling onto an already height-lerped vertex color, in place.
55
+ * `slope01` is the 0..1 steepness at the vertex; `x,z` seed the deterministic mottling.
56
+ * @internal
57
+ */
58
+ export declare function applyTerrainSurfaceShading(color: THREE.Color, x: number, z: number, slope01: number, surface: ResolvedTerrainSurfaceColorOptions): void;
30
59
  /** @internal */
31
60
  export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
32
61
  /** @internal */
@@ -51,6 +80,8 @@ export interface FieldGroundOptions {
51
80
  heightRange?: readonly [min: number, max: number];
52
81
  /** Sampled per vertex when set; `colors` still supplies `waterlineHeight` and the fallback. */
53
82
  paletteAt?: TerrainPaletteSampler;
83
+ /** Slope/noise surface shading over the height lerp. Omit for the default terrain look; pass `{ slopeStrength: 0, mottleStrength: 0 }` for a flat gradient. */
84
+ surface?: TerrainSurfaceColorOptions;
54
85
  }
55
86
  /**
56
87
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
@@ -59,5 +90,11 @@ export interface FieldGroundOptions {
59
90
  * @internal
60
91
  */
61
92
  export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
62
- /** @internal */
93
+ /**
94
+ * Meshes the built-in procedural noise terrain into a vertex-coloured ground. A thin wrapper over the
95
+ * shared {@link buildGroundGeometry}: same height lerp, plus slope-aware shading and hash mottling
96
+ * seeded from `config.seed` so the plain ground reads like terrain rather than a two-tone gradient.
97
+ * @internal
98
+ */
63
99
  export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
100
+ export {};
@@ -1,6 +1,62 @@
1
1
  import { noiseField } from "@jgengine/core/world/terrain";
2
2
  import * as THREE from "three";
3
- import {} from "./random.js";
3
+ import { hashNoise2 } from "./random.js";
4
+ /** @internal */
5
+ export const DEFAULT_TERRAIN_SURFACE_COLOR = {
6
+ slopeStrength: 0.5,
7
+ mottleStrength: 0.14,
8
+ mottleScale: 0.12,
9
+ seed: 1,
10
+ };
11
+ function resolveSurfaceColor(surface) {
12
+ return {
13
+ slopeStrength: THREE.MathUtils.clamp(surface?.slopeStrength ?? DEFAULT_TERRAIN_SURFACE_COLOR.slopeStrength, 0, 1),
14
+ mottleStrength: THREE.MathUtils.clamp(surface?.mottleStrength ?? DEFAULT_TERRAIN_SURFACE_COLOR.mottleStrength, 0, 1),
15
+ mottleScale: Math.max(0, surface?.mottleScale ?? DEFAULT_TERRAIN_SURFACE_COLOR.mottleScale),
16
+ seed: surface?.seed ?? DEFAULT_TERRAIN_SURFACE_COLOR.seed,
17
+ };
18
+ }
19
+ /** Smoothed value noise on the integer hash lattice — continuous, seamless, and a pure function of `x,z`. */
20
+ function groundValueNoise(x, z, scale, seed) {
21
+ const sx = x * scale;
22
+ const sz = z * scale;
23
+ const xi = Math.floor(sx);
24
+ const zi = Math.floor(sz);
25
+ const xf = sx - xi;
26
+ const zf = sz - zi;
27
+ const u = xf * xf * (3 - 2 * xf);
28
+ const w = zf * zf * (3 - 2 * zf);
29
+ const n00 = hashNoise2(xi, zi, seed);
30
+ const n10 = hashNoise2(xi + 1, zi, seed);
31
+ const n01 = hashNoise2(xi, zi + 1, seed);
32
+ const n11 = hashNoise2(xi + 1, zi + 1, seed);
33
+ const nx0 = n00 + (n10 - n00) * u;
34
+ const nx1 = n01 + (n11 - n01) * u;
35
+ return nx0 + (nx1 - nx0) * w;
36
+ }
37
+ /**
38
+ * Layer the slope darkening and hash mottling onto an already height-lerped vertex color, in place.
39
+ * `slope01` is the 0..1 steepness at the vertex; `x,z` seed the deterministic mottling.
40
+ * @internal
41
+ */
42
+ export function applyTerrainSurfaceShading(color, x, z, slope01, surface) {
43
+ if (surface.mottleStrength > 0 && surface.mottleScale > 0) {
44
+ // Two octaves: broad patchiness plus a finer break-up. Centered on 0.5 so it only jitters brightness.
45
+ const broad = groundValueNoise(x, z, surface.mottleScale, surface.seed);
46
+ const fine = groundValueNoise(x + 41.7, z - 19.3, surface.mottleScale * 3.1, surface.seed);
47
+ const mottle = broad * 0.65 + fine * 0.35;
48
+ color.multiplyScalar(1 + (mottle - 0.5) * 2 * surface.mottleStrength);
49
+ }
50
+ if (surface.slopeStrength > 0 && slope01 > 0) {
51
+ // Steep faces read as exposed earth: pull toward luminance (desaturate) then darken.
52
+ const slopeK = THREE.MathUtils.clamp(slope01 * surface.slopeStrength, 0, 1);
53
+ const lum = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
54
+ color.r = THREE.MathUtils.lerp(color.r, lum, slopeK * 0.5);
55
+ color.g = THREE.MathUtils.lerp(color.g, lum, slopeK * 0.5);
56
+ color.b = THREE.MathUtils.lerp(color.b, lum, slopeK * 0.5);
57
+ color.multiplyScalar(1 - 0.4 * slopeK);
58
+ }
59
+ }
4
60
  /** @internal */
5
61
  export function normalizeHeightBlend(height, minHeight, maxHeight) {
6
62
  const range = maxHeight - minHeight;
@@ -49,11 +105,17 @@ export function createFieldGroundGeometry(field, options = {}) {
49
105
  center: [cx, cz],
50
106
  heightRange: options.heightRange,
51
107
  paletteAt: options.paletteAt,
108
+ surface: options.surface,
52
109
  });
53
110
  }
54
111
  function buildGroundGeometry(size, segments, sampler, opts) {
55
112
  const colors = opts.colors;
56
113
  const paletteAt = opts.paletteAt;
114
+ const surface = resolveSurfaceColor(opts.surface);
115
+ const shadeSlope = surface.slopeStrength > 0;
116
+ // Neighbor spacing for the finite-difference slope estimate — one grid cell per axis.
117
+ const slopeStepX = shadeSlope ? Math.max(1e-3, size.width / Math.max(1, segments.x)) : 0;
118
+ const slopeStepZ = shadeSlope ? Math.max(1e-3, size.depth / Math.max(1, segments.z)) : 0;
57
119
  const vertexCountX = segments.x + 1;
58
120
  const vertexCountZ = segments.z + 1;
59
121
  const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
@@ -91,6 +153,15 @@ function buildGroundGeometry(size, segments, sampler, opts) {
91
153
  const color = low.clone().lerp(high, blend);
92
154
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
93
155
  color.lerp(waterline, 0.65);
156
+ let slope01 = 0;
157
+ if (shadeSlope) {
158
+ // Bounded per-vertex finite difference (4 extra samples) — build-time only, no per-frame scan.
159
+ const dhx = (sampler(x + slopeStepX, z) - sampler(x - slopeStepX, z)) / (2 * slopeStepX);
160
+ const dhz = (sampler(x, z + slopeStepZ) - sampler(x, z - slopeStepZ)) / (2 * slopeStepZ);
161
+ const grad = Math.hypot(dhx, dhz);
162
+ slope01 = grad / (grad + 1);
163
+ }
164
+ applyTerrainSurfaceShading(color, x, z, slope01, surface);
94
165
  colorValues[colorIndex] = color.r;
95
166
  colorValues[colorIndex + 1] = color.g;
96
167
  colorValues[colorIndex + 2] = color.b;
@@ -124,71 +195,22 @@ function buildGroundGeometry(size, segments, sampler, opts) {
124
195
  geometry.computeBoundingSphere();
125
196
  return geometry;
126
197
  }
127
- /** @internal */
198
+ /**
199
+ * Meshes the built-in procedural noise terrain into a vertex-coloured ground. A thin wrapper over the
200
+ * shared {@link buildGroundGeometry}: same height lerp, plus slope-aware shading and hash mottling
201
+ * seeded from `config.seed` so the plain ground reads like terrain rather than a two-tone gradient.
202
+ * @internal
203
+ */
128
204
  export function createProceduralGroundGeometry(config = {}, colors = {}) {
129
205
  const size = resolveTerrainSize(config.size);
130
206
  const segments = resolveTerrainSegments(config.segments);
131
207
  const sampler = createProceduralTerrainSampler(config);
132
- const vertexCountX = segments.x + 1;
133
- const vertexCountZ = segments.z + 1;
134
- const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
135
- const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
136
- const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
137
- const indices = new Uint32Array(segments.x * segments.z * 6);
138
- const low = new THREE.Color(colors.low ?? "#30402c");
139
- const high = new THREE.Color(colors.high ?? "#7f8b50");
140
- const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
141
- const minHeight = (config.baseOffset ?? 0) - (config.height ?? 1.4) * 1.2;
142
- const maxHeight = (config.baseOffset ?? 0) + (config.height ?? 1.4) * 1.2;
143
- let index = 0;
144
- let uvIndex = 0;
145
- let colorIndex = 0;
146
- for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
147
- const v = zIndex / segments.z;
148
- const z = (v - 0.5) * size.depth;
149
- for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
150
- const u = xIndex / segments.x;
151
- const x = (u - 0.5) * size.width;
152
- const y = sampler(x, z);
153
- positions[index] = x;
154
- positions[index + 1] = y;
155
- positions[index + 2] = z;
156
- uvs[uvIndex] = u;
157
- uvs[uvIndex + 1] = v;
158
- const blend = normalizeHeightBlend(y, minHeight, maxHeight);
159
- const color = low.clone().lerp(high, blend);
160
- if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
161
- color.lerp(waterline, 0.65);
162
- colorValues[colorIndex] = color.r;
163
- colorValues[colorIndex + 1] = color.g;
164
- colorValues[colorIndex + 2] = color.b;
165
- index += 3;
166
- uvIndex += 2;
167
- colorIndex += 3;
168
- }
169
- }
170
- let triangleIndex = 0;
171
- for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
172
- for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
173
- const a = zIndex * vertexCountX + xIndex;
174
- const b = a + 1;
175
- const c = a + vertexCountX;
176
- const d = c + 1;
177
- indices[triangleIndex] = a;
178
- indices[triangleIndex + 1] = c;
179
- indices[triangleIndex + 2] = b;
180
- indices[triangleIndex + 3] = b;
181
- indices[triangleIndex + 4] = c;
182
- indices[triangleIndex + 5] = d;
183
- triangleIndex += 6;
184
- }
185
- }
186
- const geometry = new THREE.BufferGeometry();
187
- geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
188
- geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
189
- geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
190
- geometry.setIndex(new THREE.BufferAttribute(indices, 1));
191
- geometry.computeVertexNormals();
192
- geometry.computeBoundingSphere();
193
- return geometry;
208
+ const base = config.baseOffset ?? 0;
209
+ const span = (config.height ?? 1.4) * 1.2;
210
+ return buildGroundGeometry(size, segments, sampler, {
211
+ colors,
212
+ center: [0, 0],
213
+ heightRange: [base - span, base + span],
214
+ surface: { seed: config.seed ?? DEFAULT_TERRAIN_SURFACE_COLOR.seed },
215
+ });
194
216
  }
@@ -1,8 +1,10 @@
1
1
  import { type MutableRefObject } from "react";
2
- import { type TouchScheme, type TouchStyle } from "@jgengine/core/input/touchScheme";
2
+ import { type TouchJoystickVariant, type TouchScheme, type TouchStyle } from "@jgengine/core/input/touchScheme";
3
3
  export interface TouchCodeSink {
4
4
  onCodeDown(code: string): void;
5
5
  onCodeUp(code: string): void;
6
+ /** Analog per-action magnitudes (0..1) from the joystick's continuous vector; `null` when the stick is released. Optional so bespoke docks that only press buttons need not care. */
7
+ onAnalog?(values: Readonly<Record<string, number>> | null): void;
6
8
  }
7
9
  /**
8
10
  * Thumb-arc placement for primary buttons around the bottom-right corner:
@@ -29,10 +31,12 @@ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPi
29
31
  maxPitch: number;
30
32
  onPrimaryTap: () => void;
31
33
  }): import("react").JSX.Element;
32
- export declare function TouchControlsDock({ scheme, sink, style, scale, }: {
34
+ export declare function TouchControlsDock({ scheme, sink, style, scale, joystickVariant, }: {
33
35
  scheme: TouchScheme;
34
36
  sink: TouchCodeSink;
35
37
  /** Player-selected skin; falls back to the scheme's game default. */
36
38
  style?: TouchStyle;
37
39
  scale?: number;
40
+ /** Player-selected joystick behavior (Settings → Controls). Default `floating`. */
41
+ joystickVariant?: TouchJoystickVariant;
38
42
  }): import("react").JSX.Element;
@@ -10,7 +10,9 @@ const ACTIONS_ZONE = { id: "control:actions", kind: "control", collisionPolicy:
10
10
  const UTILITY_ZONE = { id: "control:utility", kind: "control", collisionPolicy: "warn", collisionGroup: "touch-dock" };
11
11
  const JOYSTICK_SIZE = 132;
12
12
  const JOYSTICK_THUMB = 52;
13
- const JOYSTICK_DEADZONE = 0.28;
13
+ /** Radial deflection below which the stick reads as centered; travel past it rescales to 0..1 so analog output starts from zero instead of jumping. */
14
+ const JOYSTICK_DEADZONE = 0.16;
15
+ /** Digital fallback codes fire past this per-axis deflection — kept for edge-detection consumers (`justPressed`); analog movement itself flows through `onAnalog`. */
14
16
  const JOYSTICK_AXIS_THRESHOLD = 0.42;
15
17
  const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
16
18
  const BUTTON_SIZE = 56;
@@ -242,11 +244,60 @@ function joystickDirections(joystick, nx, ny) {
242
244
  active.add(joystick.right);
243
245
  return active;
244
246
  }
247
+ /**
248
+ * Per-action analog magnitudes for a stick deflection: radial deadzone, then travel rescaled to
249
+ * 0..1 and split across the joystick's four mapped actions. This is what actually moves the
250
+ * player — the digital codes above only exist for edge-detection consumers.
251
+ */
252
+ function joystickAnalogValues(joystick, nx, ny) {
253
+ const values = {};
254
+ const assign = (action, magnitude) => {
255
+ if (action !== null)
256
+ values[action] = magnitude;
257
+ };
258
+ const length = Math.hypot(nx, ny);
259
+ let dx = 0;
260
+ let dy = 0;
261
+ if (length >= JOYSTICK_DEADZONE) {
262
+ const travel = Math.min(1, (length - JOYSTICK_DEADZONE) / (1 - JOYSTICK_DEADZONE));
263
+ dx = (nx / length) * travel;
264
+ dy = (ny / length) * travel;
265
+ }
266
+ assign(joystick.up, Math.max(0, -dy));
267
+ assign(joystick.down, Math.max(0, dy));
268
+ assign(joystick.left, Math.max(0, -dx));
269
+ assign(joystick.right, Math.max(0, dx));
270
+ return values;
271
+ }
272
+ /** Shared drag state machine for both joystick variants: converts pointer travel around an origin into digital edge codes + analog magnitudes, and releases both on lift. */
273
+ function useJoystickDrag(joystick, sink) {
274
+ const activeRef = useRef(new Set());
275
+ const applyNormalized = (nx, ny) => {
276
+ const next = joystickDirections(joystick, nx, ny);
277
+ for (const action of activeRef.current) {
278
+ if (!next.has(action))
279
+ sink.onCodeUp(touchCode(action));
280
+ }
281
+ for (const action of next) {
282
+ if (!activeRef.current.has(action))
283
+ sink.onCodeDown(touchCode(action));
284
+ }
285
+ activeRef.current = next;
286
+ sink.onAnalog?.(joystickAnalogValues(joystick, nx, ny));
287
+ };
288
+ const release = () => {
289
+ for (const action of activeRef.current)
290
+ sink.onCodeUp(touchCode(action));
291
+ activeRef.current = new Set();
292
+ sink.onAnalog?.(null);
293
+ };
294
+ return { applyNormalized, release };
295
+ }
245
296
  function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
246
297
  const baseRef = useRef(null);
247
298
  const thumbRef = useRef(null);
248
299
  const pointerIdRef = useRef(null);
249
- const activeRef = useRef(new Set());
300
+ const drag = useJoystickDrag(joystick, sink);
250
301
  const size = JOYSTICK_SIZE * scale;
251
302
  const thumbSize = JOYSTICK_THUMB * scale;
252
303
  const applyVector = (clientX, clientY) => {
@@ -265,22 +316,11 @@ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
265
316
  }
266
317
  const travel = radius - thumbSize / 2;
267
318
  thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
268
- const next = joystickDirections(joystick, nx, ny);
269
- for (const action of activeRef.current) {
270
- if (!next.has(action))
271
- sink.onCodeUp(touchCode(action));
272
- }
273
- for (const action of next) {
274
- if (!activeRef.current.has(action))
275
- sink.onCodeDown(touchCode(action));
276
- }
277
- activeRef.current = next;
319
+ drag.applyNormalized(nx, ny);
278
320
  };
279
321
  const releaseAll = () => {
280
322
  pointerIdRef.current = null;
281
- for (const action of activeRef.current)
282
- sink.onCodeUp(touchCode(action));
283
- activeRef.current = new Set();
323
+ drag.release();
284
324
  if (thumbRef.current !== null)
285
325
  thumbRef.current.style.transform = "translate(0px, 0px)";
286
326
  };
@@ -302,6 +342,85 @@ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
302
342
  releaseAll();
303
343
  }, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) }));
304
344
  }
345
+ /** Capture-zone footprint of the floating joystick — generous around the dock corner, but bounded so it never blankets HUD elements further up the screen (the zone eats pointer events). */
346
+ const FLOATING_ZONE_WIDTH = "min(42vw, 300px)";
347
+ const FLOATING_ZONE_HEIGHT = "min(40vh, 260px)";
348
+ /**
349
+ * The floating joystick variant: its dock corner is an invisible capture zone with a faint
350
+ * resting ring for discoverability; the stick spawns centered under wherever the thumb lands and
351
+ * follows the same deadzone/analog math as the fixed stick. The modern mobile-shooter default —
352
+ * the hand never has to find a fixed target mid-fight.
353
+ */
354
+ function FloatingJoystick({ joystick, sink, tokens, scale = 1, }) {
355
+ const pointerIdRef = useRef(null);
356
+ const originRef = useRef(null);
357
+ const baseRef = useRef(null);
358
+ const thumbRef = useRef(null);
359
+ const [engaged, setEngaged] = useState(false);
360
+ const drag = useJoystickDrag(joystick, sink);
361
+ const size = JOYSTICK_SIZE * scale;
362
+ const thumbSize = JOYSTICK_THUMB * scale;
363
+ const radius = size / 2;
364
+ const applyVector = (clientX, clientY) => {
365
+ const origin = originRef.current;
366
+ const thumb = thumbRef.current;
367
+ if (origin === null || thumb === null)
368
+ return;
369
+ let nx = (clientX - origin.x) / radius;
370
+ let ny = (clientY - origin.y) / radius;
371
+ const length = Math.hypot(nx, ny);
372
+ if (length > 1) {
373
+ nx /= length;
374
+ ny /= length;
375
+ }
376
+ const travel = radius - thumbSize / 2;
377
+ thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
378
+ drag.applyNormalized(nx, ny);
379
+ };
380
+ const releaseAll = () => {
381
+ pointerIdRef.current = null;
382
+ originRef.current = null;
383
+ setEngaged(false);
384
+ drag.release();
385
+ if (thumbRef.current !== null)
386
+ thumbRef.current.style.transform = "translate(0px, 0px)";
387
+ };
388
+ return (_jsxs("div", { className: "pointer-events-auto relative touch-none select-none", style: { width: FLOATING_ZONE_WIDTH, height: FLOATING_ZONE_HEIGHT }, onPointerDown: (event) => {
389
+ if (pointerIdRef.current !== null)
390
+ return;
391
+ pointerIdRef.current = event.pointerId;
392
+ event.currentTarget.setPointerCapture(event.pointerId);
393
+ const zone = event.currentTarget.getBoundingClientRect();
394
+ // Clamp the spawn point so the whole base stays inside the zone.
395
+ const x = Math.min(Math.max(event.clientX, zone.left + radius), zone.right - radius);
396
+ const y = Math.min(Math.max(event.clientY, zone.top + radius), zone.bottom - radius);
397
+ originRef.current = { x, y };
398
+ setEngaged(true);
399
+ const base = baseRef.current;
400
+ if (base !== null) {
401
+ base.style.left = `${x - zone.left - radius}px`;
402
+ base.style.top = `${y - zone.top - radius}px`;
403
+ }
404
+ applyVector(event.clientX, event.clientY);
405
+ }, onPointerMove: (event) => {
406
+ if (event.pointerId !== pointerIdRef.current)
407
+ return;
408
+ event.stopPropagation();
409
+ applyVector(event.clientX, event.clientY);
410
+ }, onPointerUp: (event) => {
411
+ if (event.pointerId !== pointerIdRef.current)
412
+ return;
413
+ event.stopPropagation();
414
+ releaseAll();
415
+ }, onPointerCancel: () => releaseAll(), children: [_jsx("div", { className: "pointer-events-none absolute rounded-full transition-opacity", style: {
416
+ width: size,
417
+ height: size,
418
+ left: 0,
419
+ bottom: 0,
420
+ opacity: engaged ? 0 : 0.4,
421
+ ...tokens.joyBase,
422
+ } }), _jsx("div", { ref: baseRef, className: "pointer-events-none absolute flex items-center justify-center rounded-full", style: { width: size, height: size, visibility: engaged ? "visible" : "hidden", ...tokens.joyBase }, children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) })] }));
423
+ }
305
424
  function touchButtonIcon(button) {
306
425
  if (button.icon === false)
307
426
  return null;
@@ -446,7 +565,7 @@ function bucketByAnchor(buttons) {
446
565
  }
447
566
  return map;
448
567
  }
449
- export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
568
+ export function TouchControlsDock({ scheme, sink, style, scale = 1, joystickVariant = "floating", }) {
450
569
  const tokens = STYLE_TOKENS[style ?? scheme.style];
451
570
  const layout = scheme.layout;
452
571
  const docked = scheme.buttons.filter((button) => button.anchor === null);
@@ -461,7 +580,7 @@ export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
461
580
  useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
462
581
  const actionsColumn = layout.actions === "left" || layout.actions === "right";
463
582
  const utilityColumn = layout.utility === "left" || layout.utility === "right";
464
- return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale }) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
583
+ return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: joystickVariant === "floating" ? (_jsx(FloatingJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) : (_jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
465
584
  }
466
585
  function ResolvedLayout({ children }) {
467
586
  return _jsx("div", { className: "pointer-events-none absolute inset-0 z-40", children: children });