@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.16.1",
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",