@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.
- package/dist/editor/EditorBridge.d.ts +1 -0
- package/dist/editor/EditorBridge.js +50 -1
- package/dist/editor/EditorOverlayScene.d.ts +35 -0
- package/dist/editor/EditorOverlayScene.js +236 -2
- package/dist/editor/EditorState.d.ts +30 -0
- package/dist/editor/EditorState.js +19 -0
- package/dist/protocol.d.ts +31 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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;
|