@jgengine/shell 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Props for {@link GeneratedAsset}: the placed instance's `meta` (assetId + params + seed) and transform. */
|
|
2
|
+
export interface GeneratedAssetProps {
|
|
3
|
+
/** The placed instance meta — `{ assetId, seed, ...params }`. Re-resolved every render, never baked. */
|
|
4
|
+
meta: Record<string, unknown> | undefined;
|
|
5
|
+
position?: readonly [number, number, number];
|
|
6
|
+
rotationY?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders one placed generator-asset instance by re-resolving its `meta` through the registered
|
|
10
|
+
* generator and instancing the parts. Returns null when the meta names no generator. The runtime
|
|
11
|
+
* counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
|
|
12
|
+
* @internal — `AuthoredScene` mounts this for generator markers automatically.
|
|
13
|
+
*/
|
|
14
|
+
export declare function GeneratedAssetInstance({ meta, position, rotationY }: GeneratedAssetProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { resolveGeneratorAsset } from "@jgengine/core/scene/assetGenerator";
|
|
5
|
+
const UNIT_BOX = new THREE.BoxGeometry(1, 1, 1);
|
|
6
|
+
const NEUTRAL = "#b8b0a4";
|
|
7
|
+
/** All parts of one generated asset, batched into one instanced box draw per color. */
|
|
8
|
+
function GeneratedParts({ asset }) {
|
|
9
|
+
const byColor = useMemo(() => {
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const part of asset.parts) {
|
|
12
|
+
const color = part.color ?? NEUTRAL;
|
|
13
|
+
const bucket = groups.get(color);
|
|
14
|
+
if (bucket === undefined)
|
|
15
|
+
groups.set(color, [part]);
|
|
16
|
+
else
|
|
17
|
+
bucket.push(part);
|
|
18
|
+
}
|
|
19
|
+
return [...groups.entries()];
|
|
20
|
+
}, [asset]);
|
|
21
|
+
return (_jsx(_Fragment, { children: byColor.map(([color, parts]) => (_jsx(ColorBatch, { color: color, parts: parts }, color))) }));
|
|
22
|
+
}
|
|
23
|
+
function ColorBatch({ color, parts }) {
|
|
24
|
+
const meshRef = useRef(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const mesh = meshRef.current;
|
|
27
|
+
if (mesh === null)
|
|
28
|
+
return;
|
|
29
|
+
const matrix = new THREE.Matrix4();
|
|
30
|
+
const position = new THREE.Vector3();
|
|
31
|
+
const quaternion = new THREE.Quaternion();
|
|
32
|
+
const euler = new THREE.Euler();
|
|
33
|
+
const scale = new THREE.Vector3();
|
|
34
|
+
parts.forEach((part, index) => {
|
|
35
|
+
position.set(part.position[0], part.position[1], part.position[2]);
|
|
36
|
+
euler.set(0, part.rotationY ?? 0, 0);
|
|
37
|
+
quaternion.setFromEuler(euler);
|
|
38
|
+
scale.set(Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2]));
|
|
39
|
+
matrix.compose(position, quaternion, scale);
|
|
40
|
+
mesh.setMatrixAt(index, matrix);
|
|
41
|
+
});
|
|
42
|
+
mesh.count = parts.length;
|
|
43
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
44
|
+
mesh.computeBoundingSphere();
|
|
45
|
+
}, [parts]);
|
|
46
|
+
return (_jsx("instancedMesh", { ref: meshRef, args: [UNIT_BOX, undefined, Math.max(1, parts.length)], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 0.62, metalness: 0.04 }) }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Renders one placed generator-asset instance by re-resolving its `meta` through the registered
|
|
50
|
+
* generator and instancing the parts. Returns null when the meta names no generator. The runtime
|
|
51
|
+
* counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
|
|
52
|
+
* @internal — `AuthoredScene` mounts this for generator markers automatically.
|
|
53
|
+
*/
|
|
54
|
+
export function GeneratedAssetInstance({ meta, position = [0, 0, 0], rotationY = 0 }) {
|
|
55
|
+
const asset = useMemo(() => resolveGeneratorAsset(meta), [meta]);
|
|
56
|
+
if (asset === null)
|
|
57
|
+
return null;
|
|
58
|
+
return (_jsx("group", { position: [position[0], position[1], position[2]], rotation: [0, rotationY, 0], children: _jsx(GeneratedParts, { asset: asset }) }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { resolveGrassObject } from "@jgengine/core/world/grassKind";
|
|
4
|
+
import { GrassField } from "../terrain/GrassField.js";
|
|
5
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
6
|
+
/** Volume kinds turf yields to — blades never grow through cracked soil or out of open water. */
|
|
7
|
+
const GRASS_YIELDS_TO = new Set(["soil", "water"]);
|
|
8
|
+
/**
|
|
9
|
+
* Patch-local no-grow rects from the document's soil/water volumes. Water rects shrink by the
|
|
10
|
+
* shore margin so a fringe of blades still wades into the shallows like a real pond edge.
|
|
11
|
+
*/
|
|
12
|
+
function grassExclusionsFrom(context, center) {
|
|
13
|
+
const zones = [];
|
|
14
|
+
for (const volume of context.document.volumes) {
|
|
15
|
+
if (!GRASS_YIELDS_TO.has(volume.kind) || volume.halfExtents === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const shoreMargin = volume.kind === "water" ? 1.6 : 0;
|
|
18
|
+
const halfX = Math.max(0, volume.halfExtents.x - shoreMargin);
|
|
19
|
+
const halfZ = Math.max(0, volume.halfExtents.z - shoreMargin);
|
|
20
|
+
if (halfX <= 0 || halfZ <= 0)
|
|
21
|
+
continue;
|
|
22
|
+
zones.push({
|
|
23
|
+
center: [volume.center.x - center[0], volume.center.z - center[2]],
|
|
24
|
+
half: [halfX, halfZ],
|
|
25
|
+
feather: volume.kind === "water" ? 2.4 : 1.2,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return zones;
|
|
29
|
+
}
|
|
30
|
+
function hashSeed(text) {
|
|
31
|
+
let h = 2166136261 >>> 0;
|
|
32
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
33
|
+
h ^= text.charCodeAt(i);
|
|
34
|
+
h = Math.imul(h, 16777619);
|
|
35
|
+
}
|
|
36
|
+
return (h >>> 0) % 100000;
|
|
37
|
+
}
|
|
38
|
+
function sampleHeight(field, x, z) {
|
|
39
|
+
return field.sampleHeight(x, z);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One authored grass volume → an instanced vertex-wind `GrassField` over its footprint.
|
|
43
|
+
* `area`/`bladeHeight`/`wind`/`heightAt` are memoized so `GrassField`'s internal geometry and
|
|
44
|
+
* material `useMemo`s see stable references across re-renders — otherwise every re-render (the
|
|
45
|
+
* live-sync document context object is recreated per render) rebuilds the blade geometry and
|
|
46
|
+
* shader material from scratch, which shows up as recurring render hitches.
|
|
47
|
+
*/
|
|
48
|
+
function OneGrass({ object, context }) {
|
|
49
|
+
const resolved = useMemo(() => resolveGrassObject(object), [object]);
|
|
50
|
+
const heightAt = useCallback((x, z) => sampleHeight(context.field, x, z), [context.field]);
|
|
51
|
+
const area = useMemo(() => (resolved === null ? null : [resolved.size[0], resolved.size[1]]), [resolved]);
|
|
52
|
+
const bladeHeight = useMemo(() => (resolved === null ? null : [resolved.rules.bladeHeight * 0.7, resolved.rules.bladeHeight]), [resolved]);
|
|
53
|
+
const wind = useMemo(() => resolved === null
|
|
54
|
+
? null
|
|
55
|
+
: { strength: resolved.rules.windStrength, speed: resolved.rules.windSpeed, gustScale: resolved.rules.windGust }, [resolved]);
|
|
56
|
+
// Depends on the volumes slice, not the context object — the live-sync context is recreated
|
|
57
|
+
// per render and would otherwise rebuild the tuft geometry every frame.
|
|
58
|
+
const volumes = context.document.volumes;
|
|
59
|
+
const exclude = useMemo(() => (resolved === null ? null : grassExclusionsFrom(context, resolved.center)),
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
+
[volumes, resolved]);
|
|
62
|
+
if (resolved === null || area === null || bladeHeight === null || wind === null || exclude === null)
|
|
63
|
+
return null;
|
|
64
|
+
const { rules, center } = resolved;
|
|
65
|
+
// Size the instance buffer to the authored patch (bounded): the default 1500-blade buffer reads
|
|
66
|
+
// as stubble on anything bigger than a lawn, silently ignoring the authored density.
|
|
67
|
+
const instanceCount = Math.max(1500, Math.min(250000, Math.ceil(rules.density * area[0] * area[1])));
|
|
68
|
+
return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
|
|
69
|
+
}
|
|
70
|
+
/** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
71
|
+
export function registerGrassFieldRenderer() {
|
|
72
|
+
registerSceneKindRenderer("grass_field", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneGrass, { object: object, context: context }, object.id))) })));
|
|
73
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime renderer for the built-in `pole_line` scene kind: instanced poles (proxy cylinders, or a
|
|
4
|
+
* GLB pole asset when one is named) plus merged tube geometry for the sagging cables. Registered by
|
|
5
|
+
* `registerBuiltinSceneKindRenderers` so any document with a `pole_line` path renders with no game
|
|
6
|
+
* wiring. Uses the generic core primitive `readNamedSockets` to hang wires off a GLB pole's authored
|
|
7
|
+
* crossarm empties (falling back to computed offsets).
|
|
8
|
+
*/
|
|
9
|
+
import { Suspense, useEffect, useMemo, useRef } from "react";
|
|
10
|
+
import { useLoader } from "@react-three/fiber";
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
13
|
+
import { parseParams } from "@jgengine/core/scene/sceneKinds";
|
|
14
|
+
import { readNamedSockets } from "@jgengine/core/scene/modelSockets";
|
|
15
|
+
import { POLE_LINE_KIND, POLE_LINE_SCHEMA, resolvePoleLine } from "@jgengine/core/world/poleLineKind";
|
|
16
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
17
|
+
function Cables({ cables }) {
|
|
18
|
+
const geometry = useMemo(() => {
|
|
19
|
+
const parts = [];
|
|
20
|
+
for (const cable of cables) {
|
|
21
|
+
if (cable.points.length < 2)
|
|
22
|
+
continue;
|
|
23
|
+
const curve = new THREE.CatmullRomCurve3(cable.points.map((point) => new THREE.Vector3(point[0], point[1], point[2])));
|
|
24
|
+
parts.push(new THREE.TubeGeometry(curve, Math.max(2, (cable.points.length - 1) * 2), cable.radius, 6, false));
|
|
25
|
+
}
|
|
26
|
+
if (parts.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const merged = mergePositionGeometries(parts);
|
|
29
|
+
for (const part of parts)
|
|
30
|
+
part.dispose();
|
|
31
|
+
return merged;
|
|
32
|
+
}, [cables]);
|
|
33
|
+
useEffect(() => () => geometry?.dispose(), [geometry]);
|
|
34
|
+
if (geometry === null)
|
|
35
|
+
return null;
|
|
36
|
+
// Dark warm rubber-sheath tone with a hint of sheen — pitch black tubes read as ink scribbles
|
|
37
|
+
// against the sky, real cable catches a little light along its top.
|
|
38
|
+
return (_jsx("mesh", { geometry: geometry, castShadow: true, children: _jsx("meshStandardMaterial", { color: "#31281f", roughness: 0.55, metalness: 0.15 }) }));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Weathered wood for the proxy poles: vertical grain streaks + broad tonal banding driven by the
|
|
42
|
+
* pole's own cylindrical coordinates, offset per instance so no two poles share a pattern, and a
|
|
43
|
+
* sun-bleach lift toward the top. Injected via `onBeforeCompile`, instancing-aware.
|
|
44
|
+
*/
|
|
45
|
+
function createWeatheredWoodMaterial(color, grainAlong = "y") {
|
|
46
|
+
const material = new THREE.MeshStandardMaterial({ color, roughness: 0.92, metalness: 0 });
|
|
47
|
+
material.onBeforeCompile = (shader) => {
|
|
48
|
+
shader.vertexShader = shader.vertexShader
|
|
49
|
+
.replace("#include <common>", `#include <common>
|
|
50
|
+
varying vec3 vJgWoodLocal;
|
|
51
|
+
varying vec2 vJgWoodSeed;`)
|
|
52
|
+
.replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
53
|
+
vJgWoodLocal = position;
|
|
54
|
+
#ifdef USE_INSTANCING
|
|
55
|
+
vJgWoodSeed = (instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xz;
|
|
56
|
+
#else
|
|
57
|
+
vJgWoodSeed = vec2(0.0);
|
|
58
|
+
#endif`);
|
|
59
|
+
shader.fragmentShader = shader.fragmentShader
|
|
60
|
+
.replace("#include <common>", `#include <common>
|
|
61
|
+
varying vec3 vJgWoodLocal;
|
|
62
|
+
varying vec2 vJgWoodSeed;
|
|
63
|
+
float jgWoodHash(vec2 p){
|
|
64
|
+
uvec2 q = uvec2(ivec2(floor(p)));
|
|
65
|
+
uint x = q.x * 1664525u + q.y * 1013904223u;
|
|
66
|
+
x = (x ^ (x >> 16u)) * 2246822519u;
|
|
67
|
+
x = (x ^ (x >> 13u)) * 3266489917u;
|
|
68
|
+
return float(x ^ (x >> 16u)) * (1.0 / 4294967296.0);
|
|
69
|
+
}
|
|
70
|
+
float jgWoodNoise(vec2 p){
|
|
71
|
+
vec2 i = floor(p); vec2 f = fract(p);
|
|
72
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
73
|
+
float a = jgWoodHash(i); float b = jgWoodHash(i + vec2(1.0, 0.0));
|
|
74
|
+
float c = jgWoodHash(i + vec2(0.0, 1.0)); float d = jgWoodHash(i + vec2(1.0, 1.0));
|
|
75
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
76
|
+
}`)
|
|
77
|
+
.replace("#include <color_fragment>", `#include <color_fragment>
|
|
78
|
+
${grainAlong === "y"
|
|
79
|
+
? `vec2 jgWoodUv = vec2(atan(vJgWoodLocal.x, vJgWoodLocal.z) * 1.4, vJgWoodLocal.y * 0.55);`
|
|
80
|
+
: `vec2 jgWoodUv = vec2(vJgWoodLocal.y * 9.0 + vJgWoodLocal.z * 7.0, vJgWoodLocal.x * 0.8);`}
|
|
81
|
+
vec2 jgWoodOff = vec2(dot(vJgWoodSeed, vec2(0.73, 1.31)), dot(vJgWoodSeed, vec2(1.17, 0.41)));
|
|
82
|
+
float jgWoodBand = jgWoodNoise(jgWoodUv * vec2(2.0, 1.0) + jgWoodOff);
|
|
83
|
+
float jgWoodStreak = jgWoodNoise(jgWoodUv * vec2(7.0, 0.35) + jgWoodOff * 1.7);
|
|
84
|
+
diffuseColor.rgb *= mix(0.74, 1.14, jgWoodBand * 0.45 + jgWoodStreak * 0.55);
|
|
85
|
+
${grainAlong === "y" ? `// Sun-bleached crown, damp-darkened foot — reads as years outdoors, not fresh paint.
|
|
86
|
+
diffuseColor.rgb = mix(diffuseColor.rgb * 0.82, diffuseColor.rgb * vec3(1.08, 1.05, 0.98), smoothstep(-3.5, 3.0, vJgWoodLocal.y));` : ""}`);
|
|
87
|
+
};
|
|
88
|
+
material.customProgramCacheKey = () => `jgengine-weathered-wood-${grainAlong}`;
|
|
89
|
+
return material;
|
|
90
|
+
}
|
|
91
|
+
/** Minimal position/normal/index merge — avoids depending on a specific BufferGeometryUtils path. */
|
|
92
|
+
function mergePositionGeometries(geometries) {
|
|
93
|
+
let vertexCount = 0;
|
|
94
|
+
let indexCount = 0;
|
|
95
|
+
for (const geometry of geometries) {
|
|
96
|
+
vertexCount += geometry.getAttribute("position").count;
|
|
97
|
+
const index = geometry.getIndex();
|
|
98
|
+
indexCount += index === null ? geometry.getAttribute("position").count : index.count;
|
|
99
|
+
}
|
|
100
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
101
|
+
const normals = new Float32Array(vertexCount * 3);
|
|
102
|
+
const indices = new Uint32Array(indexCount);
|
|
103
|
+
let vertexOffset = 0;
|
|
104
|
+
let indexOffset = 0;
|
|
105
|
+
for (const geometry of geometries) {
|
|
106
|
+
const position = geometry.getAttribute("position");
|
|
107
|
+
const normal = geometry.getAttribute("normal");
|
|
108
|
+
positions.set(position.array, vertexOffset * 3);
|
|
109
|
+
if (normal !== undefined)
|
|
110
|
+
normals.set(normal.array, vertexOffset * 3);
|
|
111
|
+
const index = geometry.getIndex();
|
|
112
|
+
if (index === null) {
|
|
113
|
+
for (let i = 0; i < position.count; i += 1)
|
|
114
|
+
indices[indexOffset + i] = vertexOffset + i;
|
|
115
|
+
indexOffset += position.count;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
for (let i = 0; i < index.count; i += 1)
|
|
119
|
+
indices[indexOffset + i] = vertexOffset + index.getX(i);
|
|
120
|
+
indexOffset += index.count;
|
|
121
|
+
}
|
|
122
|
+
vertexOffset += position.count;
|
|
123
|
+
}
|
|
124
|
+
const merged = new THREE.BufferGeometry();
|
|
125
|
+
merged.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
126
|
+
merged.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
|
|
127
|
+
merged.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
128
|
+
return merged;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Proxy utility poles built from primitives when no GLB pole asset is named: a tapered trunk, a
|
|
132
|
+
* wood crossarm sized to the wire spread with a small brace, and one ceramic insulator peg under
|
|
133
|
+
* each wire offset — so the cables visibly hang *from* the pole instead of floating beside a stick.
|
|
134
|
+
*/
|
|
135
|
+
function ProxyPoles({ poles, height, wireCount, wireSpacing, }) {
|
|
136
|
+
const trunkRef = useRef(null);
|
|
137
|
+
const armRef = useRef(null);
|
|
138
|
+
const braceRef = useRef(null);
|
|
139
|
+
const insulatorRef = useRef(null);
|
|
140
|
+
const armLength = wireCount > 1 ? (wireCount - 1) * wireSpacing + 0.6 : 0.8;
|
|
141
|
+
// Chunkier members than a real-scale pole: stylized worlds read thin cylinders as wire, not wood.
|
|
142
|
+
const trunkGeometry = useMemo(() => new THREE.CylinderGeometry(0.13, 0.22, height, 10), [height]);
|
|
143
|
+
const armGeometry = useMemo(() => new THREE.BoxGeometry(armLength, 0.17, 0.14), [armLength]);
|
|
144
|
+
const braceGeometry = useMemo(() => {
|
|
145
|
+
const braceSpan = Math.hypot(armLength * 0.32, 0.7);
|
|
146
|
+
return new THREE.BoxGeometry(0.07, braceSpan, 0.06);
|
|
147
|
+
}, [armLength]);
|
|
148
|
+
const insulatorGeometry = useMemo(() => new THREE.CylinderGeometry(0.06, 0.085, 0.24, 8), []);
|
|
149
|
+
const trunkMaterial = useMemo(() => createWeatheredWoodMaterial("#6b533a"), []);
|
|
150
|
+
const armMaterial = useMemo(() => createWeatheredWoodMaterial("#59452e", "x"), []);
|
|
151
|
+
useEffect(() => () => {
|
|
152
|
+
trunkGeometry.dispose();
|
|
153
|
+
armGeometry.dispose();
|
|
154
|
+
braceGeometry.dispose();
|
|
155
|
+
insulatorGeometry.dispose();
|
|
156
|
+
}, [trunkGeometry, armGeometry, braceGeometry, insulatorGeometry]);
|
|
157
|
+
useEffect(() => () => {
|
|
158
|
+
trunkMaterial.dispose();
|
|
159
|
+
armMaterial.dispose();
|
|
160
|
+
}, [trunkMaterial, armMaterial]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const trunk = trunkRef.current;
|
|
163
|
+
const arm = armRef.current;
|
|
164
|
+
const brace = braceRef.current;
|
|
165
|
+
const insulator = insulatorRef.current;
|
|
166
|
+
if (trunk === null)
|
|
167
|
+
return;
|
|
168
|
+
const matrix = new THREE.Matrix4();
|
|
169
|
+
const position = new THREE.Vector3();
|
|
170
|
+
const quaternion = new THREE.Quaternion();
|
|
171
|
+
const euler = new THREE.Euler();
|
|
172
|
+
const scale = new THREE.Vector3(1, 1, 1);
|
|
173
|
+
const armY = height - 0.22;
|
|
174
|
+
const half = (wireCount - 1) / 2;
|
|
175
|
+
const braceAngle = Math.atan2(armLength * 0.32, 0.7);
|
|
176
|
+
poles.forEach((pole, index) => {
|
|
177
|
+
euler.set(0, pole.yaw, 0);
|
|
178
|
+
quaternion.setFromEuler(euler);
|
|
179
|
+
position.set(pole.position[0], pole.position[1] + height / 2, pole.position[2]);
|
|
180
|
+
matrix.compose(position, quaternion, scale);
|
|
181
|
+
trunk.setMatrixAt(index, matrix);
|
|
182
|
+
if (arm !== null) {
|
|
183
|
+
position.set(pole.position[0], pole.position[1] + armY, pole.position[2]);
|
|
184
|
+
matrix.compose(position, quaternion, scale);
|
|
185
|
+
arm.setMatrixAt(index, matrix);
|
|
186
|
+
}
|
|
187
|
+
if (brace !== null) {
|
|
188
|
+
// Two diagonal braces from the trunk up to the crossarm — the classic A-frame silhouette.
|
|
189
|
+
const cos = Math.cos(pole.yaw);
|
|
190
|
+
const sin = Math.sin(pole.yaw);
|
|
191
|
+
for (let side = 0; side < 2; side += 1) {
|
|
192
|
+
const direction = side === 0 ? 1 : -1;
|
|
193
|
+
const lateral = direction * armLength * 0.16;
|
|
194
|
+
position.set(pole.position[0] + cos * lateral, pole.position[1] + armY - 0.38, pole.position[2] - sin * lateral);
|
|
195
|
+
euler.set(0, pole.yaw, direction * braceAngle);
|
|
196
|
+
quaternion.setFromEuler(euler);
|
|
197
|
+
matrix.compose(position, quaternion, scale);
|
|
198
|
+
brace.setMatrixAt(index * 2 + side, matrix);
|
|
199
|
+
}
|
|
200
|
+
euler.set(0, pole.yaw, 0);
|
|
201
|
+
quaternion.setFromEuler(euler);
|
|
202
|
+
}
|
|
203
|
+
if (insulator !== null) {
|
|
204
|
+
// Insulator pegs sit on the crossarm at each wire's lateral offset (the pole's local X axis
|
|
205
|
+
// after yaw — the same perpendicular the resolver strings cables along).
|
|
206
|
+
const cos = Math.cos(pole.yaw);
|
|
207
|
+
const sin = Math.sin(pole.yaw);
|
|
208
|
+
for (let w = 0; w < wireCount; w += 1) {
|
|
209
|
+
const offset = (w - half) * wireSpacing;
|
|
210
|
+
position.set(pole.position[0] + cos * offset, pole.position[1] + height - 0.08, pole.position[2] - sin * offset);
|
|
211
|
+
matrix.compose(position, quaternion, scale);
|
|
212
|
+
insulator.setMatrixAt(index * wireCount + w, matrix);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
trunk.count = poles.length;
|
|
217
|
+
trunk.instanceMatrix.needsUpdate = true;
|
|
218
|
+
trunk.computeBoundingSphere();
|
|
219
|
+
if (arm !== null) {
|
|
220
|
+
arm.count = poles.length;
|
|
221
|
+
arm.instanceMatrix.needsUpdate = true;
|
|
222
|
+
arm.computeBoundingSphere();
|
|
223
|
+
}
|
|
224
|
+
if (brace !== null) {
|
|
225
|
+
brace.count = poles.length * 2;
|
|
226
|
+
brace.instanceMatrix.needsUpdate = true;
|
|
227
|
+
brace.computeBoundingSphere();
|
|
228
|
+
}
|
|
229
|
+
if (insulator !== null) {
|
|
230
|
+
insulator.count = poles.length * wireCount;
|
|
231
|
+
insulator.instanceMatrix.needsUpdate = true;
|
|
232
|
+
insulator.computeBoundingSphere();
|
|
233
|
+
}
|
|
234
|
+
}, [poles, height, wireCount, wireSpacing]);
|
|
235
|
+
return (_jsxs(_Fragment, { children: [_jsx("instancedMesh", { ref: trunkRef, args: [trunkGeometry, trunkMaterial, Math.max(1, poles.length)], castShadow: true, receiveShadow: true }), wireCount > 0 ? (_jsxs(_Fragment, { children: [_jsx("instancedMesh", { ref: armRef, args: [armGeometry, armMaterial, Math.max(1, poles.length)], castShadow: true, receiveShadow: true }), _jsx("instancedMesh", { ref: braceRef, args: [braceGeometry, armMaterial, Math.max(1, poles.length * 2)], castShadow: true }), _jsx("instancedMesh", { ref: insulatorRef, args: [insulatorGeometry, undefined, Math.max(1, poles.length * wireCount)], castShadow: true, children: _jsx("meshStandardMaterial", { color: "#a8a18c", roughness: 0.32, metalness: 0.12 }) })] })) : null] }));
|
|
236
|
+
}
|
|
237
|
+
function ModelPoles({ url, poles }) {
|
|
238
|
+
const gltf = useLoader(GLTFLoader, url);
|
|
239
|
+
const sources = useMemo(() => {
|
|
240
|
+
const meshes = [];
|
|
241
|
+
gltf.scene.updateWorldMatrix(true, true);
|
|
242
|
+
gltf.scene.traverse((node) => {
|
|
243
|
+
const mesh = node;
|
|
244
|
+
if (mesh.isMesh)
|
|
245
|
+
meshes.push({ geometry: mesh.geometry, material: mesh.material, local: mesh.matrixWorld.clone() });
|
|
246
|
+
});
|
|
247
|
+
// readNamedSockets is available for wire-anchor refinement; the proxy path already honors offsets.
|
|
248
|
+
void readNamedSockets(gltf.scene);
|
|
249
|
+
return meshes;
|
|
250
|
+
}, [gltf]);
|
|
251
|
+
return (_jsx(_Fragment, { children: sources.map((source, index) => (_jsx(ModelPoleSource, { source: source, poles: poles }, index))) }));
|
|
252
|
+
}
|
|
253
|
+
function ModelPoleSource({ source, poles }) {
|
|
254
|
+
const meshRef = useRef(null);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const mesh = meshRef.current;
|
|
257
|
+
if (mesh === null)
|
|
258
|
+
return;
|
|
259
|
+
const base = new THREE.Matrix4();
|
|
260
|
+
const composed = new THREE.Matrix4();
|
|
261
|
+
const position = new THREE.Vector3();
|
|
262
|
+
const quaternion = new THREE.Quaternion();
|
|
263
|
+
const euler = new THREE.Euler();
|
|
264
|
+
const scale = new THREE.Vector3(1, 1, 1);
|
|
265
|
+
poles.forEach((pole, index) => {
|
|
266
|
+
position.set(pole.position[0], pole.position[1], pole.position[2]);
|
|
267
|
+
euler.set(0, pole.yaw, 0);
|
|
268
|
+
quaternion.setFromEuler(euler);
|
|
269
|
+
base.compose(position, quaternion, scale);
|
|
270
|
+
composed.multiplyMatrices(base, source.local);
|
|
271
|
+
mesh.setMatrixAt(index, composed);
|
|
272
|
+
});
|
|
273
|
+
mesh.count = poles.length;
|
|
274
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
275
|
+
mesh.computeBoundingSphere();
|
|
276
|
+
}, [poles, source]);
|
|
277
|
+
return _jsx("instancedMesh", { ref: meshRef, args: [source.geometry, source.material, Math.max(1, poles.length)], castShadow: true, receiveShadow: true });
|
|
278
|
+
}
|
|
279
|
+
function OnePoleLine({ object, context }) {
|
|
280
|
+
const resolved = useMemo(() => resolvePoleLine(object, parseParams(POLE_LINE_SCHEMA, object.meta), { sampleHeight: (x, z) => context.field.sampleHeight(x, z) }), [object, context.field]);
|
|
281
|
+
const modelUrl = resolved.poleAsset.length > 0 ? context.assets?.resolve(resolved.poleAsset)?.url : undefined;
|
|
282
|
+
return (_jsxs(_Fragment, { children: [modelUrl !== undefined ? (_jsx(Suspense, { fallback: _jsx(ProxyPoles, { poles: resolved.poles, height: resolved.poleHeight, wireCount: resolved.wireCount, wireSpacing: resolved.wireSpacing }), children: _jsx(ModelPoles, { url: modelUrl, poles: resolved.poles }) })) : (_jsx(ProxyPoles, { poles: resolved.poles, height: resolved.poleHeight, wireCount: resolved.wireCount, wireSpacing: resolved.wireSpacing })), _jsx(Cables, { cables: resolved.cables })] }));
|
|
283
|
+
}
|
|
284
|
+
/** Registers the built-in pole-line runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
285
|
+
export function registerPoleLineRenderer() {
|
|
286
|
+
registerSceneKindRenderer(POLE_LINE_KIND, ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OnePoleLine, { object: object, context: context }, object.id))) })));
|
|
287
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { resolveSoilObject } from "@jgengine/core/world/soilKind";
|
|
5
|
+
import { createSoilPatchMaterial } from "../terrain/soilPatchMaterial.js";
|
|
6
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
7
|
+
/** Height offset above the sampled terrain so the draped patch never z-fights the ground. */
|
|
8
|
+
const SOIL_LIFT = 0.04;
|
|
9
|
+
/** Amplitude of the seeded micro-relief layered onto the drape, meters. */
|
|
10
|
+
const SOIL_RELIEF = 0.13;
|
|
11
|
+
/** Cheap deterministic 2D value noise (two octaves) for the patch's baked micro-relief. */
|
|
12
|
+
function reliefNoise(x, z) {
|
|
13
|
+
const n = (px, pz) => {
|
|
14
|
+
const s = Math.sin(px * 12.9898 + pz * 78.233) * 43758.5453;
|
|
15
|
+
return s - Math.floor(s);
|
|
16
|
+
};
|
|
17
|
+
const octave = (px, pz) => {
|
|
18
|
+
const ix = Math.floor(px);
|
|
19
|
+
const iz = Math.floor(pz);
|
|
20
|
+
const fx = px - ix;
|
|
21
|
+
const fz = pz - iz;
|
|
22
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
23
|
+
const uz = fz * fz * (3 - 2 * fz);
|
|
24
|
+
const a = n(ix, iz);
|
|
25
|
+
const b = n(ix + 1, iz);
|
|
26
|
+
const c = n(ix, iz + 1);
|
|
27
|
+
const d = n(ix + 1, iz + 1);
|
|
28
|
+
return a + (b - a) * ux + (c - a) * uz + (a - b - c + d) * ux * uz;
|
|
29
|
+
};
|
|
30
|
+
return octave(x * 0.7, z * 0.7) * 0.65 + octave(x * 2.3, z * 2.3) * 0.35;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* One authored soil volume → a subdivided plane draped over the live terrain (each vertex sampled
|
|
34
|
+
* from the height field) with the crack/moss shader and a noise-eroded edge fade, so the patch hugs
|
|
35
|
+
* relief and dissolves into the surrounding ground instead of reading as a hovering brown box.
|
|
36
|
+
*/
|
|
37
|
+
function OneSoil({ object, context }) {
|
|
38
|
+
const resolved = useMemo(() => resolveSoilObject(object), [object]);
|
|
39
|
+
const groundColorAt = context.groundColorAt;
|
|
40
|
+
const material = useMemo(() => resolved === null
|
|
41
|
+
? null
|
|
42
|
+
: createSoilPatchMaterial(resolved.rules, {
|
|
43
|
+
center: [resolved.center[0], resolved.center[2]],
|
|
44
|
+
halfSize: [resolved.size[0] / 2, resolved.size[1] / 2],
|
|
45
|
+
...(groundColorAt === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { groundColor: groundColorAt(resolved.center[0], resolved.center[2]) }),
|
|
48
|
+
}), [resolved, groundColorAt]);
|
|
49
|
+
const geometry = useMemo(() => {
|
|
50
|
+
if (resolved === null)
|
|
51
|
+
return null;
|
|
52
|
+
const [w, d] = resolved.size;
|
|
53
|
+
const segX = Math.min(96, Math.max(12, Math.round(w / 0.4)));
|
|
54
|
+
const segZ = Math.min(96, Math.max(12, Math.round(d / 0.4)));
|
|
55
|
+
const plane = new THREE.PlaneGeometry(w, d, segX, segZ);
|
|
56
|
+
plane.rotateX(-Math.PI / 2);
|
|
57
|
+
const positions = plane.getAttribute("position");
|
|
58
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
59
|
+
const x = positions.getX(i);
|
|
60
|
+
const z = positions.getZ(i);
|
|
61
|
+
const wx = resolved.center[0] + x;
|
|
62
|
+
const wz = resolved.center[2] + z;
|
|
63
|
+
// Seeded micro-relief so the cracked dirt catches light like packed earth, not a flat decal;
|
|
64
|
+
// the amplitude eases out toward the border so the fading edge still meets the ground flush.
|
|
65
|
+
const edgeX = 1 - Math.min(1, Math.abs(x) / (w / 2));
|
|
66
|
+
const edgeZ = 1 - Math.min(1, Math.abs(z) / (d / 2));
|
|
67
|
+
const edgeEase = Math.min(1, Math.min(edgeX, edgeZ) * 3);
|
|
68
|
+
const relief = (reliefNoise(wx, wz) - 0.5) * 2 * SOIL_RELIEF * edgeEase;
|
|
69
|
+
positions.setY(i, context.field.sampleHeight(wx, wz) + SOIL_LIFT + relief);
|
|
70
|
+
}
|
|
71
|
+
positions.needsUpdate = true;
|
|
72
|
+
plane.computeVertexNormals();
|
|
73
|
+
return plane;
|
|
74
|
+
}, [resolved, context.field]);
|
|
75
|
+
useEffect(() => () => material?.dispose(), [material]);
|
|
76
|
+
useEffect(() => () => geometry?.dispose(), [geometry]);
|
|
77
|
+
if (resolved === null || material === null || geometry === null)
|
|
78
|
+
return null;
|
|
79
|
+
return _jsx("mesh", { position: [resolved.center[0], 0, resolved.center[2]], geometry: geometry, material: material, receiveShadow: true });
|
|
80
|
+
}
|
|
81
|
+
/** Registers the `soil` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
82
|
+
export function registerSoilPatchRenderer() {
|
|
83
|
+
registerSceneKindRenderer("soil", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneSoil, { object: object, context: context }, object.id))) })));
|
|
84
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Lighting mood for a {@link StudioStage} — a named 3-point rig + backdrop palette. */
|
|
3
|
+
export type StudioMood = "studio" | "daylight" | "dusk" | "night";
|
|
4
|
+
/** Props for {@link StudioStage}. */
|
|
5
|
+
export interface StudioStageProps {
|
|
6
|
+
mood?: StudioMood;
|
|
7
|
+
/** Override the scene background color (hex). Default: the mood's backdrop. */
|
|
8
|
+
backdrop?: string;
|
|
9
|
+
/** Turntable spin speed in radians/second (spins the children). 0 disables. Default 0. */
|
|
10
|
+
turntable?: number;
|
|
11
|
+
/** Draw the seamless floor + set the scene background. Off for open-world scenes. Default true. */
|
|
12
|
+
environment?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Yaw the children every frame so `forward` points at the active camera — frame the declared front
|
|
15
|
+
* with zero `rotationY` tuning. Overrides `turntable` (the two are mutually exclusive: one spins the
|
|
16
|
+
* content, the other holds it facing whoever's looking). Default false.
|
|
17
|
+
*/
|
|
18
|
+
faceCamera?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The staged content's declared forward axis (`GeneratedAsset.forward`, or a scene kind's own
|
|
21
|
+
* convention) — the direction `faceCamera` turns to point at the camera. Default `[0, 0, 1]`, the
|
|
22
|
+
* engine-wide convention (`DEFAULT_FORWARD` in `@jgengine/core/scene/facing`).
|
|
23
|
+
*/
|
|
24
|
+
forward?: readonly [number, number, number];
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
|
|
29
|
+
* seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
|
|
30
|
+
* a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
|
|
31
|
+
* STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
|
|
32
|
+
* and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
|
|
33
|
+
* the camera instead of hand-tuning `rotationY` on the child.
|
|
34
|
+
*
|
|
35
|
+
* @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
|
|
36
|
+
*/
|
|
37
|
+
export declare function StudioStage({ mood, backdrop, turntable, environment, faceCamera, forward, children, }: StudioStageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { DEFAULT_FORWARD, yawToFace } from "@jgengine/core/scene/facing";
|
|
6
|
+
const MOODS = {
|
|
7
|
+
studio: { key: 3.1, keyColor: "#fff4e6", fill: 0.9, fillColor: "#cdd8ff", rim: 2.6, rimColor: "#ffe9c7", ambient: 0.35, ambientColor: "#5b6472", backdrop: "#14161c", floor: "#1b1e26" },
|
|
8
|
+
daylight: { key: 3.4, keyColor: "#fff6ea", fill: 1.2, fillColor: "#d6e6ff", rim: 1.6, rimColor: "#ffffff", ambient: 0.6, ambientColor: "#8fa2bf", backdrop: "#cdd7e4", floor: "#aeb7c4" },
|
|
9
|
+
dusk: { key: 2.6, keyColor: "#ffcf9e", fill: 0.7, fillColor: "#7c8fd8", rim: 3.0, rimColor: "#ff9d6b", ambient: 0.4, ambientColor: "#4a4668", backdrop: "#221a26", floor: "#241d2a" },
|
|
10
|
+
night: { key: 1.6, keyColor: "#bcd0ff", fill: 0.4, fillColor: "#38507e", rim: 2.4, rimColor: "#9fd0ff", ambient: 0.22, ambientColor: "#1e2740", backdrop: "#0a0c12", floor: "#0e1119" },
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
|
|
14
|
+
* seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
|
|
15
|
+
* a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
|
|
16
|
+
* STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
|
|
17
|
+
* and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
|
|
18
|
+
* the camera instead of hand-tuning `rotationY` on the child.
|
|
19
|
+
*
|
|
20
|
+
* @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
|
|
21
|
+
*/
|
|
22
|
+
export function StudioStage({ mood = "studio", backdrop, turntable = 0, environment = true, faceCamera = false, forward = DEFAULT_FORWARD, children, }) {
|
|
23
|
+
const spin = useRef(null);
|
|
24
|
+
const camera = useThree((state) => state.camera);
|
|
25
|
+
const groupWorld = useRef(new THREE.Vector3()).current;
|
|
26
|
+
const cameraWorld = useRef(new THREE.Vector3()).current;
|
|
27
|
+
useFrame((_, delta) => {
|
|
28
|
+
const group = spin.current;
|
|
29
|
+
if (group === null)
|
|
30
|
+
return;
|
|
31
|
+
if (faceCamera) {
|
|
32
|
+
group.getWorldPosition(groupWorld);
|
|
33
|
+
camera.getWorldPosition(cameraWorld);
|
|
34
|
+
group.rotation.y = yawToFace(forward, [groupWorld.x, groupWorld.z], [cameraWorld.x, cameraWorld.z]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (turntable !== 0)
|
|
38
|
+
group.rotation.y += turntable * delta;
|
|
39
|
+
});
|
|
40
|
+
const rig = MOODS[mood];
|
|
41
|
+
return (_jsxs(_Fragment, { children: [environment ? _jsx("color", { attach: "background", args: [backdrop ?? rig.backdrop] }) : null, _jsx("ambientLight", { intensity: rig.ambient, color: rig.ambientColor }), _jsx("directionalLight", { position: [7, 9, 5], intensity: rig.key, color: rig.keyColor, castShadow: true, "shadow-mapSize": [2048, 2048], "shadow-bias": -0.0004, children: _jsx("orthographicCamera", { attach: "shadow-camera", args: [-24, 24, 24, -24, 0.5, 80] }) }), _jsx("directionalLight", { position: [-8, 5, 3], intensity: rig.fill, color: rig.fillColor }), _jsx("directionalLight", { position: [0, 6, -10], intensity: rig.rim, color: rig.rimColor }), environment ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": -0.01, receiveShadow: true, children: [_jsx("planeGeometry", { args: [400, 400] }), _jsx("meshStandardMaterial", { color: rig.floor, roughness: 0.95, metalness: 0 })] })) : null, _jsx("group", { ref: spin, children: children })] }));
|
|
42
|
+
}
|