@umicat/three-sdk 0.17.2 → 0.17.3
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.
|
@@ -16,10 +16,20 @@ export interface EditorDesignPlayer3DOptions {
|
|
|
16
16
|
* The 3D counterpart of `@umicat/phaser-sdk`'s `EditorDesignScene` (ADR-021)
|
|
17
17
|
* — renders a scene's AUTHORED data with no game code and no save, so the
|
|
18
18
|
* platform's Edit tab can show a 3D game's layout the same way it already
|
|
19
|
-
* does for 2D.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* does for 2D.
|
|
20
|
+
*
|
|
21
|
+
* Two interactions:
|
|
22
|
+
* - **Click** an entity to select it — a bounding box frames it and a
|
|
23
|
+
* marker reads off the exact world coordinate, posted to the host so a
|
|
24
|
+
* person can point at a real spot instead of guessing one blind when
|
|
25
|
+
* telling the AI where to place something.
|
|
26
|
+
* - **Drag** an entity to move it (Phase 1 of real editing, ground-plane
|
|
27
|
+
* constrained — position only, no rotation/scale, no gizmo, no
|
|
28
|
+
* Inspector). This function itself never writes anything — it only
|
|
29
|
+
* moves the live THREE.Object3D and posts the final LOCAL position on
|
|
30
|
+
* release (`umicat:editor:dragEnd3d`); the host owns turning that into a
|
|
31
|
+
* disk write. Grabbing empty space/sky orbits instead, same convention
|
|
32
|
+
* as Unity/Blender.
|
|
23
33
|
*
|
|
24
34
|
* Takes over `renderer`'s animation loop entirely for the rest of the page's
|
|
25
35
|
* life — there is no scene-swap API and no teardown, matching how the
|
|
@@ -5,10 +5,20 @@ import { loadScene3D } from '../SceneLoader3D.js';
|
|
|
5
5
|
* The 3D counterpart of `@umicat/phaser-sdk`'s `EditorDesignScene` (ADR-021)
|
|
6
6
|
* — renders a scene's AUTHORED data with no game code and no save, so the
|
|
7
7
|
* platform's Edit tab can show a 3D game's layout the same way it already
|
|
8
|
-
* does for 2D.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* does for 2D.
|
|
9
|
+
*
|
|
10
|
+
* Two interactions:
|
|
11
|
+
* - **Click** an entity to select it — a bounding box frames it and a
|
|
12
|
+
* marker reads off the exact world coordinate, posted to the host so a
|
|
13
|
+
* person can point at a real spot instead of guessing one blind when
|
|
14
|
+
* telling the AI where to place something.
|
|
15
|
+
* - **Drag** an entity to move it (Phase 1 of real editing, ground-plane
|
|
16
|
+
* constrained — position only, no rotation/scale, no gizmo, no
|
|
17
|
+
* Inspector). This function itself never writes anything — it only
|
|
18
|
+
* moves the live THREE.Object3D and posts the final LOCAL position on
|
|
19
|
+
* release (`umicat:editor:dragEnd3d`); the host owns turning that into a
|
|
20
|
+
* disk write. Grabbing empty space/sky orbits instead, same convention
|
|
21
|
+
* as Unity/Blender.
|
|
12
22
|
*
|
|
13
23
|
* Takes over `renderer`'s animation loop entirely for the rest of the page's
|
|
14
24
|
* life — there is no scene-swap API and no teardown, matching how the
|
|
@@ -67,64 +77,143 @@ export async function runEditorDesignPlayer3D(renderer, opts = {}) {
|
|
|
67
77
|
};
|
|
68
78
|
resize();
|
|
69
79
|
window.addEventListener('resize', resize);
|
|
80
|
+
const raycaster = new THREE.Raycaster();
|
|
81
|
+
const ndcFromEvent = (e) => {
|
|
82
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
83
|
+
return new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
|
|
84
|
+
};
|
|
85
|
+
// Only VISIBLE entity roots are ever raycast against. three.js's
|
|
86
|
+
// Mesh.raycast does NOT check `.visible` on its own (confirmed against the
|
|
87
|
+
// pinned three source — no such check in Raycaster.js or Object3D.raycast),
|
|
88
|
+
// and a real scene can ship an invisible collision box alongside
|
|
89
|
+
// separately-placed visible ground models — without this filter, a click
|
|
90
|
+
// that visibly lands on the rendered ground can silently return the
|
|
91
|
+
// collider's coordinates instead.
|
|
92
|
+
const pickEntityAt = (e) => {
|
|
93
|
+
raycaster.setFromCamera(ndcFromEvent(e), camera);
|
|
94
|
+
const targets = Array.from(entityByObject.keys()).filter((o) => o.visible !== false);
|
|
95
|
+
const hits = raycaster.intersectObjects(targets, true);
|
|
96
|
+
if (hits.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
let node = hits[0].object;
|
|
99
|
+
while (node) {
|
|
100
|
+
const id = entityByObject.get(node);
|
|
101
|
+
if (id)
|
|
102
|
+
return { entityId: id, entityRoot: node, point: hits[0].point };
|
|
103
|
+
node = node.parent;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
70
107
|
// Orbit-vs-click: a real drag moves the pointer more than a few px.
|
|
71
108
|
let downX = 0;
|
|
72
109
|
let downY = 0;
|
|
73
110
|
let moved = false;
|
|
111
|
+
// Entity drag — Phase 1 of real editing (translate only, ground-plane
|
|
112
|
+
// constrained). Grabbing on an entity moves it; grabbing empty space/sky
|
|
113
|
+
// still orbits, same convention as Unity/Blender. Whether THIS gesture is
|
|
114
|
+
// a drag is decided synchronously in pointerdown (a raycast hit or not) —
|
|
115
|
+
// `OrbitControls`'s own pointerdown listener runs first (registered
|
|
116
|
+
// earlier) and always starts its internal drag bookkeeping, but its
|
|
117
|
+
// `onPointerMove`/related handlers re-check `this.enabled` on every event
|
|
118
|
+
// rather than only at attach time (confirmed against the pinned
|
|
119
|
+
// OrbitControls source), so disabling it HERE still correctly suppresses
|
|
120
|
+
// every subsequent move for this gesture; its `onPointerUp` cleanup does
|
|
121
|
+
// not check `enabled` at all, so re-enabling afterward never leaves it
|
|
122
|
+
// stuck. No drag state survives a scene reload — it's cleared on release,
|
|
123
|
+
// and a scene swap only ever happens via a full iframe reload anyway.
|
|
124
|
+
let dragEntity = null;
|
|
125
|
+
let dragEntityId = null;
|
|
126
|
+
let dragPlane = null;
|
|
127
|
+
let dragGrabOffset = null; // world-space, plane-point minus object position at grab time
|
|
74
128
|
const onPointerDown = (e) => {
|
|
75
129
|
downX = e.clientX;
|
|
76
130
|
downY = e.clientY;
|
|
77
131
|
moved = false;
|
|
132
|
+
if (!scene)
|
|
133
|
+
return;
|
|
134
|
+
const hit = pickEntityAt(e);
|
|
135
|
+
if (!hit)
|
|
136
|
+
return; // empty space — let OrbitControls own this gesture
|
|
137
|
+
const objectPos = hit.entityRoot.getWorldPosition(new THREE.Vector3());
|
|
138
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -objectPos.y);
|
|
139
|
+
const planePoint = new THREE.Vector3();
|
|
140
|
+
// Looking exactly parallel to the ground has no plane intersection —
|
|
141
|
+
// vanishingly rare for this camera (auto-framed, always angled down at
|
|
142
|
+
// the scene) but cheap to guard: fall through to orbit rather than
|
|
143
|
+
// start a drag from a garbage grab offset.
|
|
144
|
+
if (!raycaster.ray.intersectPlane(plane, planePoint))
|
|
145
|
+
return;
|
|
146
|
+
dragEntity = hit.entityRoot;
|
|
147
|
+
dragEntityId = hit.entityId;
|
|
148
|
+
dragPlane = plane;
|
|
149
|
+
dragGrabOffset = planePoint.sub(objectPos);
|
|
150
|
+
controls.enabled = false;
|
|
151
|
+
try {
|
|
152
|
+
renderer.domElement.setPointerCapture(e.pointerId);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Multi-touch / an already-released pointer can throw here — never
|
|
156
|
+
// let that leave the gesture half-started (see the board-game touch
|
|
157
|
+
// lesson: an uncaught throw from setPointerCapture stranded a finger
|
|
158
|
+
// as permanently "down"). The drag still works via normal bubbling;
|
|
159
|
+
// capture is an enhancement (keeps it live if the cursor leaves the
|
|
160
|
+
// canvas mid-drag), not a requirement.
|
|
161
|
+
}
|
|
78
162
|
};
|
|
79
163
|
const onPointerMove = (e) => {
|
|
80
164
|
if (Math.abs(e.clientX - downX) > 4 || Math.abs(e.clientY - downY) > 4)
|
|
81
165
|
moved = true;
|
|
166
|
+
if (!dragEntity || !dragPlane || !dragGrabOffset || !scene)
|
|
167
|
+
return;
|
|
168
|
+
raycaster.setFromCamera(ndcFromEvent(e), camera);
|
|
169
|
+
const planePoint = new THREE.Vector3();
|
|
170
|
+
if (!raycaster.ray.intersectPlane(dragPlane, planePoint))
|
|
171
|
+
return; // looking parallel to the plane
|
|
172
|
+
const newWorldPos = planePoint.sub(dragGrabOffset);
|
|
173
|
+
if (dragEntity.parent)
|
|
174
|
+
dragEntity.parent.worldToLocal(newWorldPos);
|
|
175
|
+
dragEntity.position.copy(newWorldPos);
|
|
176
|
+
selectionBox.setFromObject(dragEntity);
|
|
82
177
|
};
|
|
83
178
|
const onPointerUp = (e) => {
|
|
179
|
+
try {
|
|
180
|
+
renderer.domElement.releasePointerCapture(e.pointerId);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Already released, or never captured (e.g. this gesture never hit an
|
|
184
|
+
// entity) — nothing to do either way.
|
|
185
|
+
}
|
|
186
|
+
// A drag only really happened if the pointer actually moved — grabbing
|
|
187
|
+
// an entity and releasing without moving is a CLICK on it (case below),
|
|
188
|
+
// not a same-position no-op save.
|
|
189
|
+
if (moved && dragEntity && dragEntityId) {
|
|
190
|
+
window.parent.postMessage({
|
|
191
|
+
type: 'umicat:editor:dragEnd3d',
|
|
192
|
+
sceneId: opts.sceneId,
|
|
193
|
+
entityId: dragEntityId,
|
|
194
|
+
position: { x: dragEntity.position.x, y: dragEntity.position.y, z: dragEntity.position.z },
|
|
195
|
+
}, '*');
|
|
196
|
+
}
|
|
197
|
+
dragEntity = null;
|
|
198
|
+
dragEntityId = null;
|
|
199
|
+
dragPlane = null;
|
|
200
|
+
dragGrabOffset = null;
|
|
201
|
+
controls.enabled = true;
|
|
84
202
|
if (moved || !scene)
|
|
85
203
|
return;
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
raycaster.setFromCamera(mouse, camera);
|
|
90
|
-
// Only cast against VISIBLE entity roots. three.js's Mesh.raycast does
|
|
91
|
-
// NOT check `.visible` on its own (confirmed against the pinned three
|
|
92
|
-
// source — no such check in Raycaster.js or Object3D.raycast), and a
|
|
93
|
-
// real scene can ship an invisible collision box alongside separately-
|
|
94
|
-
// placed visible ground models — without this filter, a click that
|
|
95
|
-
// visibly lands on the rendered ground can silently return the
|
|
96
|
-
// collider's coordinates instead.
|
|
97
|
-
const targets = Array.from(entityByObject.keys()).filter((o) => o.visible !== false);
|
|
98
|
-
const hits = raycaster.intersectObjects(targets, true);
|
|
99
|
-
if (hits.length === 0)
|
|
204
|
+
// A plain click (no drag) — the existing read-a-coordinate behavior.
|
|
205
|
+
const hit = pickEntityAt(e);
|
|
206
|
+
if (!hit)
|
|
100
207
|
return;
|
|
101
|
-
const hit = hits[0];
|
|
102
|
-
let node = hit.object;
|
|
103
|
-
let entityId = null;
|
|
104
|
-
let entityRoot = null;
|
|
105
|
-
while (node) {
|
|
106
|
-
const id = entityByObject.get(node);
|
|
107
|
-
if (id) {
|
|
108
|
-
entityId = id;
|
|
109
|
-
entityRoot = node;
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
node = node.parent;
|
|
113
|
-
}
|
|
114
208
|
marker.position.copy(hit.point);
|
|
115
209
|
marker.visible = true;
|
|
116
210
|
// Frame the whole entity the click landed on, not just the point on its
|
|
117
211
|
// surface — `setFromObject` walks the root's full subtree, so a
|
|
118
212
|
// multi-mesh entity (a model with separate parts) gets one box around
|
|
119
213
|
// all of it, matching what "entityId" actually refers to.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
selectionBox.visible = false;
|
|
126
|
-
}
|
|
127
|
-
window.parent.postMessage({ type: 'umicat:editor:pickPoint3d', x: hit.point.x, y: hit.point.y, z: hit.point.z, entityId }, '*');
|
|
214
|
+
selectionBox.setFromObject(hit.entityRoot);
|
|
215
|
+
selectionBox.visible = true;
|
|
216
|
+
window.parent.postMessage({ type: 'umicat:editor:pickPoint3d', x: hit.point.x, y: hit.point.y, z: hit.point.z, entityId: hit.entityId }, '*');
|
|
128
217
|
};
|
|
129
218
|
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
|
130
219
|
renderer.domElement.addEventListener('pointermove', onPointerMove);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
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",
|