@umicat/phaser-sdk 1.0.0 → 1.0.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.
- package/SDK-GUIDE.md +5 -4
- package/dist/core/UmicatGame.js +59 -0
- package/dist/core/phaser-global.d.ts +1 -0
- package/dist/core/phaser-global.js +10 -0
- package/dist/editor/EditorBridge.d.ts +1 -0
- package/dist/editor/EditorBridge.js +1 -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
package/SDK-GUIDE.md
CHANGED
|
@@ -626,10 +626,11 @@ class GameScene extends Phaser.Scene {
|
|
|
626
626
|
// ...wire physics + input on `this.player`...
|
|
627
627
|
}
|
|
628
628
|
update() {
|
|
629
|
-
// Safe:
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
629
|
+
// Safe: an async create() suspends update() until it resolves. The SDK
|
|
630
|
+
// gates this in createUmicatGame (since 1.0.1) for EVERY registered
|
|
631
|
+
// scene, so any class field assigned after ANY `await` in create() —
|
|
632
|
+
// loadWorldScene, saves.get(), umicatReady, a fetch — is defined by the
|
|
633
|
+
// first time update() ticks. No `if (!this.player) return;` guard needed.
|
|
633
634
|
this.player.setVelocityX(0);
|
|
634
635
|
}
|
|
635
636
|
}
|
package/dist/core/UmicatGame.js
CHANGED
|
@@ -6,11 +6,70 @@ import { setupEditorModeListener } from '../scene/EditorMode.js';
|
|
|
6
6
|
import { ORIENTATION_DIMENSIONS } from '../orientation.js';
|
|
7
7
|
import { setRenderScriptRegistry, } from '../scene/renderScripts.js';
|
|
8
8
|
import { UmicatHudScene } from '../scene/HudRuntime.js';
|
|
9
|
+
/**
|
|
10
|
+
* Make a Promise-returning `create()` safe against Phaser's update loop.
|
|
11
|
+
*
|
|
12
|
+
* Phaser does NOT await an async `create()`. The moment `create()` suspends
|
|
13
|
+
* at its first `await`, Phaser flips the scene to RUNNING and starts calling
|
|
14
|
+
* `update()` every frame — so any object the game builds *after* that await
|
|
15
|
+
* (a HUD text, the player sprite) is `undefined` for those frames. An
|
|
16
|
+
* `update()` that reads it throws on every frame and the game appears frozen
|
|
17
|
+
* on the loading screen. This is a very easy trap to fall into: the docs
|
|
18
|
+
* pattern `const hi = await saves.get('highScore')` at the top of `create()`
|
|
19
|
+
* is enough to trigger it.
|
|
20
|
+
*
|
|
21
|
+
* We give async `create()` the same guarantee `preload()` already has —
|
|
22
|
+
* "update() does not run until setup is finished" — by wrapping each scene so
|
|
23
|
+
* `update()` is a no-op until the `create()` promise settles. Synchronous
|
|
24
|
+
* `create()` is untouched (it never returns a thenable, so the gate is never
|
|
25
|
+
* armed). Re-runs cleanly across `scene.restart()` since the flag lives on
|
|
26
|
+
* the scene instance and is re-armed each create().
|
|
27
|
+
*/
|
|
28
|
+
function guardAsyncCreate(SceneClass) {
|
|
29
|
+
const proto = SceneClass.prototype;
|
|
30
|
+
// Patch the prototype at most once, even under HMR / repeated factory calls.
|
|
31
|
+
if (proto.__umicatGuarded)
|
|
32
|
+
return;
|
|
33
|
+
const origCreate = proto.create;
|
|
34
|
+
if (typeof origCreate !== 'function')
|
|
35
|
+
return; // no create() → nothing to gate
|
|
36
|
+
proto.__umicatGuarded = true;
|
|
37
|
+
const PENDING = '__umicatCreatePending';
|
|
38
|
+
proto.create = function (...args) {
|
|
39
|
+
const ret = origCreate.apply(this, args);
|
|
40
|
+
if (ret && typeof ret.then === 'function') {
|
|
41
|
+
this[PENDING] = true;
|
|
42
|
+
Promise.resolve(ret)
|
|
43
|
+
.catch((e) => {
|
|
44
|
+
// Surface the failure instead of hanging update() forever.
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.error('[umicat] async create() rejected:', e);
|
|
47
|
+
})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
this[PENDING] = false;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return ret;
|
|
53
|
+
};
|
|
54
|
+
const origUpdate = proto.update;
|
|
55
|
+
if (typeof origUpdate === 'function') {
|
|
56
|
+
proto.update = function (...args) {
|
|
57
|
+
if (this[PENDING])
|
|
58
|
+
return undefined; // setup still running — skip this frame
|
|
59
|
+
return origUpdate.apply(this, args);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
9
63
|
/**
|
|
10
64
|
* Create an Umicat-enhanced Phaser game instance.
|
|
11
65
|
* Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
|
|
12
66
|
*/
|
|
13
67
|
export function createUmicatGame(options) {
|
|
68
|
+
// Harden every game scene against the async-create()/update() race before
|
|
69
|
+
// Phaser instantiates them (see guardAsyncCreate). The auto-registered
|
|
70
|
+
// UmicatHudScene is ours and uses a sync create(), so it's left alone.
|
|
71
|
+
for (const SceneClass of options.scenes)
|
|
72
|
+
guardAsyncCreate(SceneClass);
|
|
14
73
|
const { width, height } = 'orientation' in options && options.orientation
|
|
15
74
|
? ORIENTATION_DIMENSIONS[options.orientation]
|
|
16
75
|
: { width: options.width, height: options.height };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
// phaser3-rex-plugins (e.g. the NinePatch plugin) were written for Phaser 3's
|
|
3
|
+
// UMD build, which set a global `window.Phaser`. Their module bodies reference a
|
|
4
|
+
// bare `Phaser` at eval time (e.g. `Phaser.Utils.Objects.GetValue`). Phaser 4 is
|
|
5
|
+
// pure ESM and does NOT expose a global, so those plugins throw
|
|
6
|
+
// "Phaser is not defined". Expose Phaser on globalThis here.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT: this module must be imported BEFORE any `phaser3-rex-plugins/*`
|
|
9
|
+
// import so the global exists before the plugin's top-level code evaluates.
|
|
10
|
+
globalThis.Phaser = Phaser;
|
|
@@ -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
|
|
@@ -407,7 +407,7 @@ function postRulesSnapshot(game) {
|
|
|
407
407
|
// inside one RAF tick — Inspector spam (per-keystroke transform edits)
|
|
408
408
|
// would otherwise spam the postMessage channel.
|
|
409
409
|
const RECT_RAF_FLAG = '__unboxyEditorSelectionRectRafScheduled';
|
|
410
|
-
function postSelectionRect(game) {
|
|
410
|
+
export function postSelectionRect(game) {
|
|
411
411
|
const bag = game;
|
|
412
412
|
if (bag[RECT_RAF_FLAG])
|
|
413
413
|
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;
|