@unboxy/phaser-sdk 0.2.26 → 0.2.27

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.
@@ -4,7 +4,7 @@ import { parseColor, spawnEntity } from '../scene/spawnEntity.js';
4
4
  import { resolveRenderScript } from '../scene/renderScripts.js';
5
5
  import { getManifest } from '../scene/SceneLoader.js';
6
6
  import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
7
- import { getEditorState, setEditorActive, setSelection, } from './EditorState.js';
7
+ import { getEditorState, setEditorActive, setSelection, getSelection, } from './EditorState.js';
8
8
  /**
9
9
  * EditorBridge wires the host (home-ui) to the iframe's Phaser game during
10
10
  * Edit mode. Manages:
@@ -52,15 +52,18 @@ function handleMessage(game, msg) {
52
52
  break;
53
53
  case 'unboxy:editor:setSelection':
54
54
  setSelection(game, msg.entityIds[0] ?? null);
55
+ postSelectionRect(game);
55
56
  break;
56
57
  case 'unboxy:editor:panZoom':
57
58
  applyPanZoom(game, msg);
59
+ postSelectionRect(game);
58
60
  break;
59
61
  case 'unboxy:editor:createEntity':
60
62
  void createEntity(game, msg.entity, msg.manifestAsset);
61
63
  break;
62
64
  case 'unboxy:editor:deleteEntity':
63
65
  deleteEntity(game, msg.entityId);
66
+ postSelectionRect(game);
64
67
  break;
65
68
  }
66
69
  }
@@ -164,6 +167,121 @@ function postSceneSnapshot(game) {
164
167
  });
165
168
  game[SNAPSHOT_POSTED_FLAG] = sceneFile.id;
166
169
  }
170
+ // --- Selection rect (slice 4) ---------------------------------------------
171
+ //
172
+ // Emit the selected entity's DOM screen rect to the host so it can anchor
173
+ // the ✨ button + popover next to the entity. Coalesce multiple calls
174
+ // inside one RAF tick — Inspector spam (per-keystroke transform edits)
175
+ // would otherwise spam the postMessage channel.
176
+ const RECT_RAF_FLAG = '__unboxyEditorSelectionRectRafScheduled';
177
+ function postSelectionRect(game) {
178
+ const bag = game;
179
+ if (bag[RECT_RAF_FLAG])
180
+ return;
181
+ bag[RECT_RAF_FLAG] = true;
182
+ // requestAnimationFrame runs after Phaser's render → bounds reflect the
183
+ // freshly-applied transform. Guarantees one emit per frame max.
184
+ if (typeof requestAnimationFrame === 'function') {
185
+ requestAnimationFrame(() => {
186
+ bag[RECT_RAF_FLAG] = false;
187
+ doPostSelectionRect(game);
188
+ });
189
+ }
190
+ else {
191
+ bag[RECT_RAF_FLAG] = false;
192
+ doPostSelectionRect(game);
193
+ }
194
+ }
195
+ function doPostSelectionRect(game) {
196
+ const id = getSelection(game);
197
+ if (!id) {
198
+ postToHost({ type: 'unboxy:editor:selectionRect', entityId: null, rect: null });
199
+ return;
200
+ }
201
+ const registry = findRegistry(game);
202
+ const go = registry?.byId(id);
203
+ if (!go) {
204
+ postToHost({ type: 'unboxy:editor:selectionRect', entityId: id, rect: null });
205
+ return;
206
+ }
207
+ const rect = computeScreenRect(game, go);
208
+ postToHost({ type: 'unboxy:editor:selectionRect', entityId: id, rect });
209
+ }
210
+ /**
211
+ * Compute the entity's DOM screen rect — viewport pixels relative to the
212
+ * iframe's top-left. The host adds the iframe's bounding rect to translate
213
+ * into page coords.
214
+ *
215
+ * Path: world coords → camera transform → canvas pixels → iframe pixels.
216
+ * Phaser's Scale.FIT may letterbox/pillarbox the canvas inside the iframe,
217
+ * so we factor in the canvas's parent offset too.
218
+ */
219
+ function computeScreenRect(game, go) {
220
+ // Pull world-coord bounds — same logic the editor uses for hit-test.
221
+ const hitW = go.getData('editorHitWidth');
222
+ const hitH = go.getData('editorHitHeight');
223
+ const positioned = go;
224
+ let worldRect;
225
+ if (typeof hitW === 'number' && typeof hitH === 'number') {
226
+ worldRect = {
227
+ x: positioned.x - hitW / 2,
228
+ y: positioned.y - hitH / 2,
229
+ width: hitW,
230
+ height: hitH,
231
+ };
232
+ }
233
+ else {
234
+ const withBounds = go;
235
+ if (typeof withBounds.getBounds !== 'function')
236
+ return null;
237
+ const r = withBounds.getBounds();
238
+ worldRect = { x: r.x, y: r.y, width: r.width, height: r.height };
239
+ }
240
+ // Find the world scene's camera — it carries scrollX/Y + zoom.
241
+ let cam = null;
242
+ for (const scene of game.scene.getScenes(false)) {
243
+ const key = scene.scene.key;
244
+ if (BOOT_SCENE_KEYS.has(key))
245
+ continue;
246
+ if (key === EDITOR_OVERLAY_KEY)
247
+ continue;
248
+ if (getEntityRegistry(scene)) {
249
+ cam = scene.cameras.main;
250
+ break;
251
+ }
252
+ }
253
+ if (!cam)
254
+ return null;
255
+ // World → canvas pixels (relative to canvas top-left, inside Phaser's
256
+ // logical canvas space — i.e. the game.config width/height, NOT the
257
+ // scaled visible pixels).
258
+ const canvasX = (worldRect.x - cam.scrollX) * cam.zoom;
259
+ const canvasY = (worldRect.y - cam.scrollY) * cam.zoom;
260
+ const canvasW = worldRect.width * cam.zoom;
261
+ const canvasH = worldRect.height * cam.zoom;
262
+ // Phaser FIT scale factor — the actual rendered canvas is scaled by this
263
+ // ratio inside the iframe, with letterbox/pillarbox margins centered.
264
+ const scaleManager = game.scale;
265
+ const renderScale = scaleManager.displayScale.x; // x and y are equal under FIT
266
+ // Canvas's offset inside the iframe (CSS pixels).
267
+ const canvas = game.canvas;
268
+ let offsetX = 0;
269
+ let offsetY = 0;
270
+ // Walk the offset chain — iframe canvas is usually centered under
271
+ // CENTER_BOTH, so its parent has padding. getBoundingClientRect on the
272
+ // canvas vs the iframe document root tells us the offset.
273
+ if (canvas && typeof canvas.getBoundingClientRect === 'function') {
274
+ const r = canvas.getBoundingClientRect();
275
+ offsetX = r.left;
276
+ offsetY = r.top;
277
+ }
278
+ return {
279
+ x: offsetX + canvasX * renderScale,
280
+ y: offsetY + canvasY * renderScale,
281
+ width: canvasW * renderScale,
282
+ height: canvasH * renderScale,
283
+ };
284
+ }
167
285
  function readActiveManifest(game) {
168
286
  for (const scene of game.scene.getScenes(false)) {
169
287
  const cache = scene.cache.json;
@@ -285,6 +403,10 @@ function applyEdit(game, entityId, patch) {
285
403
  if (patch.properties !== undefined) {
286
404
  go.setData('entityProperties', patch.properties);
287
405
  }
406
+ // If this edit moved the selected entity, the host's ✨ button + popover
407
+ // anchor needs the new rect. Cheap RAF-coalesced.
408
+ if (getSelection(game) === entityId)
409
+ postSelectionRect(game);
288
410
  }
289
411
  /**
290
412
  * Re-render a code-rendered entity's visual when its params change.
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript,
26
26
  export type { RenderScriptModule } from './scene/renderScripts.js';
27
27
  export { setupEditorBridge } from './editor/EditorBridge.js';
28
28
  export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
29
- export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSdkToHostMessage, } from './protocol.js';
29
+ export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSelectionRectMessage, EditorSdkToHostMessage, } from './protocol.js';
30
30
  export { SCHEMA_VERSION, } from './scene/types.js';
31
31
  export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, TriggerShape, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, Transform, Anchor, AnchorSide, } from './scene/types.js';
32
32
  export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, type GameDataGetParams, type GameDataGetResult, type GameDataSetParams, type GameDataSetResult, type GameDataDeleteParams, type GameDataDeleteResult, type GameDataListResult, type RealtimeGetTokenParams, type RealtimeGetTokenResult, } from './protocol.js';
@@ -179,7 +179,31 @@ export interface EditorShortcutMessage {
179
179
  type: 'unboxy:editor:shortcut';
180
180
  action: 'undo' | 'redo' | 'save' | 'delete';
181
181
  }
182
- export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage | EditorShortcutMessage;
182
+ /**
183
+ * The selected entity's DOM screen rect (slice 4 — popover anchoring).
184
+ *
185
+ * Emitted on selection change, drag-end, and pan/zoom — NOT per frame. The
186
+ * host uses this to anchor the floating ✨ button + InlineAIPopover next
187
+ * to the entity. Coords are relative to the iframe element (not the
188
+ * entity's world coords); the host adds the iframe's own bounding rect to
189
+ * land in viewport space. Sent with `null` when nothing is selected, so
190
+ * the host can hide the ✨ button.
191
+ *
192
+ * Slice 4 only handles single-select. Multi-select (slice 4.5) will pass
193
+ * the union bounding box.
194
+ */
195
+ export interface EditorSelectionRectMessage {
196
+ type: 'unboxy:editor:selectionRect';
197
+ entityId: string | null;
198
+ /** null when nothing is selected. */
199
+ rect: {
200
+ x: number;
201
+ y: number;
202
+ width: number;
203
+ height: number;
204
+ } | null;
205
+ }
206
+ export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage | EditorShortcutMessage | EditorSelectionRectMessage;
183
207
  export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list' | 'gameData.get' | 'gameData.set' | 'gameData.delete' | 'gameData.list' | 'realtime.getToken';
184
208
  export interface SavesGetParams {
185
209
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Unboxy Phaser 3 SDK — game infrastructure for the Unboxy platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",