@jgengine/shell 0.11.0 → 0.13.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 +52 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +27 -1
- package/dist/GameHost.js +83 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +63 -359
- package/dist/GameUiPreview.js +3 -0
- 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 +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +24 -10
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +27 -0
- package/dist/camera/rigMath.js +45 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +17 -3
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- 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/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +39 -0
- package/dist/presentationResolve.js +53 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +31 -180
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +127 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +43 -2
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -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/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -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/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +32 -4
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- 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 +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- 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/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/**
|
|
3
|
+
* The app base URL every root-absolute asset path resolves under. `/` (the
|
|
4
|
+
* default) leaves URLs untouched — standalone game runners serve assets from
|
|
5
|
+
* the site root. The website serves the runner under `/play/`, where a game's
|
|
6
|
+
* `/models/…` or `/materials/…` must load from `/play/models/…`.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
let assetBase = "/";
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
|
|
12
|
+
* installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
|
|
13
|
+
* `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
|
|
14
|
+
* as the URL modifier on THREE loading managers so every model and texture
|
|
15
|
+
* load resolves correctly wherever the app is mounted.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveAssetBaseUrl(url) {
|
|
18
|
+
if (assetBase === "/" || !url.startsWith("/") || url.startsWith("//") || url.startsWith(assetBase)) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
return `${assetBase.replace(/\/+$/, "")}${url}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
|
|
25
|
+
* asset paths load from under it. Call once at app startup, before any game
|
|
26
|
+
* loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
|
|
27
|
+
* default. Also registers {@link resolveAssetBaseUrl} on
|
|
28
|
+
* `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
|
|
29
|
+
* / `useGLTF`, and every other loader on the default manager.
|
|
30
|
+
*/
|
|
31
|
+
export function installAssetBase(base) {
|
|
32
|
+
const normalized = base.endsWith("/") ? base : `${base}/`;
|
|
33
|
+
assetBase = normalized.startsWith("/") && !normalized.startsWith("//") ? normalized : "/";
|
|
34
|
+
THREE.DefaultLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
/** Subtrees flagged with this userData key are excluded from bounds measurement — debug gizmos,
|
|
5
|
+
* selection quads, effect billboards that should not inflate the hitbox. Sprites and invisible
|
|
6
|
+
* subtrees are always excluded. */
|
|
7
|
+
export declare const MEASURE_EXCLUDE_KEY = "jgMeasureExclude";
|
|
8
|
+
/** Entity-local AABB of the meshes under a measured root, plus how many meshes contributed — the
|
|
9
|
+
* count lets callers detect async subtree fill-in without diffing bounds. */
|
|
10
|
+
export interface MeasuredLocalBounds {
|
|
11
|
+
min: [number, number, number];
|
|
12
|
+
max: [number, number, number];
|
|
13
|
+
meshCount: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Measure the meshes under `root` in `root`'s own frame (root's transform is the measuring frame,
|
|
17
|
+
* children compose their local matrices below it). Returns `null` when nothing measurable is
|
|
18
|
+
* mounted. Skinned meshes measure at bind pose — the standard engine approximation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function measureLocalBounds(root: THREE.Object3D): MeasuredLocalBounds | null;
|
|
21
|
+
/** Forward a measured result into the context (entity kind or object catalog id), deduping repeat
|
|
22
|
+
* reports of the same shape so remounts and multi-instance kinds don't churn collider resolution. */
|
|
23
|
+
export declare function reportMeasuredBounds(ctx: GameContext, target: "entity" | "object", key: string, bounds: MeasuredLocalBounds): void;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps custom-rendered actor content (`renderEntity` / `renderObject`) and reports its measured
|
|
26
|
+
* entity-local bounds into collider resolution, so the hitbox wraps what is actually on screen
|
|
27
|
+
* instead of the fixed-size fallback box. A successful measure re-reports only when the subtree
|
|
28
|
+
* gained or lost meshes (async fill-in), not when idle animation nudges the same meshes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function MeasuredBoundsGroup({ target, measureKey, children, }: {
|
|
31
|
+
target: "entity" | "object";
|
|
32
|
+
measureKey: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
/** Subtrees flagged with this userData key are excluded from bounds measurement — debug gizmos,
|
|
7
|
+
* selection quads, effect billboards that should not inflate the hitbox. Sprites and invisible
|
|
8
|
+
* subtrees are always excluded. */
|
|
9
|
+
export const MEASURE_EXCLUDE_KEY = "jgMeasureExclude";
|
|
10
|
+
const IDENTITY = new THREE.Matrix4();
|
|
11
|
+
const tmpBox = new THREE.Box3();
|
|
12
|
+
/**
|
|
13
|
+
* Measure the meshes under `root` in `root`'s own frame (root's transform is the measuring frame,
|
|
14
|
+
* children compose their local matrices below it). Returns `null` when nothing measurable is
|
|
15
|
+
* mounted. Skinned meshes measure at bind pose — the standard engine approximation.
|
|
16
|
+
*/
|
|
17
|
+
export function measureLocalBounds(root) {
|
|
18
|
+
const box = new THREE.Box3();
|
|
19
|
+
box.makeEmpty();
|
|
20
|
+
const meshCount = collectBounds(root, null, box);
|
|
21
|
+
if (meshCount === 0 || box.isEmpty())
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
min: [box.min.x, box.min.y, box.min.z],
|
|
25
|
+
max: [box.max.x, box.max.y, box.max.z],
|
|
26
|
+
meshCount,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function collectBounds(object, parentMatrix, into) {
|
|
30
|
+
if (parentMatrix !== null) {
|
|
31
|
+
if (!object.visible)
|
|
32
|
+
return 0;
|
|
33
|
+
if (object.isSprite === true)
|
|
34
|
+
return 0;
|
|
35
|
+
if (object.userData[MEASURE_EXCLUDE_KEY] === true)
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
let matrix = null;
|
|
39
|
+
if (parentMatrix !== null) {
|
|
40
|
+
if (object.matrixAutoUpdate)
|
|
41
|
+
object.updateMatrix();
|
|
42
|
+
matrix = new THREE.Matrix4().multiplyMatrices(parentMatrix, object.matrix);
|
|
43
|
+
}
|
|
44
|
+
let count = 0;
|
|
45
|
+
const mesh = object;
|
|
46
|
+
if (mesh.isMesh === true && mesh.geometry !== undefined) {
|
|
47
|
+
const geometry = mesh.geometry;
|
|
48
|
+
if (geometry.boundingBox === null)
|
|
49
|
+
geometry.computeBoundingBox();
|
|
50
|
+
const bounds = geometry.boundingBox;
|
|
51
|
+
if (bounds !== null && !bounds.isEmpty()) {
|
|
52
|
+
tmpBox.copy(bounds);
|
|
53
|
+
if (matrix !== null)
|
|
54
|
+
tmpBox.applyMatrix4(matrix);
|
|
55
|
+
into.union(tmpBox);
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const childMatrix = matrix ?? IDENTITY;
|
|
60
|
+
for (const child of object.children)
|
|
61
|
+
count += collectBounds(child, childMatrix, into);
|
|
62
|
+
return count;
|
|
63
|
+
}
|
|
64
|
+
const reportedByContext = new WeakMap();
|
|
65
|
+
const EPSILON = 0.01;
|
|
66
|
+
function boundsClose(a, b) {
|
|
67
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
68
|
+
if (Math.abs(a.min[axis] - b.min[axis]) > EPSILON)
|
|
69
|
+
return false;
|
|
70
|
+
if (Math.abs(a.max[axis] - b.max[axis]) > EPSILON)
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Forward a measured result into the context (entity kind or object catalog id), deduping repeat
|
|
76
|
+
* reports of the same shape so remounts and multi-instance kinds don't churn collider resolution. */
|
|
77
|
+
export function reportMeasuredBounds(ctx, target, key, bounds) {
|
|
78
|
+
let map = reportedByContext.get(ctx);
|
|
79
|
+
if (map === undefined) {
|
|
80
|
+
map = new Map();
|
|
81
|
+
reportedByContext.set(ctx, map);
|
|
82
|
+
}
|
|
83
|
+
const registryKey = `${target}:${key}`;
|
|
84
|
+
const prev = map.get(registryKey);
|
|
85
|
+
if (prev !== undefined && prev.meshCount === bounds.meshCount && boundsClose(prev, bounds))
|
|
86
|
+
return;
|
|
87
|
+
const accepted = target === "entity"
|
|
88
|
+
? ctx.scene.entity.reportBounds(key, bounds)
|
|
89
|
+
: ctx.scene.object.reportBounds(key, bounds);
|
|
90
|
+
if (accepted)
|
|
91
|
+
map.set(registryKey, bounds);
|
|
92
|
+
}
|
|
93
|
+
/** Frames-since-mount at which a marker re-measures — bounded retries catch content that fills in
|
|
94
|
+
* asynchronously after first mount without paying a traversal every frame forever. */
|
|
95
|
+
const MEASURE_FRAMES = [1, 15, 30, 60];
|
|
96
|
+
/**
|
|
97
|
+
* Wraps custom-rendered actor content (`renderEntity` / `renderObject`) and reports its measured
|
|
98
|
+
* entity-local bounds into collider resolution, so the hitbox wraps what is actually on screen
|
|
99
|
+
* instead of the fixed-size fallback box. A successful measure re-reports only when the subtree
|
|
100
|
+
* gained or lost meshes (async fill-in), not when idle animation nudges the same meshes.
|
|
101
|
+
*/
|
|
102
|
+
export function MeasuredBoundsGroup({ target, measureKey, children, }) {
|
|
103
|
+
const ctx = useGameContext();
|
|
104
|
+
const groupRef = useRef(null);
|
|
105
|
+
const frameRef = useRef(0);
|
|
106
|
+
const attemptRef = useRef(0);
|
|
107
|
+
const lastMeshCountRef = useRef(null);
|
|
108
|
+
useFrame(() => {
|
|
109
|
+
if (attemptRef.current >= MEASURE_FRAMES.length)
|
|
110
|
+
return;
|
|
111
|
+
frameRef.current += 1;
|
|
112
|
+
if (frameRef.current < MEASURE_FRAMES[attemptRef.current])
|
|
113
|
+
return;
|
|
114
|
+
attemptRef.current += 1;
|
|
115
|
+
const group = groupRef.current;
|
|
116
|
+
if (group === null)
|
|
117
|
+
return;
|
|
118
|
+
const measured = measureLocalBounds(group);
|
|
119
|
+
if (measured === null)
|
|
120
|
+
return;
|
|
121
|
+
if (lastMeshCountRef.current === measured.meshCount)
|
|
122
|
+
return;
|
|
123
|
+
lastMeshCountRef.current = measured.meshCount;
|
|
124
|
+
reportMeasuredBounds(ctx, target, measureKey, measured);
|
|
125
|
+
});
|
|
126
|
+
return _jsx("group", { ref: groupRef, children: children });
|
|
127
|
+
}
|
package/dist/render/modelLoad.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
4
4
|
import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
5
|
+
import { resolveAssetBaseUrl } from "./assetBase.js";
|
|
5
6
|
/**
|
|
6
7
|
* Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
|
|
7
8
|
* The shared default manager is process-wide; under repeated dev-server navigations its
|
|
@@ -10,6 +11,7 @@ import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
|
|
|
10
11
|
* @internal
|
|
11
12
|
*/
|
|
12
13
|
const modelLoadingManager = new THREE.LoadingManager();
|
|
14
|
+
modelLoadingManager.setURLModifier(resolveAssetBaseUrl);
|
|
13
15
|
/** How many leading bytes to read when probing a failed model URL for its signature. @internal */
|
|
14
16
|
const PROBE_BYTE_LIMIT = 64;
|
|
15
17
|
/**
|
|
@@ -52,7 +54,8 @@ export class DiagnosticGLTFLoader extends GLTFLoader {
|
|
|
52
54
|
super.load(url, onLoad, onProgress, (event) => {
|
|
53
55
|
if (onError === undefined)
|
|
54
56
|
return;
|
|
55
|
-
|
|
57
|
+
// Probe the URL the loader actually fetched (base-resolved), not the raw input.
|
|
58
|
+
void probeModelUrl(this.manager.resolveURL(url))
|
|
56
59
|
.then((diagnosis) => {
|
|
57
60
|
if (diagnosis.ok) {
|
|
58
61
|
// The bytes look fine on re-fetch — surface the original parse error unchanged.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
2
2
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { GameContextModels } from "@jgengine/core/runtime/gameContext";
|
|
3
4
|
export interface ModelResolveContext {
|
|
4
5
|
seam: "entityModels" | "objectModels" | "scatterModels";
|
|
5
6
|
key: string;
|
|
@@ -40,6 +41,20 @@ export declare function pickModel(assets: AssetCatalog, pick: ModelPick): ModelC
|
|
|
40
41
|
* @internal
|
|
41
42
|
*/
|
|
42
43
|
export declare function resolveModelPlan(assets: AssetCatalog, plan: Record<string, ModelPick>): Record<string, ModelConfig>;
|
|
44
|
+
/**
|
|
45
|
+
* The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
|
|
46
|
+
* object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
|
|
47
|
+
* ids (a missing catalog id keeps the default collider instead of throwing — the render path already
|
|
48
|
+
* reports it), passes inline configs through. `undefined` when the playable maps no models at all.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function contextModels(playable: {
|
|
52
|
+
game: {
|
|
53
|
+
assets: AssetCatalog;
|
|
54
|
+
};
|
|
55
|
+
entityModels?: Record<string, string | ModelConfig>;
|
|
56
|
+
objectModels?: Record<string, string | ModelConfig>;
|
|
57
|
+
}): GameContextModels | undefined;
|
|
43
58
|
/**
|
|
44
59
|
* Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
|
|
45
60
|
* `undefined` when either half is missing so the caller can fall back to its own default renderer.
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ModelConfig` a resolved catalog ref renders as. Stamps `animation: "auto"` so any rigged
|
|
3
|
+
* catalog asset animates by default — the shell derives idle/walk/run states and one-shots from
|
|
4
|
+
* the loaded GLB's clip names; clipless models are unaffected. An explicit `style.animation`
|
|
5
|
+
* (merged after resolve) or `"none"` overrides.
|
|
6
|
+
*/
|
|
7
|
+
function catalogModelConfig(ref) {
|
|
8
|
+
return {
|
|
9
|
+
url: ref.url,
|
|
10
|
+
...(ref.dims === undefined ? {} : { dims: ref.dims }),
|
|
11
|
+
...(ref.collisionMesh === undefined ? {} : { collisionMesh: ref.collisionMesh }),
|
|
12
|
+
animation: "auto",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Resolve a string asset id or a direct ModelConfig. Missing/misspelled catalog
|
|
3
17
|
* ids throw — silent generic-primitive fallback only happens when the mapping
|
|
@@ -14,7 +28,7 @@ export function resolveModel(value, assets, context) {
|
|
|
14
28
|
const where = context === undefined ? `model asset "${value}"` : `${context.seam}["${context.key}"] → "${value}"`;
|
|
15
29
|
throw new Error(`[jgengine] missing ${where} — not in the asset catalog. Fix the id, register it, or omit the mapping to keep the deliberate primitive/sprite fallback.`);
|
|
16
30
|
}
|
|
17
|
-
return ref
|
|
31
|
+
return catalogModelConfig(ref);
|
|
18
32
|
}
|
|
19
33
|
/** Soft lookup used when an object catalog id may double as a model asset id.
|
|
20
34
|
* @internal
|
|
@@ -23,7 +37,7 @@ export function tryResolveCatalogModel(id, assets) {
|
|
|
23
37
|
const ref = assets.resolve(id);
|
|
24
38
|
if (ref === null)
|
|
25
39
|
return undefined;
|
|
26
|
-
return ref
|
|
40
|
+
return catalogModelConfig(ref);
|
|
27
41
|
}
|
|
28
42
|
/**
|
|
29
43
|
* Resolve an entity model plus bone-attachment / kit-of-parts models through the catalog
|
|
@@ -82,6 +96,33 @@ export function resolveModelPlan(assets, plan) {
|
|
|
82
96
|
}
|
|
83
97
|
return out;
|
|
84
98
|
}
|
|
99
|
+
function softModelLookup(map, assets) {
|
|
100
|
+
if (map === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
return (key) => {
|
|
103
|
+
const value = map[key];
|
|
104
|
+
if (value === undefined || typeof value !== "string")
|
|
105
|
+
return value;
|
|
106
|
+
return tryResolveCatalogModel(value, assets);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The `createGameContext` `models` lookup derived from a playable's render maps: what each entity kind /
|
|
111
|
+
* object catalog id actually renders as, so colliders auto-fit the rendered mesh. Soft-resolves string
|
|
112
|
+
* ids (a missing catalog id keeps the default collider instead of throwing — the render path already
|
|
113
|
+
* reports it), passes inline configs through. `undefined` when the playable maps no models at all.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export function contextModels(playable) {
|
|
117
|
+
const entity = softModelLookup(playable.entityModels, playable.game.assets);
|
|
118
|
+
const object = softModelLookup(playable.objectModels, playable.game.assets);
|
|
119
|
+
if (entity === undefined && object === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
return {
|
|
122
|
+
...(entity === undefined ? {} : { entity }),
|
|
123
|
+
...(object === undefined ? {} : { object }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
85
126
|
/**
|
|
86
127
|
* Builds a `(key) => ModelConfig | null` override from a `seam`'s id/config map plus its catalog —
|
|
87
128
|
* `undefined` when either half is missing so the caller can fall back to its own default renderer.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelAnimationConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/**
|
|
4
|
+
* The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs,
|
|
5
|
+
* for games that render a cloned scene themselves (custom materials, procedural composition).
|
|
6
|
+
* Handles `"auto"` derivation from the GLB's clip names, speed-driven idle/walk/run crossfades
|
|
7
|
+
* read from the entity's live position when `instanceId` is set, one-shots fired from
|
|
8
|
+
* `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { resolveAnimationConfig } from "@jgengine/core/game/clipRoles";
|
|
5
|
+
import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
|
|
6
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
+
/**
|
|
8
|
+
* The engine's model animation driver as a standalone hook — the same mixer `EntityModel` runs,
|
|
9
|
+
* for games that render a cloned scene themselves (custom materials, procedural composition).
|
|
10
|
+
* Handles `"auto"` derivation from the GLB's clip names, speed-driven idle/walk/run crossfades
|
|
11
|
+
* read from the entity's live position when `instanceId` is set, one-shots fired from
|
|
12
|
+
* `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
|
|
13
|
+
*/
|
|
14
|
+
export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
15
|
+
const ctx = useGameContext();
|
|
16
|
+
// "auto" (stamped by catalog resolution, or set inline) derives states/one-shots from the
|
|
17
|
+
// loaded GLB's actual clip names; "none" and absent render the bind pose.
|
|
18
|
+
const animation = useMemo(() => resolveAnimationConfig(animationInput, clips.map((clip) => clip.name)), [animationInput, clips]);
|
|
19
|
+
const mixerRef = useRef(null);
|
|
20
|
+
const animationPausedRef = useRef(false);
|
|
21
|
+
const stateActionsRef = useRef(null);
|
|
22
|
+
const states = animation?.states;
|
|
23
|
+
const oneShots = animation?.oneShots;
|
|
24
|
+
const oneShotPlayRef = useRef(null);
|
|
25
|
+
const activeOneShotRef = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (animation === undefined || clips.length === 0) {
|
|
28
|
+
mixerRef.current = null;
|
|
29
|
+
stateActionsRef.current = null;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const mixer = new THREE.AnimationMixer(scene);
|
|
33
|
+
if (states !== undefined) {
|
|
34
|
+
const clipFor = (name) => THREE.AnimationClip.findByName(clips, name) ?? clips[0];
|
|
35
|
+
const actions = {
|
|
36
|
+
idle: mixer.clipAction(clipFor(states.idle)),
|
|
37
|
+
walk: mixer.clipAction(clipFor(states.walk)),
|
|
38
|
+
...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
|
|
39
|
+
};
|
|
40
|
+
for (const action of Object.values(actions)) {
|
|
41
|
+
action.setLoop(THREE.LoopRepeat, Infinity);
|
|
42
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
43
|
+
action.enabled = true;
|
|
44
|
+
}
|
|
45
|
+
actions.idle.play();
|
|
46
|
+
mixer.update(0);
|
|
47
|
+
mixerRef.current = mixer;
|
|
48
|
+
stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
|
|
49
|
+
animationPausedRef.current = false;
|
|
50
|
+
let onOneShotFinished = null;
|
|
51
|
+
if (oneShots !== undefined) {
|
|
52
|
+
const clipNames = new Set();
|
|
53
|
+
for (const spec of Object.values(oneShots)) {
|
|
54
|
+
if (typeof spec === "string")
|
|
55
|
+
clipNames.add(spec);
|
|
56
|
+
else
|
|
57
|
+
for (const name of spec)
|
|
58
|
+
clipNames.add(name);
|
|
59
|
+
}
|
|
60
|
+
const oneShotActions = new Map();
|
|
61
|
+
for (const name of clipNames) {
|
|
62
|
+
const found = THREE.AnimationClip.findByName(clips, name);
|
|
63
|
+
if (found === null)
|
|
64
|
+
continue;
|
|
65
|
+
const oneShotAction = mixer.clipAction(found);
|
|
66
|
+
oneShotAction.setLoop(THREE.LoopOnce, 1);
|
|
67
|
+
oneShotAction.enabled = true;
|
|
68
|
+
oneShotActions.set(name, oneShotAction);
|
|
69
|
+
}
|
|
70
|
+
onOneShotFinished = ({ action }) => {
|
|
71
|
+
const active = activeOneShotRef.current;
|
|
72
|
+
if (active === null || action !== active.action || active.isDeath)
|
|
73
|
+
return;
|
|
74
|
+
const machine = stateActionsRef.current;
|
|
75
|
+
const back = machine?.actions[machine.active];
|
|
76
|
+
action.fadeOut(0.15);
|
|
77
|
+
if (back !== undefined)
|
|
78
|
+
back.reset().fadeIn(0.15).play();
|
|
79
|
+
activeOneShotRef.current = null;
|
|
80
|
+
};
|
|
81
|
+
mixer.addEventListener("finished", onOneShotFinished);
|
|
82
|
+
oneShotPlayRef.current = (event) => {
|
|
83
|
+
const active = activeOneShotRef.current;
|
|
84
|
+
if (active !== null && active.isDeath)
|
|
85
|
+
return;
|
|
86
|
+
const clipName = resolveOneShotClip(oneShots, event, Math.random());
|
|
87
|
+
if (clipName === null)
|
|
88
|
+
return;
|
|
89
|
+
const oneShotAction = oneShotActions.get(clipName);
|
|
90
|
+
if (oneShotAction === undefined)
|
|
91
|
+
return;
|
|
92
|
+
const machine = stateActionsRef.current;
|
|
93
|
+
machine?.actions[machine.active]?.fadeOut(0.1);
|
|
94
|
+
if (active !== null && active.action !== oneShotAction)
|
|
95
|
+
active.action.stop();
|
|
96
|
+
oneShotAction.clampWhenFinished = event === "death";
|
|
97
|
+
oneShotAction.reset().fadeIn(0.1).play();
|
|
98
|
+
activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return () => {
|
|
102
|
+
if (onOneShotFinished !== null)
|
|
103
|
+
mixer.removeEventListener("finished", onOneShotFinished);
|
|
104
|
+
oneShotPlayRef.current = null;
|
|
105
|
+
activeOneShotRef.current = null;
|
|
106
|
+
mixer.stopAllAction();
|
|
107
|
+
mixerRef.current = null;
|
|
108
|
+
stateActionsRef.current = null;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(clips, animation.clip) : undefined) ??
|
|
112
|
+
clips[0];
|
|
113
|
+
const action = mixer.clipAction(clip);
|
|
114
|
+
action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
|
|
115
|
+
action.clampWhenFinished = animation.loop === false;
|
|
116
|
+
action.timeScale = animation.timeScale ?? 1;
|
|
117
|
+
action.enabled = true;
|
|
118
|
+
action.paused = animation.paused === true;
|
|
119
|
+
action.play();
|
|
120
|
+
if (animation.time !== undefined)
|
|
121
|
+
action.time = animation.time;
|
|
122
|
+
mixer.update(0);
|
|
123
|
+
mixerRef.current = mixer;
|
|
124
|
+
animationPausedRef.current = animation.paused === true;
|
|
125
|
+
return () => {
|
|
126
|
+
mixer.stopAllAction();
|
|
127
|
+
mixerRef.current = null;
|
|
128
|
+
};
|
|
129
|
+
}, [
|
|
130
|
+
scene,
|
|
131
|
+
clips,
|
|
132
|
+
animation?.clip,
|
|
133
|
+
animation?.loop,
|
|
134
|
+
animation?.timeScale,
|
|
135
|
+
animation?.paused,
|
|
136
|
+
animation?.time,
|
|
137
|
+
states,
|
|
138
|
+
oneShots,
|
|
139
|
+
]);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (instanceId === undefined || oneShots === undefined)
|
|
142
|
+
return;
|
|
143
|
+
const fire = (event) => oneShotPlayRef.current?.(event);
|
|
144
|
+
const offAnimation = ctx.game.events.on("entity.animation", (event) => {
|
|
145
|
+
if (event.instanceId === instanceId)
|
|
146
|
+
fire(event.event);
|
|
147
|
+
});
|
|
148
|
+
const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
|
|
149
|
+
if (event.instanceId === instanceId)
|
|
150
|
+
fire("hit");
|
|
151
|
+
});
|
|
152
|
+
const offDied = ctx.game.events.on("entity.died", (event) => {
|
|
153
|
+
if (event.instanceId === instanceId)
|
|
154
|
+
fire("death");
|
|
155
|
+
});
|
|
156
|
+
return () => {
|
|
157
|
+
offAnimation();
|
|
158
|
+
offHit();
|
|
159
|
+
offDied();
|
|
160
|
+
};
|
|
161
|
+
}, [ctx, instanceId, oneShots]);
|
|
162
|
+
useFrame((_state, delta) => {
|
|
163
|
+
const stateMachine = stateActionsRef.current;
|
|
164
|
+
if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
|
|
165
|
+
const entity = ctx.scene.entity.get(instanceId);
|
|
166
|
+
if (entity !== null) {
|
|
167
|
+
const [x, , z] = entity.position;
|
|
168
|
+
if (stateMachine.lastPos !== null) {
|
|
169
|
+
const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
|
|
170
|
+
stateMachine.smoothedSpeed +=
|
|
171
|
+
(instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
|
|
172
|
+
}
|
|
173
|
+
stateMachine.lastPos = [x, entity.position[1], z];
|
|
174
|
+
const walkSpeed = states.walkSpeed ?? 0.5;
|
|
175
|
+
const runSpeed = states.runSpeed ?? 6;
|
|
176
|
+
const next = stateMachine.smoothedSpeed < walkSpeed
|
|
177
|
+
? "idle"
|
|
178
|
+
: stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
|
|
179
|
+
? "run"
|
|
180
|
+
: "walk";
|
|
181
|
+
if (next !== stateMachine.active) {
|
|
182
|
+
if (activeOneShotRef.current === null) {
|
|
183
|
+
const fade = states.fadeSec ?? 0.2;
|
|
184
|
+
const from = stateMachine.actions[stateMachine.active];
|
|
185
|
+
const to = stateMachine.actions[next];
|
|
186
|
+
if (from !== undefined && to !== undefined) {
|
|
187
|
+
to.reset().fadeIn(fade).play();
|
|
188
|
+
from.fadeOut(fade);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
stateMachine.active = next;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (mixerRef.current !== null && !animationPausedRef.current)
|
|
196
|
+
mixerRef.current.update(delta);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as THREE from "three";
|
|
|
5
5
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
6
|
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
|
|
7
7
|
import { chunkScatterInstances } from "@jgengine/core/world/scatterRegion";
|
|
8
|
+
import { beginFallbackSeam, reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams";
|
|
8
9
|
import { buildScatterProxy } from "./scatterProxies.js";
|
|
9
10
|
import { buildScatterModelSources, disposeScatterModelSources } from "./scatterModels.js";
|
|
10
11
|
/** Chunk edge (m): dense fields split into this-sized buckets, each frustum-culled independently. */
|
|
@@ -90,6 +91,9 @@ function ScatterModelSpecies({ model, batches }) {
|
|
|
90
91
|
export function InstancedScatter({ instances, chunkSize = DEFAULT_CHUNK_SIZE, maxInstances = DEFAULT_MAX_INSTANCES, resolveItem, }) {
|
|
91
92
|
const batches = useMemo(() => batchInstances(instances.length > maxInstances ? instances.slice(0, maxInstances) : instances, chunkSize), [instances, chunkSize, maxInstances]);
|
|
92
93
|
const { proxyBatches, modelSpecies } = useMemo(() => {
|
|
94
|
+
// Dev diagnostic (no-op unless armed): re-zero the scatter seam before re-tallying which species
|
|
95
|
+
// fall to a stylized proxy (no resolveItem mapping / a null result). Reported once per species.
|
|
96
|
+
beginFallbackSeam("scatter");
|
|
93
97
|
const proxy = [];
|
|
94
98
|
const bySpecies = new Map();
|
|
95
99
|
const knownProxy = new Set();
|
|
@@ -106,6 +110,7 @@ export function InstancedScatter({ instances, chunkSize = DEFAULT_CHUNK_SIZE, ma
|
|
|
106
110
|
const resolved = resolveItem?.(batch.item) ?? null;
|
|
107
111
|
if (resolved === null) {
|
|
108
112
|
knownProxy.add(batch.item);
|
|
113
|
+
reportFallbackSeam("scatter", "omittedMapping");
|
|
109
114
|
proxy.push(batch);
|
|
110
115
|
continue;
|
|
111
116
|
}
|
|
@@ -6,6 +6,9 @@ const OAK = "#4a7a37";
|
|
|
6
6
|
const BUSH = "#4d7d3a";
|
|
7
7
|
const ROCK = "#82858c";
|
|
8
8
|
const GRASS = "#5f9a3c";
|
|
9
|
+
const PALM_BARK = "#8a7355";
|
|
10
|
+
const PALM_FROND = "#4c8b3d";
|
|
11
|
+
const CYPRESS = "#31502f";
|
|
9
12
|
/** Concatenates part geometries into one vertex-colored buffer (position + normal + color). */
|
|
10
13
|
function mergeParts(parts) {
|
|
11
14
|
const geos = parts.map((part) => (part.geometry.index === null ? part.geometry : part.geometry.toNonIndexed()));
|
|
@@ -80,6 +83,27 @@ const BUILDERS = {
|
|
|
80
83
|
{ geometry: cone(0.08, 0.55, 0).translate(0.12, 0, 0.05), color: GRASS },
|
|
81
84
|
{ geometry: cone(0.08, 0.6, 0).translate(-0.1, 0, -0.06), color: GRASS },
|
|
82
85
|
]),
|
|
86
|
+
palm: () => {
|
|
87
|
+
// Tall bare trunk with a radial crown of drooping fronds — each frond a flattened cone laid
|
|
88
|
+
// near-horizontal, tips sagging below the crown.
|
|
89
|
+
const parts = [{ geometry: trunk(2.6, 0.09), color: PALM_BARK }];
|
|
90
|
+
for (let i = 0; i < 6; i += 1) {
|
|
91
|
+
const yaw = (i / 6) * Math.PI * 2;
|
|
92
|
+
const frond = new THREE.ConeGeometry(0.16, 1.35, 4);
|
|
93
|
+
frond.scale(1, 1, 0.35);
|
|
94
|
+
frond.rotateX(-Math.PI / 2 - 0.5);
|
|
95
|
+
frond.rotateY(yaw);
|
|
96
|
+
frond.translate(Math.sin(yaw) * 0.32, 2.62, Math.cos(yaw) * 0.32);
|
|
97
|
+
parts.push({ geometry: frond, color: PALM_FROND });
|
|
98
|
+
}
|
|
99
|
+
parts.push({ geometry: sphere(0.14, 2.56), color: PALM_BARK });
|
|
100
|
+
return mergeParts(parts);
|
|
101
|
+
},
|
|
102
|
+
cypress: () => mergeParts([
|
|
103
|
+
{ geometry: trunk(0.5, 0.07), color: BARK },
|
|
104
|
+
{ geometry: cone(0.34, 2.5, 0.35, 6), color: CYPRESS },
|
|
105
|
+
{ geometry: cone(0.2, 0.7, 2.75, 5), color: CYPRESS },
|
|
106
|
+
]),
|
|
83
107
|
};
|
|
84
108
|
function defaultProxy() {
|
|
85
109
|
return mergeParts([{ geometry: cone(0.35, 1.2, 0), color: GRASS }]);
|
|
@@ -68,6 +68,8 @@ export interface AuthoredSceneProps {
|
|
|
68
68
|
placeObjects?: boolean | {
|
|
69
69
|
verticalOffset?: number;
|
|
70
70
|
};
|
|
71
|
+
/** Terrain surface color sampler forwarded to studio renderers (see `SceneKindRenderContext.groundColorAt`). */
|
|
72
|
+
groundColorAt?: (x: number, z: number) => string;
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
75
|
* Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one
|
|
@@ -79,4 +81,4 @@ export interface AuthoredSceneProps {
|
|
|
79
81
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
80
82
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
81
83
|
*/
|
|
82
|
-
export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
84
|
+
export declare function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live, placeObjects, groundColorAt, }: AuthoredSceneProps): import("react").JSX.Element;
|
|
@@ -163,11 +163,16 @@ export function AuthoredObjects({ document, field, verticalOffset = 0, onExistin
|
|
|
163
163
|
* place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
|
|
164
164
|
* real catalog GLBs; unmapped items keep the stylized proxy.
|
|
165
165
|
*/
|
|
166
|
-
export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }) {
|
|
166
|
+
export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, groundColorAt, }) {
|
|
167
167
|
const liveDocument = useLiveEditorDocument(document, live);
|
|
168
168
|
const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]);
|
|
169
169
|
const resolveItem = useMemo(() => createModelMapResolver(scatterModels, assets, "scatterModels"), [scatterModels, assets]);
|
|
170
170
|
const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
|
|
171
171
|
const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
|
|
172
|
-
return (_jsxs(_Fragment, { children: [_jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
|
|
172
|
+
return (_jsxs(_Fragment, { children: [_jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: {
|
|
173
|
+
document: liveDocument,
|
|
174
|
+
field,
|
|
175
|
+
...(assets === undefined ? {} : { assets }),
|
|
176
|
+
...(groundColorAt === undefined ? {} : { groundColorAt }),
|
|
177
|
+
} }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset })) : null] }));
|
|
173
178
|
}
|