@umicat/three-sdk 0.17.0 → 0.17.2

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.
@@ -21,13 +21,43 @@ import { loadScene3D } from '../SceneLoader3D.js';
21
21
  * real game.
22
22
  */
23
23
  export async function runEditorDesignPlayer3D(renderer, opts = {}) {
24
+ hideNonCanvasStaticMarkup(renderer.domElement);
24
25
  const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.05, 2000);
25
26
  const controls = new OrbitControls(camera, renderer.domElement);
26
27
  controls.enableDamping = true;
27
- // A single persistent marker, repositioned per click.
28
+ // A single persistent marker, repositioned per click — the exact world
29
+ // point clicked (what "put a torch at the spot I clicked" needs), distinct
30
+ // from the box below (which entity was hit, not just where).
28
31
  const marker = new THREE.Mesh(new THREE.SphereGeometry(0.12, 16, 12), new THREE.MeshBasicMaterial({ color: '#ff3b6b', depthTest: false }));
29
32
  marker.visible = false;
30
33
  marker.renderOrder = 999;
34
+ // Selection box — Unity/Blender-style "frame the object you clicked", not
35
+ // just a point on its surface. `BoxHelper.setFromObject` (three.js core,
36
+ // no custom geometry math needed) recomputes a world-space AABB wireframe
37
+ // from scratch each call, so retargeting it to a different entity per
38
+ // click is just calling it again — it does not need to track a moving
39
+ // object (this scene never animates in design mode).
40
+ const selectionBox = new THREE.BoxHelper(new THREE.Object3D(), 0xffd23f);
41
+ selectionBox.visible = false;
42
+ selectionBox.renderOrder = 998;
43
+ selectionBox.material.depthTest = false;
44
+ // Orientation gizmo — a small corner compass tracking the main camera's
45
+ // rotation only (never its position/zoom), same convention every 3D DCC
46
+ // uses: X=red, Y=green, Z=blue. Rendered as a second, tiny scene into a
47
+ // scissored corner viewport each frame (see the render loop below) — the
48
+ // simplest way to get a HUD-anchored 3D readout without a second canvas.
49
+ const GIZMO_SIZE = 84;
50
+ const GIZMO_MARGIN = 14;
51
+ const gizmoScene = new THREE.Scene();
52
+ const gizmoCamera = new THREE.OrthographicCamera(-1.6, 1.6, 1.6, -1.6, 0.1, 10);
53
+ const AXES = [
54
+ [new THREE.Vector3(1, 0, 0), 0xff4d4d], // X — red
55
+ [new THREE.Vector3(0, 1, 0), 0x4dff88], // Y — green
56
+ [new THREE.Vector3(0, 0, 1), 0x4d9fff], // Z — blue
57
+ ];
58
+ for (const [dir, color] of AXES) {
59
+ gizmoScene.add(new THREE.ArrowHelper(dir, new THREE.Vector3(0, 0, 0), 1, color, 0.35, 0.22));
60
+ }
31
61
  let scene = null;
32
62
  const entityByObject = new Map();
33
63
  const resize = () => {
@@ -71,16 +101,29 @@ export async function runEditorDesignPlayer3D(renderer, opts = {}) {
71
101
  const hit = hits[0];
72
102
  let node = hit.object;
73
103
  let entityId = null;
104
+ let entityRoot = null;
74
105
  while (node) {
75
106
  const id = entityByObject.get(node);
76
107
  if (id) {
77
108
  entityId = id;
109
+ entityRoot = node;
78
110
  break;
79
111
  }
80
112
  node = node.parent;
81
113
  }
82
114
  marker.position.copy(hit.point);
83
115
  marker.visible = true;
116
+ // Frame the whole entity the click landed on, not just the point on its
117
+ // surface — `setFromObject` walks the root's full subtree, so a
118
+ // multi-mesh entity (a model with separate parts) gets one box around
119
+ // all of it, matching what "entityId" actually refers to.
120
+ if (entityRoot) {
121
+ selectionBox.setFromObject(entityRoot);
122
+ selectionBox.visible = true;
123
+ }
124
+ else {
125
+ selectionBox.visible = false;
126
+ }
84
127
  window.parent.postMessage({ type: 'umicat:editor:pickPoint3d', x: hit.point.x, y: hit.point.y, z: hit.point.z, entityId }, '*');
85
128
  };
86
129
  renderer.domElement.addEventListener('pointerdown', onPointerDown);
@@ -91,8 +134,30 @@ export async function runEditorDesignPlayer3D(renderer, opts = {}) {
91
134
  // step, so the loop only ever needs to render.
92
135
  renderer.setAnimationLoop(() => {
93
136
  controls.update();
94
- if (scene)
95
- renderer.render(scene, camera);
137
+ if (!scene)
138
+ return;
139
+ renderer.setScissorTest(false);
140
+ renderer.render(scene, camera);
141
+ // Second pass: the orientation gizmo, into a small scissored corner
142
+ // viewport. Its camera copies the MAIN camera's rotation only (not
143
+ // position/zoom) so it reads as "which way am I looking", exactly what
144
+ // orbiting the main view is meant to answer. three.js viewport Y is
145
+ // bottom-up, so "top-right on screen" is `height - size - margin`.
146
+ const w = renderer.domElement.clientWidth;
147
+ const h = renderer.domElement.clientHeight;
148
+ gizmoCamera.position.set(0, 0, 4).applyQuaternion(camera.quaternion);
149
+ gizmoCamera.up.copy(camera.up);
150
+ gizmoCamera.lookAt(0, 0, 0);
151
+ const gx = w - GIZMO_SIZE - GIZMO_MARGIN;
152
+ const gy = h - GIZMO_SIZE - GIZMO_MARGIN;
153
+ renderer.setScissorTest(true);
154
+ renderer.setScissor(gx, gy, GIZMO_SIZE, GIZMO_SIZE);
155
+ renderer.setViewport(gx, gy, GIZMO_SIZE, GIZMO_SIZE);
156
+ // render()'s own autoClear respects the scissor rect just set, so this
157
+ // only wipes the corner box, not the scene pass above.
158
+ renderer.render(gizmoScene, gizmoCamera);
159
+ renderer.setScissorTest(false);
160
+ renderer.setViewport(0, 0, w, h);
96
161
  });
97
162
  const sceneId = opts.sceneId;
98
163
  if (!sceneId)
@@ -107,6 +172,8 @@ export async function runEditorDesignPlayer3D(renderer, opts = {}) {
107
172
  entityByObject.set(obj, id);
108
173
  marker.visible = false;
109
174
  scene.add(marker);
175
+ selectionBox.visible = false;
176
+ scene.add(selectionBox);
110
177
  // Frame the whole scene — auto-fit, since a small template scene and a
111
178
  // real level's dozens of entities need wildly different orbit distances.
112
179
  const box = new THREE.Box3().setFromObject(scene);
@@ -126,3 +193,42 @@ export async function runEditorDesignPlayer3D(renderer, opts = {}) {
126
193
  window.parent.postMessage({ type: 'umicat:editor:scene3dLoaded', sceneId, error: true }, '*');
127
194
  }
128
195
  }
196
+ /**
197
+ * Hides every other top-level DOM node the game shipped, keeping only the
198
+ * canvas. Mirrors `@umicat/phaser-sdk`'s edit-mode markup hider (`UmicatGame.ts`)
199
+ * exactly, including its `data-umicat-keep` opt-out — same bug, same fix,
200
+ * different renderer.
201
+ *
202
+ * Two distinct problems, one cause: `index.html` markup has no owner in edit
203
+ * mode, because none of the game's own code runs to manage it. A static score
204
+ * HUD just clutters a scene that's supposed to read as empty of gameplay. A
205
+ * game with its own full-screen input-capturing overlay (a drawing surface
206
+ * that "the whole screen collects strokes" is a real, shipped example) is
207
+ * worse: that overlay sits ABOVE the canvas and silently swallows every
208
+ * pointer event meant for OrbitControls — confirmed live, orbit/zoom did
209
+ * nothing at all until this ran, with zero console errors, because the events
210
+ * simply never reached the renderer's own `domElement`.
211
+ */
212
+ function hideNonCanvasStaticMarkup(canvas) {
213
+ if (typeof document === 'undefined' || !document.body)
214
+ return;
215
+ const hidden = [];
216
+ for (const el of Array.from(document.body.children)) {
217
+ if (!(el instanceof HTMLElement))
218
+ continue;
219
+ const tag = el.tagName;
220
+ if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'NOSCRIPT')
221
+ continue;
222
+ if (el === canvas)
223
+ continue;
224
+ if (el.hasAttribute('data-umicat-keep'))
225
+ continue; // opt OUT, for markup that IS design-time content
226
+ if (el.style.display === 'none')
227
+ continue;
228
+ el.style.display = 'none';
229
+ hidden.push(el.id ? `#${el.id}` : tag.toLowerCase());
230
+ }
231
+ if (hidden.length > 0) {
232
+ console.info(`[umicat/editor] hid ${hidden.join(', ')} — index.html markup with no game code running to remove it. Add data-umicat-keep to keep one visible in Edit.`);
233
+ }
234
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/three-sdk",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
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",