@umicat/three-sdk 0.16.1 → 0.17.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.
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
export interface EditorDesignPlayer3DOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Which scene to render — a filename stem under `scenes3d/`, e.g. `"main"`
|
|
5
|
+
* for `scenes3d/main.json`. Fetched by this exact filename convention,
|
|
6
|
+
* NEVER via `manifest.scenes` — that array is scaffold data the template
|
|
7
|
+
* ships with one entry and real games never keep in sync (confirmed
|
|
8
|
+
* against a real, shipped game: its `manifest.scenes` still points at a
|
|
9
|
+
* `main.json` that does not exist, while the game's own boot code fetches
|
|
10
|
+
* `scenes3d/${level.id}.json` directly). Omit to leave the view idle — the
|
|
11
|
+
* host hasn't chosen a scene yet.
|
|
12
|
+
*/
|
|
13
|
+
sceneId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The 3D counterpart of `@umicat/phaser-sdk`'s `EditorDesignScene` (ADR-021)
|
|
17
|
+
* — renders a scene's AUTHORED data with no game code and no save, so the
|
|
18
|
+
* platform's Edit tab can show a 3D game's layout the same way it already
|
|
19
|
+
* does for 2D. Read-only: no drag, no gizmo, no apply-edit — the one
|
|
20
|
+
* interaction is click-to-pick a world coordinate, posted to the host so a
|
|
21
|
+
* person can point at a real spot instead of guessing one blind when telling
|
|
22
|
+
* the AI where to place something.
|
|
23
|
+
*
|
|
24
|
+
* Takes over `renderer`'s animation loop entirely for the rest of the page's
|
|
25
|
+
* life — there is no scene-swap API and no teardown, matching how the
|
|
26
|
+
* template's own `start()` never tears down either. The caller's own game
|
|
27
|
+
* boot must never run alongside this: `main.ts`'s `?umicatEdit=1` branch
|
|
28
|
+
* calls this and returns immediately, skipping `RAPIER.init()` and every
|
|
29
|
+
* other real-game step. `designMode: true` (passed straight through to
|
|
30
|
+
* `loadScene3D`) already skips physics and animation playback on its own, so
|
|
31
|
+
* this needs no Rapier import at all — the design view boots faster than the
|
|
32
|
+
* real game.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runEditorDesignPlayer3D(renderer: THREE.WebGLRenderer, opts?: EditorDesignPlayer3DOptions): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
3
|
+
import { loadScene3D } from '../SceneLoader3D.js';
|
|
4
|
+
/**
|
|
5
|
+
* The 3D counterpart of `@umicat/phaser-sdk`'s `EditorDesignScene` (ADR-021)
|
|
6
|
+
* — renders a scene's AUTHORED data with no game code and no save, so the
|
|
7
|
+
* platform's Edit tab can show a 3D game's layout the same way it already
|
|
8
|
+
* does for 2D. Read-only: no drag, no gizmo, no apply-edit — the one
|
|
9
|
+
* interaction is click-to-pick a world coordinate, posted to the host so a
|
|
10
|
+
* person can point at a real spot instead of guessing one blind when telling
|
|
11
|
+
* the AI where to place something.
|
|
12
|
+
*
|
|
13
|
+
* Takes over `renderer`'s animation loop entirely for the rest of the page's
|
|
14
|
+
* life — there is no scene-swap API and no teardown, matching how the
|
|
15
|
+
* template's own `start()` never tears down either. The caller's own game
|
|
16
|
+
* boot must never run alongside this: `main.ts`'s `?umicatEdit=1` branch
|
|
17
|
+
* calls this and returns immediately, skipping `RAPIER.init()` and every
|
|
18
|
+
* other real-game step. `designMode: true` (passed straight through to
|
|
19
|
+
* `loadScene3D`) already skips physics and animation playback on its own, so
|
|
20
|
+
* this needs no Rapier import at all — the design view boots faster than the
|
|
21
|
+
* real game.
|
|
22
|
+
*/
|
|
23
|
+
export async function runEditorDesignPlayer3D(renderer, opts = {}) {
|
|
24
|
+
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.05, 2000);
|
|
25
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
26
|
+
controls.enableDamping = true;
|
|
27
|
+
// A single persistent marker, repositioned per click.
|
|
28
|
+
const marker = new THREE.Mesh(new THREE.SphereGeometry(0.12, 16, 12), new THREE.MeshBasicMaterial({ color: '#ff3b6b', depthTest: false }));
|
|
29
|
+
marker.visible = false;
|
|
30
|
+
marker.renderOrder = 999;
|
|
31
|
+
let scene = null;
|
|
32
|
+
const entityByObject = new Map();
|
|
33
|
+
const resize = () => {
|
|
34
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
35
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
36
|
+
camera.updateProjectionMatrix();
|
|
37
|
+
};
|
|
38
|
+
resize();
|
|
39
|
+
window.addEventListener('resize', resize);
|
|
40
|
+
// Orbit-vs-click: a real drag moves the pointer more than a few px.
|
|
41
|
+
let downX = 0;
|
|
42
|
+
let downY = 0;
|
|
43
|
+
let moved = false;
|
|
44
|
+
const onPointerDown = (e) => {
|
|
45
|
+
downX = e.clientX;
|
|
46
|
+
downY = e.clientY;
|
|
47
|
+
moved = false;
|
|
48
|
+
};
|
|
49
|
+
const onPointerMove = (e) => {
|
|
50
|
+
if (Math.abs(e.clientX - downX) > 4 || Math.abs(e.clientY - downY) > 4)
|
|
51
|
+
moved = true;
|
|
52
|
+
};
|
|
53
|
+
const onPointerUp = (e) => {
|
|
54
|
+
if (moved || !scene)
|
|
55
|
+
return;
|
|
56
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
57
|
+
const mouse = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
|
|
58
|
+
const raycaster = new THREE.Raycaster();
|
|
59
|
+
raycaster.setFromCamera(mouse, camera);
|
|
60
|
+
// Only cast against VISIBLE entity roots. three.js's Mesh.raycast does
|
|
61
|
+
// NOT check `.visible` on its own (confirmed against the pinned three
|
|
62
|
+
// source — no such check in Raycaster.js or Object3D.raycast), and a
|
|
63
|
+
// real scene can ship an invisible collision box alongside separately-
|
|
64
|
+
// placed visible ground models — without this filter, a click that
|
|
65
|
+
// visibly lands on the rendered ground can silently return the
|
|
66
|
+
// collider's coordinates instead.
|
|
67
|
+
const targets = Array.from(entityByObject.keys()).filter((o) => o.visible !== false);
|
|
68
|
+
const hits = raycaster.intersectObjects(targets, true);
|
|
69
|
+
if (hits.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
const hit = hits[0];
|
|
72
|
+
let node = hit.object;
|
|
73
|
+
let entityId = null;
|
|
74
|
+
while (node) {
|
|
75
|
+
const id = entityByObject.get(node);
|
|
76
|
+
if (id) {
|
|
77
|
+
entityId = id;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
node = node.parent;
|
|
81
|
+
}
|
|
82
|
+
marker.position.copy(hit.point);
|
|
83
|
+
marker.visible = true;
|
|
84
|
+
window.parent.postMessage({ type: 'umicat:editor:pickPoint3d', x: hit.point.x, y: hit.point.y, z: hit.point.z, entityId }, '*');
|
|
85
|
+
};
|
|
86
|
+
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
|
87
|
+
renderer.domElement.addEventListener('pointermove', onPointerMove);
|
|
88
|
+
renderer.domElement.addEventListener('pointerup', onPointerUp);
|
|
89
|
+
// Design mode skips physics and animation entirely (loadScene3D's own
|
|
90
|
+
// designMode flag) — there are no mixers to advance and no Rapier world to
|
|
91
|
+
// step, so the loop only ever needs to render.
|
|
92
|
+
renderer.setAnimationLoop(() => {
|
|
93
|
+
controls.update();
|
|
94
|
+
if (scene)
|
|
95
|
+
renderer.render(scene, camera);
|
|
96
|
+
});
|
|
97
|
+
const sceneId = opts.sceneId;
|
|
98
|
+
if (!sceneId)
|
|
99
|
+
return; // no scene chosen yet — idle, waiting for the host
|
|
100
|
+
try {
|
|
101
|
+
const manifest = await fetch('scenes3d/manifest.json').then((r) => r.json());
|
|
102
|
+
const scene3d = await fetch(`scenes3d/${sceneId}.json`).then((r) => r.json());
|
|
103
|
+
const loaded = await loadScene3D(scene3d, manifest, { assetBase: '', designMode: true });
|
|
104
|
+
scene = loaded.scene;
|
|
105
|
+
entityByObject.clear();
|
|
106
|
+
for (const [id, obj] of loaded.entities)
|
|
107
|
+
entityByObject.set(obj, id);
|
|
108
|
+
marker.visible = false;
|
|
109
|
+
scene.add(marker);
|
|
110
|
+
// Frame the whole scene — auto-fit, since a small template scene and a
|
|
111
|
+
// real level's dozens of entities need wildly different orbit distances.
|
|
112
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
113
|
+
const size = box.getSize(new THREE.Vector3());
|
|
114
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
115
|
+
const radius = Math.max(size.length() * 0.5, 0.5);
|
|
116
|
+
controls.target.copy(center);
|
|
117
|
+
camera.position.set(center.x + radius, center.y + radius * 0.8, center.z + radius);
|
|
118
|
+
camera.near = Math.max(radius / 100, 0.01);
|
|
119
|
+
camera.far = radius * 20;
|
|
120
|
+
camera.updateProjectionMatrix();
|
|
121
|
+
controls.update();
|
|
122
|
+
window.parent.postMessage({ type: 'umicat:editor:scene3dLoaded', sceneId }, '*');
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.warn('[umicat/editor] 3D design player: could not load scene', sceneId, e);
|
|
126
|
+
window.parent.postMessage({ type: 'umicat:editor:scene3dLoaded', sceneId, error: true }, '*');
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { GameAudio } from './GameAudio.js';
|
|
|
14
14
|
export type { GameAudioOptions, AudioClipSpec } from './GameAudio.js';
|
|
15
15
|
export { setupScreenshotListener, takeScreenshot } from './capture/ScreenshotManager.js';
|
|
16
16
|
export { setupRecordingListener } from './capture/RecordingManager.js';
|
|
17
|
+
export { runEditorDesignPlayer3D } from './editor/EditorDesignPlayer3D.js';
|
|
18
|
+
export type { EditorDesignPlayer3DOptions } from './editor/EditorDesignPlayer3D.js';
|
|
17
19
|
export type { Attachment } from './Sockets.js';
|
|
18
20
|
export type { LoadedScene3D, LoadSceneOptions } from './SceneLoader3D.js';
|
|
19
21
|
export { ORIENTATION_DIMENSIONS } from '@umicat/platform-sdk/orientation.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { flashTint, updateTints, isTinted } from './Tint.js';
|
|
|
14
14
|
export { GameAudio } from './GameAudio.js';
|
|
15
15
|
export { setupScreenshotListener, takeScreenshot } from './capture/ScreenshotManager.js';
|
|
16
16
|
export { setupRecordingListener } from './capture/RecordingManager.js';
|
|
17
|
+
export { runEditorDesignPlayer3D } from './editor/EditorDesignPlayer3D.js';
|
|
17
18
|
// Re-exported so a game imports one package for the common case. A game should
|
|
18
19
|
// not have to know that identity and saves come from a different package than
|
|
19
20
|
// the renderer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|