@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.
- package/dist/editor/EditorBridge.js +123 -1
- package/dist/index.d.ts +1 -1
- package/dist/protocol.d.ts +25 -1
- package/package.json +1 -1
|
@@ -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';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -179,7 +179,31 @@ export interface EditorShortcutMessage {
|
|
|
179
179
|
type: 'unboxy:editor:shortcut';
|
|
180
180
|
action: 'undo' | 'redo' | 'save' | 'delete';
|
|
181
181
|
}
|
|
182
|
-
|
|
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;
|