@combos-fun/plugin-development-tool 0.0.53 → 0.0.54
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/agent-skill.md +3 -1
- package/dist/plugin-development-tool.cjs.js +143 -19
- package/dist/plugin-development-tool.cjs.js.map +1 -1
- package/dist/plugin-development-tool.cjs.prod.js +1 -1
- package/dist/plugin-development-tool.d.ts +7 -1
- package/dist/plugin-development-tool.esm.js +143 -19
- package/dist/plugin-development-tool.esm.js.map +1 -1
- package/package.json +3 -3
package/agent-skill.md
CHANGED
|
@@ -60,7 +60,7 @@ Do not add `CombosDevelopmentToolTarget` from game code. Opt out of a construct
|
|
|
60
60
|
|
|
61
61
|
## Runtime behaviour
|
|
62
62
|
|
|
63
|
-
The System starts disabled. In pick mode it shows a full-canvas overlay (`pointer-events: auto`) so game `Event` / `Event3D` do not receive the tap. Targets are hit-tested from engine transforms projected to overlay CSS pixels. Target additions/removals are observed automatically; `requestSceneRescan()` is for parent-driven structural refreshes.
|
|
63
|
+
The System starts disabled. In pick mode it shows a full-canvas overlay (`pointer-events: auto`) so game `Event` / `Event3D` do not receive the tap, and it also listens on `window` capture so a DOM HUD above the canvas cannot swallow the tap. Targets are hit-tested from engine transforms projected to overlay CSS pixels. Overlapping hits prefer content: hollow / invisible / low-alpha nodes and rects covering ≥75% of the overlay lose to a smaller target underneath; two covering layers pick the earlier scene-walk item. Target additions/removals are observed automatically; `requestSceneRescan()` is for parent-driven structural refreshes.
|
|
64
64
|
|
|
65
65
|
2D bounds come from `Transform` size / hierarchy (position, origin, anchor, scale, rotation). Set `transform.size` when the visual does not match a zero size. 3D bounds project `Transform3D` world position through the active `Renderer3DSystem` camera; a mesh bounding box is used when the Three object is available, otherwise a screen-space pad around the projected point.
|
|
66
66
|
|
|
@@ -92,6 +92,8 @@ Snapshots expose described schema-backed `fieldDescriptors`, not raw fields. For
|
|
|
92
92
|
|
|
93
93
|
- 2D picking needs a positive `transform.size` to get a usable hit rect.
|
|
94
94
|
- 3D picking needs `Renderer3DSystem` (for the camera) at runtime; there is no package dependency.
|
|
95
|
+
- Pick ranking is not pixel-perfect: a fullscreen `Img` with `alpha: 1` still counts as drawable even if the PNG is mostly transparent.
|
|
96
|
+
- A covering overlay is still selected when it is the only hit under the pointer.
|
|
95
97
|
- Parent messages require an iframe (`window.parent !== window`) and an allowed origin.
|
|
96
98
|
- Structural replacement needs `refresh`; ordinary Target add/remove does not.
|
|
97
99
|
- Persistence fails if the game build did not run `combosDevelopmentTargetPlugin`.
|
|
@@ -255,14 +255,17 @@ function mergeAllowedMessageOrigins(extra) {
|
|
|
255
255
|
|
|
256
256
|
const OVERLAY_Z_INDEX = '2147483000';
|
|
257
257
|
const OVERLAY_ATTR = 'data-combos-development-tool-overlay';
|
|
258
|
-
function ensureParent(
|
|
259
|
-
const parent =
|
|
258
|
+
function ensureParent(_gameCanvas, overlay) {
|
|
259
|
+
const parent = document.body;
|
|
260
|
+
if (!parent)
|
|
261
|
+
return;
|
|
260
262
|
if (overlay.parentElement !== parent) {
|
|
261
263
|
parent.appendChild(overlay);
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
/**
|
|
265
|
-
* Transparent HTML canvas stacked on the game canvas.
|
|
267
|
+
* Transparent HTML canvas stacked on the game canvas and mounted on `document.body`
|
|
268
|
+
* so a transformed canvas parent cannot trap `position: fixed` under a DOM HUD.
|
|
266
269
|
* Pick mode sets `pointer-events: auto` so game Event / Event3D do not receive the tap.
|
|
267
270
|
*/
|
|
268
271
|
function createOverlayCanvas() {
|
|
@@ -990,16 +993,100 @@ function projectGameObjectToScreen(go, opts) {
|
|
|
990
993
|
function rectContains(rect, x, y) {
|
|
991
994
|
return x >= rect.x && y >= rect.y && x <= rect.x + rect.width && y <= rect.y + rect.height;
|
|
992
995
|
}
|
|
993
|
-
/**
|
|
994
|
-
|
|
996
|
+
/** Fraction of the overlay a rect must cover to count as a fullscreen pass-through layer. */
|
|
997
|
+
const COVERING_OVERLAY_RATIO = 0.75;
|
|
998
|
+
/** `Render.alpha` / `Render3D.opacity` at or below this is treated as see-through. */
|
|
999
|
+
const LOW_ALPHA_PASSTHROUGH = 0.35;
|
|
1000
|
+
/**
|
|
1001
|
+
* Known drawable component names. Duck-typed so this package does not import
|
|
1002
|
+
* renderer plugins. Unknown custom visuals are treated as non-drawable only for
|
|
1003
|
+
* pass-through ranking; they remain pickable when they are the only hit.
|
|
1004
|
+
*/
|
|
1005
|
+
const KNOWN_VISUAL_COMPONENT_NAMES = [
|
|
1006
|
+
'Img',
|
|
1007
|
+
'Sprite',
|
|
1008
|
+
'TilingSprite',
|
|
1009
|
+
'NinePatch',
|
|
1010
|
+
'Text',
|
|
1011
|
+
'Graphics',
|
|
1012
|
+
'SpriteAnimation',
|
|
1013
|
+
'Img3D',
|
|
1014
|
+
'Sprite3D',
|
|
1015
|
+
'TilingSprite3D',
|
|
1016
|
+
'Text3D',
|
|
1017
|
+
'Graphics3D',
|
|
1018
|
+
'Model3D',
|
|
1019
|
+
'SpriteAnimation3D',
|
|
1020
|
+
];
|
|
1021
|
+
function rectOverlapArea(a, b) {
|
|
1022
|
+
const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
1023
|
+
const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
1024
|
+
if (width <= 0 || height <= 0)
|
|
1025
|
+
return 0;
|
|
1026
|
+
return width * height;
|
|
1027
|
+
}
|
|
1028
|
+
function isCoveringOverlay(rect, viewport, ratio = COVERING_OVERLAY_RATIO) {
|
|
1029
|
+
if (!viewport || viewport.width <= 0 || viewport.height <= 0) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
const viewportArea = viewport.width * viewport.height;
|
|
1033
|
+
if (viewportArea <= 0)
|
|
1034
|
+
return false;
|
|
1035
|
+
return rectOverlapArea(rect, { x: 0, y: 0, width: viewport.width, height: viewport.height }) / viewportArea >= ratio;
|
|
1036
|
+
}
|
|
1037
|
+
function hasKnownVisual(go) {
|
|
1038
|
+
return KNOWN_VISUAL_COMPONENT_NAMES.some(name => Boolean(go.getComponent(name)));
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Higher = more likely to click through to a target underneath.
|
|
1042
|
+
* Invisible / zero-alpha layers rank above hollow containers and low-alpha dimmers.
|
|
1043
|
+
*/
|
|
1044
|
+
function pickPassThrough(go) {
|
|
1045
|
+
const render = go.getComponent('Render');
|
|
1046
|
+
const render3d = go.getComponent('Render3D');
|
|
1047
|
+
const visible = render?.visible !== false && render3d?.visible !== false;
|
|
1048
|
+
const alpha = Math.min(typeof render?.alpha === 'number' && Number.isFinite(render.alpha) ? render.alpha : 1, typeof render3d?.opacity === 'number' && Number.isFinite(render3d.opacity) ? render3d.opacity : 1);
|
|
1049
|
+
let score = 0;
|
|
1050
|
+
if (!visible || alpha <= 0)
|
|
1051
|
+
score += 2;
|
|
1052
|
+
else if (alpha <= LOW_ALPHA_PASSTHROUGH)
|
|
1053
|
+
score += 1;
|
|
1054
|
+
if (!hasKnownVisual(go))
|
|
1055
|
+
score += 1;
|
|
1056
|
+
return score;
|
|
1057
|
+
}
|
|
1058
|
+
function isBetterHit(next, best) {
|
|
1059
|
+
if (next.score !== best.score)
|
|
1060
|
+
return next.score < best.score;
|
|
1061
|
+
if (next.covering !== best.covering)
|
|
1062
|
+
return !next.covering;
|
|
1063
|
+
if (next.covering) {
|
|
1064
|
+
return next.index < best.index;
|
|
1065
|
+
}
|
|
1066
|
+
if (next.area !== best.area)
|
|
1067
|
+
return next.area < best.area;
|
|
1068
|
+
return next.index > best.index;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Pick among overlapping targets:
|
|
1072
|
+
* - lower pass-through score wins (content over invisible / hollow / low-alpha);
|
|
1073
|
+
* - a covering fullscreen layer loses to any non-covering hit;
|
|
1074
|
+
* - among covering-only hits, the earlier scene-walk item wins (what's behind);
|
|
1075
|
+
* - among content hits, smallest rect wins, later items win ties (front-most).
|
|
1076
|
+
*/
|
|
1077
|
+
function hitTestRects(x, y, items, opts) {
|
|
995
1078
|
let best = null;
|
|
1079
|
+
const coveringRatio = opts?.coveringRatio ?? COVERING_OVERLAY_RATIO;
|
|
996
1080
|
for (let i = 0; i < items.length; i++) {
|
|
997
1081
|
const item = items[i];
|
|
998
1082
|
if (!rectContains(item.rect, x, y))
|
|
999
1083
|
continue;
|
|
1000
1084
|
const area = Math.max(item.rect.width, 0) * Math.max(item.rect.height, 0);
|
|
1001
|
-
|
|
1002
|
-
|
|
1085
|
+
const covering = isCoveringOverlay(item.rect, opts?.viewport, coveringRatio);
|
|
1086
|
+
const score = (item.passThrough ?? 0) + (covering ? 1 : 0);
|
|
1087
|
+
const next = { id: item.id, score, covering, area, index: i };
|
|
1088
|
+
if (!best || isBetterHit(next, best)) {
|
|
1089
|
+
best = next;
|
|
1003
1090
|
}
|
|
1004
1091
|
}
|
|
1005
1092
|
return best ? best.id : null;
|
|
@@ -1011,8 +1098,10 @@ const OUTLINE_COLOR = '#55ffaa';
|
|
|
1011
1098
|
/**
|
|
1012
1099
|
* **Off** at start. While enabled:
|
|
1013
1100
|
* - a transparent HTML canvas overlay captures pointer events (game Event / Event3D
|
|
1014
|
-
* do not receive the tap);
|
|
1101
|
+
* do not receive the tap); pick also listens on `window` capture so a mostly
|
|
1102
|
+
* transparent DOM HUD above the canvas cannot swallow the tap;
|
|
1015
1103
|
* - only nodes with {@link CombosDevelopmentToolTarget} are pickable;
|
|
1104
|
+
* - fullscreen / hollow / low-alpha layers lose to content underneath;
|
|
1016
1105
|
* - selection outline and markers are drawn with the Canvas 2D API (no renderer plugin).
|
|
1017
1106
|
*
|
|
1018
1107
|
* Turn on/off via:
|
|
@@ -1040,6 +1129,7 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1040
1129
|
this.overlay = null;
|
|
1041
1130
|
this.overlayCss = { width: 0, height: 0 };
|
|
1042
1131
|
this.pointerDownGoId = null;
|
|
1132
|
+
this.pickListening = false;
|
|
1043
1133
|
this.needsRescan = false;
|
|
1044
1134
|
/** Remembered mute when `SoundSystem` is not registered yet. */
|
|
1045
1135
|
this.mutedWithoutSoundSystem = false;
|
|
@@ -1105,10 +1195,11 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1105
1195
|
this.requestSceneRescan();
|
|
1106
1196
|
};
|
|
1107
1197
|
this.onPointerDown = (e) => {
|
|
1108
|
-
if (!this.enabled)
|
|
1198
|
+
if (!this.enabled || !this.isPickPointInGame(e))
|
|
1109
1199
|
return;
|
|
1110
1200
|
e.preventDefault();
|
|
1111
1201
|
e.stopPropagation();
|
|
1202
|
+
e.stopImmediatePropagation();
|
|
1112
1203
|
try {
|
|
1113
1204
|
this.overlay?.canvas.setPointerCapture(e.pointerId);
|
|
1114
1205
|
}
|
|
@@ -1121,8 +1212,13 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1121
1212
|
this.onPointerUp = (e) => {
|
|
1122
1213
|
if (!this.enabled)
|
|
1123
1214
|
return;
|
|
1215
|
+
if (!this.isPickPointInGame(e)) {
|
|
1216
|
+
this.pointerDownGoId = null;
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1124
1219
|
e.preventDefault();
|
|
1125
1220
|
e.stopPropagation();
|
|
1221
|
+
e.stopImmediatePropagation();
|
|
1126
1222
|
const hit = this.hitTestPointer(e);
|
|
1127
1223
|
if (hit && hit.id === this.pointerDownGoId) {
|
|
1128
1224
|
this.onSelect(hit, e);
|
|
@@ -1135,10 +1231,6 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1135
1231
|
this.postMessageOrigin = params?.postMessageOrigin ?? '*';
|
|
1136
1232
|
this.allowedMessageOrigins = mergeAllowedMessageOrigins(params?.allowedMessageOrigins);
|
|
1137
1233
|
this.overlay = createOverlayCanvas();
|
|
1138
|
-
if (this.overlay) {
|
|
1139
|
-
this.overlay.canvas.addEventListener('pointerdown', this.onPointerDown);
|
|
1140
|
-
this.overlay.canvas.addEventListener('pointerup', this.onPointerUp);
|
|
1141
|
-
}
|
|
1142
1234
|
if (typeof window !== 'undefined') {
|
|
1143
1235
|
window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, this.onWindowSetPickMode);
|
|
1144
1236
|
window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
|
|
@@ -1154,9 +1246,8 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1154
1246
|
}
|
|
1155
1247
|
}
|
|
1156
1248
|
onDestroy() {
|
|
1249
|
+
this.unbindPickListeners();
|
|
1157
1250
|
if (this.overlay) {
|
|
1158
|
-
this.overlay.canvas.removeEventListener('pointerdown', this.onPointerDown);
|
|
1159
|
-
this.overlay.canvas.removeEventListener('pointerup', this.onPointerUp);
|
|
1160
1251
|
this.overlay.destroy();
|
|
1161
1252
|
this.overlay = null;
|
|
1162
1253
|
}
|
|
@@ -1273,6 +1364,38 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1273
1364
|
const active = this.enabled || this.markerOverlayEnabled;
|
|
1274
1365
|
this.overlay?.setVisible(active);
|
|
1275
1366
|
this.overlay?.setPickEnabled(this.enabled);
|
|
1367
|
+
if (this.enabled) {
|
|
1368
|
+
this.bindPickListeners();
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
this.unbindPickListeners();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
bindPickListeners() {
|
|
1375
|
+
if (this.pickListening || typeof window === 'undefined')
|
|
1376
|
+
return;
|
|
1377
|
+
window.addEventListener('pointerdown', this.onPointerDown, true);
|
|
1378
|
+
window.addEventListener('pointerup', this.onPointerUp, true);
|
|
1379
|
+
this.pickListening = true;
|
|
1380
|
+
}
|
|
1381
|
+
unbindPickListeners() {
|
|
1382
|
+
if (!this.pickListening || typeof window === 'undefined')
|
|
1383
|
+
return;
|
|
1384
|
+
window.removeEventListener('pointerdown', this.onPointerDown, true);
|
|
1385
|
+
window.removeEventListener('pointerup', this.onPointerUp, true);
|
|
1386
|
+
this.pickListening = false;
|
|
1387
|
+
}
|
|
1388
|
+
isPickPointInGame(e) {
|
|
1389
|
+
const canvas = this.game.canvas ?? this.game.scene?.canvas ?? this.overlay?.canvas;
|
|
1390
|
+
if (!canvas)
|
|
1391
|
+
return false;
|
|
1392
|
+
const rect = canvas.getBoundingClientRect();
|
|
1393
|
+
return (rect.width > 0 &&
|
|
1394
|
+
rect.height > 0 &&
|
|
1395
|
+
e.clientX >= rect.left &&
|
|
1396
|
+
e.clientX <= rect.right &&
|
|
1397
|
+
e.clientY >= rect.top &&
|
|
1398
|
+
e.clientY <= rect.bottom);
|
|
1276
1399
|
}
|
|
1277
1400
|
collectGameObjects(go, out) {
|
|
1278
1401
|
out.push(go);
|
|
@@ -1312,13 +1435,14 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1312
1435
|
if (!go.getComponent(CombosDevelopmentToolTarget))
|
|
1313
1436
|
continue;
|
|
1314
1437
|
const rect = this.projectGo(go);
|
|
1438
|
+
const passThrough = pickPassThrough(go);
|
|
1315
1439
|
if (rect) {
|
|
1316
|
-
hits.push({ go, rect });
|
|
1440
|
+
hits.push({ go, rect, passThrough });
|
|
1317
1441
|
}
|
|
1318
1442
|
if (this.markerOverlayEnabled && resolveMarkerDef(go)) {
|
|
1319
1443
|
const markerRect = this.markerRectFor(go, rect);
|
|
1320
1444
|
if (markerRect) {
|
|
1321
|
-
hits.push({ go, rect: markerRect });
|
|
1445
|
+
hits.push({ go, rect: markerRect, passThrough });
|
|
1322
1446
|
}
|
|
1323
1447
|
}
|
|
1324
1448
|
}
|
|
@@ -1353,7 +1477,7 @@ let CombosDevelopmentToolSystem$1 = class CombosDevelopmentToolSystem extends en
|
|
|
1353
1477
|
hitTestPointer(e) {
|
|
1354
1478
|
const pt = this.overlayPoint(e);
|
|
1355
1479
|
const hits = this.collectOverlayHits();
|
|
1356
|
-
return hitTestRects(pt.x, pt.y, hits.map(h => ({ id: h.go, rect: h.rect })));
|
|
1480
|
+
return hitTestRects(pt.x, pt.y, hits.map(h => ({ id: h.go, rect: h.rect, passThrough: h.passThrough })), { viewport: this.overlayCss });
|
|
1357
1481
|
}
|
|
1358
1482
|
redrawOverlay() {
|
|
1359
1483
|
const overlay = this.overlay;
|
|
@@ -1572,7 +1696,7 @@ var CombosDevelopmentToolSystem = CombosDevelopmentToolSystem$1;
|
|
|
1572
1696
|
/** Auto-generated by scripts/build-package.mjs — do not edit. */
|
|
1573
1697
|
Object.assign(CombosDevelopmentToolSystem, {
|
|
1574
1698
|
packageName: "@combos-fun/plugin-development-tool",
|
|
1575
|
-
packageVersion: "0.0.
|
|
1699
|
+
packageVersion: "0.0.54",
|
|
1576
1700
|
});
|
|
1577
1701
|
|
|
1578
1702
|
function isInternalEditorGo(go) {
|