@combos-fun/plugin-development-tool 0.0.52 → 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 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(gameCanvas, overlay) {
259
- const parent = gameCanvas.parentElement ?? document.body;
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
- /** Smallest containing rect wins; later items win ties (front-most in walk order). */
994
- function hitTestRects(x, y, items) {
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
- if (!best || area < best.area || (area === best.area && i > best.index)) {
1002
- best = { id: item.id, area, index: i };
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.52",
1699
+ packageVersion: "0.0.54",
1576
1700
  });
1577
1701
 
1578
1702
  function isInternalEditorGo(go) {