@umicat/phaser-sdk 1.0.1 → 1.0.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.
@@ -2,6 +2,7 @@ import Phaser from 'phaser';
2
2
  import { AssetRecord } from '../scene/types.js';
3
3
  import { TilemapEditOp } from '../protocol.js';
4
4
  export declare function setupEditorBridge(game: Phaser.Game): void;
5
+ export declare function postSelectionRect(game: Phaser.Game): void;
5
6
  /**
6
7
  * FB.9a — return ALL entities at the given world coords, sorted by depth
7
8
  * DESC (topmost first). Used by EditorOverlayScene's Alt+click handler
@@ -43,6 +43,16 @@ export function setupEditorBridge(game) {
43
43
  return;
44
44
  handleMessage(game, data);
45
45
  });
46
+ // Boot-time edit flag (2026-06-11). When the host remounts the iframe
47
+ // while edit mode is on (post-save / post-agent-turn rebuild), it appends
48
+ // `umicatEdit=1` to the iframe URL. Enter edit IMMEDIATELY at game
49
+ // construction so scenes pause before their first update. Without this
50
+ // the game visibly plays for a second or two after every rebuild: the
51
+ // host's `enter` postMessage can't arrive any earlier because main.ts
52
+ // awaits font fetches before createUmicatGame installs this listener.
53
+ if (typeof window !== 'undefined' && /[?&]umicatEdit=1/.test(window.location.search)) {
54
+ enterEdit(game);
55
+ }
46
56
  }
47
57
  function handleMessage(game, msg) {
48
58
  switch (msg.type) {
@@ -134,6 +144,15 @@ function enterEdit(game) {
134
144
  restoreCanvasAfterEditor(game);
135
145
  setEditorActive(game, true);
136
146
  pauseActiveNonEditor(game);
147
+ // Continuous pause enforcement (2026-06-11) — the bounded 3s re-attempt
148
+ // loop below misses scenes that finish booting later than 3s after
149
+ // enter (slow CDN fetch of scene assets, etc.), and even within the
150
+ // window a scene can run for up to 100ms before the next tick pauses
151
+ // it. A PRE_STEP hook pauses any newly-active non-editor scene BEFORE
152
+ // its first update, for as long as edit mode is on. pauseActiveNonEditor
153
+ // skips already-paused scenes, so the per-frame cost is a short loop
154
+ // over the scene list.
155
+ installPauseEnforcement(game);
137
156
  // P1 infinite canvas — expand the Phaser canvas to fill its container
138
157
  // (host's iframe). Without this, the editor surface stays locked to
139
158
  // the game's intrinsic aspect ratio (720×1280 portrait, etc.) with
@@ -199,6 +218,33 @@ function enterEdit(game) {
199
218
  };
200
219
  setTimeout(reattempt, 100);
201
220
  }
221
+ const PAUSE_ENFORCER_FLAG = '__umicatEditorPauseEnforcer';
222
+ /**
223
+ * While edit mode is active, pause any non-editor scene before each game
224
+ * step. Catches scenes that boot AFTER enterEdit (iframe rebuild path) no
225
+ * matter how late — the game never visibly plays under the editor.
226
+ * Self-disarms via the active check; uninstalled on exitEdit.
227
+ */
228
+ function installPauseEnforcement(game) {
229
+ const bag = game;
230
+ if (bag[PAUSE_ENFORCER_FLAG])
231
+ return;
232
+ const handler = () => {
233
+ if (!getEditorState(game).active)
234
+ return;
235
+ pauseActiveNonEditor(game);
236
+ };
237
+ game.events.on(Phaser.Core.Events.PRE_STEP, handler);
238
+ bag[PAUSE_ENFORCER_FLAG] = handler;
239
+ }
240
+ function uninstallPauseEnforcement(game) {
241
+ const bag = game;
242
+ const handler = bag[PAUSE_ENFORCER_FLAG];
243
+ if (!handler)
244
+ return;
245
+ game.events.off(Phaser.Core.Events.PRE_STEP, handler);
246
+ delete bag[PAUSE_ENFORCER_FLAG];
247
+ }
202
248
  function pauseActiveNonEditor(game) {
203
249
  // P1 infinite canvas (2026-05-17) — use `setActive(false)` instead of
204
250
  // `pause()` so scenes STOP updating (game logic frozen) but KEEP
@@ -250,6 +296,9 @@ function exitEdit(game) {
250
296
  if (!getEditorState(game).active)
251
297
  return;
252
298
  setEditorActive(game, false);
299
+ // Active is false now so the enforcer self-disables, but remove the
300
+ // hook too — no point paying the per-frame check during play.
301
+ uninstallPauseEnforcement(game);
253
302
  setSelection(game, null);
254
303
  // Allow next enter to re-post the snapshot (scene file may have changed).
255
304
  delete game[SNAPSHOT_POSTED_FLAG];
@@ -407,7 +456,7 @@ function postRulesSnapshot(game) {
407
456
  // inside one RAF tick — Inspector spam (per-keystroke transform edits)
408
457
  // would otherwise spam the postMessage channel.
409
458
  const RECT_RAF_FLAG = '__unboxyEditorSelectionRectRafScheduled';
410
- function postSelectionRect(game) {
459
+ export function postSelectionRect(game) {
411
460
  const bag = game;
412
461
  if (bag[RECT_RAF_FLAG])
413
462
  return;
@@ -225,6 +225,11 @@ export declare class EditorOverlayScene extends Phaser.Scene {
225
225
  * stay constant size on screen regardless of how zoomed in/out.
226
226
  */
227
227
  private hitTestTilemapResizeHandle;
228
+ /**
229
+ * Shared handle hit-test body — used by both the tilemap and the
230
+ * rect-entity resize affordances (same 8-handle geometry).
231
+ */
232
+ private hitTestResizeHandlesAt;
228
233
  /**
229
234
  * Set canvas cursor to a resize-arrow when hovering a handle, or restore
230
235
  * default. Direction matches the handle's axis (Figma / Sketch / browser
@@ -263,6 +268,36 @@ export declare class EditorOverlayScene extends Phaser.Scene {
263
268
  * normal editTilemap dispatch (which mutates the live runtime).
264
269
  */
265
270
  private commitTilemapResize;
271
+ /**
272
+ * Selected rect entity's bounds + 8 handle positions in world coords.
273
+ * Returns null when the selection isn't a rect, in HUD mode, or when the
274
+ * rect is rotated (axis-aligned handle math would shear a rotated rect —
275
+ * v1 simply hides the handles there).
276
+ */
277
+ private entityResizeHandlePositions;
278
+ private hitTestEntityResizeHandle;
279
+ private beginEntityResizeDrag;
280
+ /**
281
+ * Update the in-flight rect resize from the cursor position. Edges the
282
+ * handle owns follow the cursor (snapped to whole pixels); opposite edges
283
+ * stay anchored. Applies the result to the live GameObject immediately.
284
+ */
285
+ private updateEntityResizeDrag;
286
+ /** Mutate the live GameObject to the preview size + center (display px). */
287
+ private applyEntityResizePreview;
288
+ /**
289
+ * Commit the rect resize — the GO is already at its final size (applied
290
+ * live per pointermove); post `entityResized` with BASE dims (entity-
291
+ * record units) so the host records the modify-entity command. Undo
292
+ * replays the `before` patch through the normal applyEdit path.
293
+ */
294
+ private commitEntityResize;
295
+ /**
296
+ * Render the 8 resize handles around the selected rect entity. No bounds
297
+ * outline (the blue selection rect already draws it) and no ghost rect
298
+ * (the live GO resizes in place during the drag).
299
+ */
300
+ private drawEntityResizeOverlay;
266
301
  update(): void;
267
302
  /**
268
303
  * Render the 8 resize handles (when paint mode is active) and the
@@ -3,8 +3,8 @@ import { getEntityRegistry } from '../scene/EntityRegistry.js';
3
3
  import { findHudEntity, findHudRegistry, UMICAT_HUD_SCENE_KEY, } from '../scene/HudRuntime.js';
4
4
  import { isPerFrameHitbox } from '../scene/types.js';
5
5
  import { getManifest } from '../scene/SceneLoader.js';
6
- import { getEditorState, startDrag, clearDrag, getDrag, getSelection, getEditorMode, getDebugOverlayState, getTilemapToolState, isTilemapPaintMode, beginTilemapStroke, beginAutotileStroke, appendTilemapStrokeCell, appendAutotileStrokeClick, endTilemapStroke, getTilemapStroke, beginTilemapRect, updateTilemapRect, endTilemapRect, getTilemapRect, getTilemapResize, beginTilemapResize, updateTilemapResize, endTilemapResize, } from './EditorState.js';
7
- import { applyTilemapOp, findTilemapLayerById, handleEditTilemap } from './EditorBridge.js';
6
+ import { getEditorState, startDrag, clearDrag, getDrag, getSelection, getEditorMode, getDebugOverlayState, getTilemapToolState, isTilemapPaintMode, beginTilemapStroke, beginAutotileStroke, appendTilemapStrokeCell, appendAutotileStrokeClick, endTilemapStroke, getTilemapStroke, beginTilemapRect, updateTilemapRect, endTilemapRect, getTilemapRect, getTilemapResize, beginTilemapResize, updateTilemapResize, endTilemapResize, getEntityResize, beginEntityResize, updateEntityResize, endEntityResize, } from './EditorState.js';
7
+ import { applyTilemapOp, findTilemapLayerById, handleEditTilemap, postSelectionRect } from './EditorBridge.js';
8
8
  import { applyAutotile, findTerrain, getAutotileKind } from '../scene/autotile.js';
9
9
  /**
10
10
  * Editor overlay — slice 2.
@@ -258,6 +258,16 @@ export class EditorOverlayScene extends Phaser.Scene {
258
258
  // selection so the user can click out of paint mode by clicking
259
259
  // another entity / empty canvas.
260
260
  }
261
+ // Rect-entity resize handles (2026-06-10) — selected rect entities get
262
+ // the same 8-handle resize affordance as tilemaps. Checked BEFORE the
263
+ // entity hit-test so grabbing an edge/corner resizes instead of
264
+ // re-selecting + starting a move-drag.
265
+ const entityHandle = this.hitTestEntityResizeHandle(px, py);
266
+ if (entityHandle) {
267
+ this.beginEntityResizeDrag(entityHandle);
268
+ event?.preventDefault?.();
269
+ return;
270
+ }
261
271
  const hit = this.hitTest(px, py);
262
272
  const modifiers = {
263
273
  shift: !!event?.shiftKey,
@@ -321,6 +331,13 @@ export class EditorOverlayScene extends Phaser.Scene {
321
331
  this.updateTilemapResizeDrag(px, py);
322
332
  return;
323
333
  }
334
+ // Rect-entity resize drag in flight — live-applies setSize + center to
335
+ // the GameObject per move (no ghost; Rectangle.setSize is cheap).
336
+ if (getEntityResize(this.game)) {
337
+ const { x: px, y: py } = this.pointerCoords(pointer);
338
+ this.updateEntityResizeDrag(px, py);
339
+ return;
340
+ }
324
341
  // Slice 6 Phase B: continue an in-flight tilemap stroke (brush/eraser
325
342
  // drag) OR rect drag. handleTilemapPaintMove already branches on
326
343
  // which one is active internally — we just need to route to it
@@ -377,6 +394,12 @@ export class EditorOverlayScene extends Phaser.Scene {
377
394
  this.commitTilemapResize();
378
395
  return;
379
396
  }
397
+ // Rect-entity resize drag — GO is already at the final size (live-applied
398
+ // per move); post `entityResized` so the host records one undo command.
399
+ if (getEntityResize(this.game)) {
400
+ this.commitEntityResize();
401
+ return;
402
+ }
380
403
  // Slice 6 Phase B: end an in-flight tilemap stroke (brush/eraser) OR
381
404
  // rect drag. Both compose a single TilemapEditOp + post `tilemapEdited`
382
405
  // so undo reverts the whole stroke/rect in one step.
@@ -1151,6 +1174,13 @@ export class EditorOverlayScene extends Phaser.Scene {
1151
1174
  const info = this.tilemapHandlePositions();
1152
1175
  if (!info)
1153
1176
  return null;
1177
+ return this.hitTestResizeHandlesAt(worldX, worldY, info);
1178
+ }
1179
+ /**
1180
+ * Shared handle hit-test body — used by both the tilemap and the
1181
+ * rect-entity resize affordances (same 8-handle geometry).
1182
+ */
1183
+ hitTestResizeHandlesAt(worldX, worldY, info) {
1154
1184
  const cam = this.findActiveEditorCamera();
1155
1185
  const zoom = cam?.zoom ?? 1;
1156
1186
  const halfHandleWorld = 7 / zoom; // 14px square = 7px half-side
@@ -1202,13 +1232,22 @@ export class EditorOverlayScene extends Phaser.Scene {
1202
1232
  return;
1203
1233
  let handle = null;
1204
1234
  const inDrag = getTilemapResize(this.game);
1235
+ const inEntityDrag = getEntityResize(this.game);
1205
1236
  if (inDrag) {
1206
1237
  handle = inDrag.handle;
1207
1238
  }
1239
+ else if (inEntityDrag) {
1240
+ handle = inEntityDrag.handle;
1241
+ }
1208
1242
  else if (isTilemapPaintMode(this.game)) {
1209
1243
  const { x, y } = this.pointerCoords(pointer);
1210
1244
  handle = this.hitTestTilemapResizeHandle(x, y);
1211
1245
  }
1246
+ else {
1247
+ // Rect-entity handles — hover feedback whenever a rect is selected.
1248
+ const { x, y } = this.pointerCoords(pointer);
1249
+ handle = this.hitTestEntityResizeHandle(x, y);
1250
+ }
1212
1251
  if (!handle) {
1213
1252
  // Only reset when WE set it earlier; don't clobber pan-grab or other
1214
1253
  // cursor states future features might add.
@@ -1350,6 +1389,198 @@ export class EditorOverlayScene extends Phaser.Scene {
1350
1389
  window.parent.postMessage(message, '*');
1351
1390
  }
1352
1391
  }
1392
+ // --- Rect-entity resize-handle drag (2026-06-10) --------------------------
1393
+ //
1394
+ // Same 8-handle affordance as the tilemap resize above, generalized to
1395
+ // rect entities: handles render whenever a rect entity is selected (no
1396
+ // paint-mode gate — rects have no paint mode), sizes are pixel-based
1397
+ // (no cell snapping), and the live GameObject is mutated during the drag
1398
+ // because Rectangle.setSize is cheap (tilemap resize rebuilds layers, so
1399
+ // it previews with a ghost rect instead). Commit posts `entityResized`
1400
+ // so the host records ONE modify-entity command — undo/redo + Cmd+S
1401
+ // persistence ride the existing flat-patch path (width/height at the
1402
+ // patch root, center shift in transform.x/y).
1403
+ /**
1404
+ * Selected rect entity's bounds + 8 handle positions in world coords.
1405
+ * Returns null when the selection isn't a rect, in HUD mode, or when the
1406
+ * rect is rotated (axis-aligned handle math would shear a rotated rect —
1407
+ * v1 simply hides the handles there).
1408
+ */
1409
+ entityResizeHandlePositions() {
1410
+ if (getEditorMode(this.game) === 'hud')
1411
+ return null;
1412
+ const selectedId = getSelection(this.game);
1413
+ if (!selectedId)
1414
+ return null;
1415
+ const registry = this.findEntityRegistry();
1416
+ if (!registry)
1417
+ return null;
1418
+ const go = registry.byId(selectedId);
1419
+ if (!go || go.getData('entityKind') !== 'rect')
1420
+ return null;
1421
+ const rect = go;
1422
+ if (rect.rotation)
1423
+ return null;
1424
+ const pxW = rect.displayWidth ?? 0;
1425
+ const pxH = rect.displayHeight ?? 0;
1426
+ if (pxW <= 0 || pxH <= 0)
1427
+ return null;
1428
+ const cx = rect.x;
1429
+ const cy = rect.y;
1430
+ const left = cx - pxW / 2;
1431
+ const right = cx + pxW / 2;
1432
+ const top = cy - pxH / 2;
1433
+ const bottom = cy + pxH / 2;
1434
+ return {
1435
+ entityId: selectedId,
1436
+ center: { x: cx, y: cy },
1437
+ pxW,
1438
+ pxH,
1439
+ handles: [
1440
+ { id: 'nw', x: left, y: top },
1441
+ { id: 'n', x: cx, y: top },
1442
+ { id: 'ne', x: right, y: top },
1443
+ { id: 'e', x: right, y: cy },
1444
+ { id: 'se', x: right, y: bottom },
1445
+ { id: 's', x: cx, y: bottom },
1446
+ { id: 'sw', x: left, y: bottom },
1447
+ { id: 'w', x: left, y: cy },
1448
+ ],
1449
+ };
1450
+ }
1451
+ hitTestEntityResizeHandle(worldX, worldY) {
1452
+ const info = this.entityResizeHandlePositions();
1453
+ if (!info)
1454
+ return null;
1455
+ return this.hitTestResizeHandlesAt(worldX, worldY, info);
1456
+ }
1457
+ beginEntityResizeDrag(handle) {
1458
+ const info = this.entityResizeHandlePositions();
1459
+ if (!info)
1460
+ return;
1461
+ beginEntityResize(this.game, {
1462
+ entityId: info.entityId,
1463
+ handle,
1464
+ startSize: { width: info.pxW, height: info.pxH },
1465
+ startCenter: { x: info.center.x, y: info.center.y },
1466
+ previewWidth: info.pxW,
1467
+ previewHeight: info.pxH,
1468
+ previewCenter: { x: info.center.x, y: info.center.y },
1469
+ });
1470
+ }
1471
+ /**
1472
+ * Update the in-flight rect resize from the cursor position. Edges the
1473
+ * handle owns follow the cursor (snapped to whole pixels); opposite edges
1474
+ * stay anchored. Applies the result to the live GameObject immediately.
1475
+ */
1476
+ updateEntityResizeDrag(worldX, worldY) {
1477
+ const resize = getEntityResize(this.game);
1478
+ if (!resize)
1479
+ return;
1480
+ const MIN_SIZE = 2; // px — keeps the rect grabbable
1481
+ const { handle, startSize, startCenter } = resize;
1482
+ let newLeft = startCenter.x - startSize.width / 2;
1483
+ let newRight = startCenter.x + startSize.width / 2;
1484
+ let newTop = startCenter.y - startSize.height / 2;
1485
+ let newBottom = startCenter.y + startSize.height / 2;
1486
+ const movesLeft = handle === 'nw' || handle === 'w' || handle === 'sw';
1487
+ const movesRight = handle === 'ne' || handle === 'e' || handle === 'se';
1488
+ const movesTop = handle === 'nw' || handle === 'n' || handle === 'ne';
1489
+ const movesBottom = handle === 'sw' || handle === 's' || handle === 'se';
1490
+ if (movesLeft)
1491
+ newLeft = Math.min(Math.round(worldX), newRight - MIN_SIZE);
1492
+ if (movesRight)
1493
+ newRight = Math.max(Math.round(worldX), newLeft + MIN_SIZE);
1494
+ if (movesTop)
1495
+ newTop = Math.min(Math.round(worldY), newBottom - MIN_SIZE);
1496
+ if (movesBottom)
1497
+ newBottom = Math.max(Math.round(worldY), newTop + MIN_SIZE);
1498
+ const previewWidth = newRight - newLeft;
1499
+ const previewHeight = newBottom - newTop;
1500
+ const previewCenter = {
1501
+ x: (newLeft + newRight) / 2,
1502
+ y: (newTop + newBottom) / 2,
1503
+ };
1504
+ updateEntityResize(this.game, { previewWidth, previewHeight, previewCenter });
1505
+ this.applyEntityResizePreview(resize.entityId, previewWidth, previewHeight, previewCenter);
1506
+ }
1507
+ /** Mutate the live GameObject to the preview size + center (display px). */
1508
+ applyEntityResizePreview(entityId, displayW, displayH, center) {
1509
+ const registry = this.findEntityRegistry();
1510
+ const go = registry?.byId(entityId);
1511
+ if (!go)
1512
+ return;
1513
+ // setSize takes BASE dims; divide the display size by the transform
1514
+ // scale (palette rects spawn at scale 1, but be correct regardless).
1515
+ const sx = go.scaleX || 1;
1516
+ const sy = go.scaleY || 1;
1517
+ go.setSize?.(displayW / sx, displayH / sy);
1518
+ go.x = center.x;
1519
+ go.y = center.y;
1520
+ }
1521
+ /**
1522
+ * Commit the rect resize — the GO is already at its final size (applied
1523
+ * live per pointermove); post `entityResized` with BASE dims (entity-
1524
+ * record units) so the host records the modify-entity command. Undo
1525
+ * replays the `before` patch through the normal applyEdit path.
1526
+ */
1527
+ commitEntityResize() {
1528
+ const resize = endEntityResize(this.game);
1529
+ if (!resize)
1530
+ return;
1531
+ const { entityId, startSize, startCenter, previewWidth, previewHeight, previewCenter } = resize;
1532
+ if (previewWidth === startSize.width &&
1533
+ previewHeight === startSize.height &&
1534
+ previewCenter.x === startCenter.x &&
1535
+ previewCenter.y === startCenter.y) {
1536
+ return;
1537
+ }
1538
+ const registry = this.findEntityRegistry();
1539
+ const go = registry?.byId(entityId);
1540
+ const sx = go?.scaleX || 1;
1541
+ const sy = go?.scaleY || 1;
1542
+ const message = {
1543
+ type: 'umicat:editor:entityResized',
1544
+ entityId,
1545
+ before: {
1546
+ x: startCenter.x,
1547
+ y: startCenter.y,
1548
+ width: startSize.width / sx,
1549
+ height: startSize.height / sy,
1550
+ },
1551
+ after: {
1552
+ x: previewCenter.x,
1553
+ y: previewCenter.y,
1554
+ width: previewWidth / sx,
1555
+ height: previewHeight / sy,
1556
+ },
1557
+ };
1558
+ if (typeof window !== 'undefined' && window.parent) {
1559
+ window.parent.postMessage(message, '*');
1560
+ }
1561
+ // The selection rect changed size — refresh the host's anchor rect.
1562
+ postSelectionRect(this.game);
1563
+ }
1564
+ /**
1565
+ * Render the 8 resize handles around the selected rect entity. No bounds
1566
+ * outline (the blue selection rect already draws it) and no ghost rect
1567
+ * (the live GO resizes in place during the drag).
1568
+ */
1569
+ drawEntityResizeOverlay() {
1570
+ const info = this.entityResizeHandlePositions();
1571
+ if (!info)
1572
+ return;
1573
+ const cam = this.findActiveEditorCamera();
1574
+ const zoom = cam?.zoom ?? 1;
1575
+ const handleSize = 14 / zoom; // 14px on screen regardless of zoom
1576
+ const half = handleSize / 2;
1577
+ for (const h of info.handles) {
1578
+ this.graphics.fillStyle(0xffffff, 1);
1579
+ this.graphics.fillRect(h.x - half, h.y - half, handleSize, handleSize);
1580
+ this.graphics.fillStyle(SELECTION_COLOR, 1);
1581
+ this.graphics.fillRect(h.x - half + 1.5 / zoom, h.y - half + 1.5 / zoom, handleSize - 3 / zoom, handleSize - 3 / zoom);
1582
+ }
1583
+ }
1353
1584
  update() {
1354
1585
  this.graphics.clear();
1355
1586
  // Slice 6 Phase B (fix): sync tilemap layer world positions to their
@@ -1473,6 +1704,9 @@ export class EditorOverlayScene extends Phaser.Scene {
1473
1704
  // host Inspector). Ghost rect preview shows the new bounds during
1474
1705
  // an active resize drag.
1475
1706
  this.drawTilemapResizeOverlay();
1707
+ // Rect-entity resize handles — render whenever a rect entity is
1708
+ // selected (2026-06-10).
1709
+ this.drawEntityResizeOverlay();
1476
1710
  }
1477
1711
  /**
1478
1712
  * Render the 8 resize handles (when paint mode is active) and the
@@ -177,6 +177,32 @@ interface EditorStateShape {
177
177
  y: number;
178
178
  };
179
179
  } | null;
180
+ /**
181
+ * Rect-entity resize-handle drag in flight (2026-06-10). Same handle
182
+ * mechanics as tilemapResize but pixel-based (no cell snapping) and the
183
+ * live GameObject is mutated during the drag — Rectangle.setSize is cheap,
184
+ * unlike tilemap layer rebuilds, so the user sees the real rect resize
185
+ * rather than a ghost preview. All sizes are DISPLAY px (scale applied);
186
+ * the commit converts back to base dims before posting `entityResized`.
187
+ */
188
+ entityResize: {
189
+ entityId: string;
190
+ handle: TilemapResizeHandle;
191
+ startSize: {
192
+ width: number;
193
+ height: number;
194
+ };
195
+ startCenter: {
196
+ x: number;
197
+ y: number;
198
+ };
199
+ previewWidth: number;
200
+ previewHeight: number;
201
+ previewCenter: {
202
+ x: number;
203
+ y: number;
204
+ };
205
+ } | null;
180
206
  }
181
207
  /**
182
208
  * Tilemap resize handle identity. 4 corners + 4 edge midpoints. Each
@@ -248,4 +274,8 @@ export declare function getTilemapResize(game: AnyObject): EditorStateShape['til
248
274
  export declare function beginTilemapResize(game: AnyObject, resize: NonNullable<EditorStateShape['tilemapResize']>): void;
249
275
  export declare function updateTilemapResize(game: AnyObject, patch: Partial<Pick<NonNullable<EditorStateShape['tilemapResize']>, 'previewWidth' | 'previewHeight' | 'previewCenter'>>): void;
250
276
  export declare function endTilemapResize(game: AnyObject): EditorStateShape['tilemapResize'];
277
+ export declare function getEntityResize(game: AnyObject): EditorStateShape['entityResize'];
278
+ export declare function beginEntityResize(game: AnyObject, resize: NonNullable<EditorStateShape['entityResize']>): void;
279
+ export declare function updateEntityResize(game: AnyObject, patch: Partial<Pick<NonNullable<EditorStateShape['entityResize']>, 'previewWidth' | 'previewHeight' | 'previewCenter'>>): void;
280
+ export declare function endEntityResize(game: AnyObject): EditorStateShape['entityResize'];
251
281
  export {};
@@ -31,6 +31,7 @@ export function getEditorState(game) {
31
31
  tilemapStroke: null,
32
32
  tilemapRect: null,
33
33
  tilemapResize: null,
34
+ entityResize: null,
34
35
  };
35
36
  b[KEY] = fresh;
36
37
  return fresh;
@@ -195,3 +196,21 @@ export function endTilemapResize(game) {
195
196
  getEditorState(game).tilemapResize = null;
196
197
  return r;
197
198
  }
199
+ // --- Rect-entity resize-handle drag (2026-06-10) --------------------------
200
+ export function getEntityResize(game) {
201
+ return getEditorState(game).entityResize;
202
+ }
203
+ export function beginEntityResize(game, resize) {
204
+ getEditorState(game).entityResize = resize;
205
+ }
206
+ export function updateEntityResize(game, patch) {
207
+ const r = getEditorState(game).entityResize;
208
+ if (!r)
209
+ return;
210
+ Object.assign(r, patch);
211
+ }
212
+ export function endEntityResize(game) {
213
+ const r = getEditorState(game).entityResize;
214
+ getEditorState(game).entityResize = null;
215
+ return r;
216
+ }
@@ -638,6 +638,36 @@ export interface EditorDragEndMessage {
638
638
  y: number;
639
639
  };
640
640
  }
641
+ /**
642
+ * Rect-entity resize-handle drag completed (2026-06-10).
643
+ *
644
+ * Selected rect entities get the same 8-handle (4 corners + 4 edges)
645
+ * resize affordance as tilemaps. The SDK applies the new size + center to
646
+ * the live GameObject during the drag (Rectangle.setSize is cheap); on
647
+ * pointer-up it posts this message so the host records ONE modify-entity
648
+ * command for undo + flush — mirrors how `dragEnd` works for moves.
649
+ *
650
+ * `width` / `height` are the entity record's BASE dims (display px divided
651
+ * by transform scale), i.e. exactly what belongs in `RectEntity.width/
652
+ * height`. `x` / `y` are the entity transform's center position — it shifts
653
+ * during a resize because the opposite edge stays anchored.
654
+ */
655
+ export interface EditorEntityResizedMessage {
656
+ type: 'umicat:editor:entityResized';
657
+ entityId: string;
658
+ before: {
659
+ x: number;
660
+ y: number;
661
+ width: number;
662
+ height: number;
663
+ };
664
+ after: {
665
+ x: number;
666
+ y: number;
667
+ width: number;
668
+ height: number;
669
+ };
670
+ }
641
671
  /**
642
672
  * Editor keyboard shortcut intercepted inside the iframe (user clicked the
643
673
  * canvas, so focus is on the iframe and the host window doesn't see the
@@ -746,7 +776,7 @@ export interface EditorTilemapTilePickedMessage {
746
776
  layerId: string;
747
777
  tileIndex: number | null;
748
778
  }
749
- export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage | EditorShortcutMessage | EditorSelectionRectMessage | EditorRulesLoadedMessage | EditorCameraStateMessage | EditorTilemapEditedMessage | EditorTilemapTilePickedMessage;
779
+ export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage | EditorEntityResizedMessage | EditorShortcutMessage | EditorSelectionRectMessage | EditorRulesLoadedMessage | EditorCameraStateMessage | EditorTilemapEditedMessage | EditorTilemapTilePickedMessage;
750
780
  export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list' | 'gameData.get' | 'gameData.set' | 'gameData.delete' | 'gameData.list' | 'realtime.getToken';
751
781
  export interface SavesGetParams {
752
782
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Umicat Phaser 3 SDK — game infrastructure for the Umicat platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",