@combos-fun/plugin-development-tool 0.0.48 → 0.0.49

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.
@@ -1,8 +1,5 @@
1
1
  import { __decorate } from 'tslib';
2
- import { Component, System, OBSERVER_TYPE, GameObject, decorators } from '@combos-fun/engine';
3
- import { RendererSystem } from '@combos-fun/plugin-renderer';
4
- import { Graphics } from '@combos-fun/plugin-renderer-graphics';
5
- import { Event, HIT_AREA_TYPE } from '@combos-fun/plugin-renderer-event';
2
+ import { Component, System, OBSERVER_TYPE, decorators } from '@combos-fun/engine';
6
3
  import { IDE_PROPERTY_METADATA } from '@combos-fun/inspector-decorator';
7
4
 
8
5
  /** Parent → iframe (also `window` CustomEvent / `game.emit`): toggle pick mode. Payload: `{ enabled: boolean }`. */
@@ -43,8 +40,7 @@ const COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS = 'combos-development-tool:
43
40
 
44
41
  /**
45
42
  * Marks a `GameObject` as selectable in editor pick mode. While the development tool is enabled,
46
- * only nodes carrying this component receive a pick `Event`; all other game `Event` components
47
- * are removed and cached until disable.
43
+ * only nodes carrying this component are hit-tested on the canvas overlay.
48
44
  */
49
45
  class CombosDevelopmentToolTarget extends Component {
50
46
  constructor() {
@@ -57,6 +53,69 @@ class CombosDevelopmentToolTarget extends Component {
57
53
  }
58
54
  }
59
55
 
56
+ function cssRgba(color, alpha = 1) {
57
+ const r = (color >> 16) & 0xff;
58
+ const g = (color >> 8) & 0xff;
59
+ const b = color & 0xff;
60
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
61
+ }
62
+ /** Pixi-v8-style path builder backed by `CanvasRenderingContext2D`. */
63
+ class CanvasMarkerGraphics {
64
+ constructor(ctx) {
65
+ this.ctx = ctx;
66
+ }
67
+ clear() {
68
+ this.ctx.beginPath();
69
+ }
70
+ roundRect(x, y, width, height, radius = 0) {
71
+ const ctx = this.ctx;
72
+ if (typeof ctx.roundRect === 'function') {
73
+ ctx.roundRect(x, y, width, height, radius);
74
+ return;
75
+ }
76
+ const r = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
77
+ ctx.moveTo(x + r, y);
78
+ ctx.lineTo(x + width - r, y);
79
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
80
+ ctx.lineTo(x + width, y + height - r);
81
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
82
+ ctx.lineTo(x + r, y + height);
83
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
84
+ ctx.lineTo(x, y + r);
85
+ ctx.quadraticCurveTo(x, y, x + r, y);
86
+ ctx.closePath();
87
+ }
88
+ rect(x, y, width, height) {
89
+ this.ctx.rect(x, y, width, height);
90
+ }
91
+ circle(x, y, radius) {
92
+ this.ctx.moveTo(x + radius, y);
93
+ this.ctx.arc(x, y, radius, 0, Math.PI * 2);
94
+ }
95
+ poly(points, close = true) {
96
+ if (points.length < 4)
97
+ return;
98
+ this.ctx.moveTo(points[0], points[1]);
99
+ for (let i = 2; i < points.length; i += 2) {
100
+ this.ctx.lineTo(points[i], points[i + 1]);
101
+ }
102
+ if (close) {
103
+ this.ctx.closePath();
104
+ }
105
+ }
106
+ fill(style) {
107
+ this.ctx.fillStyle = cssRgba(style.color, style.alpha ?? 1);
108
+ this.ctx.fill();
109
+ this.ctx.beginPath();
110
+ }
111
+ stroke(style) {
112
+ this.ctx.strokeStyle = cssRgba(style.color, style.alpha ?? 1);
113
+ this.ctx.lineWidth = style.width;
114
+ this.ctx.stroke();
115
+ this.ctx.beginPath();
116
+ }
117
+ }
118
+
60
119
  /**
61
120
  * Registered marker kinds, in priority order: a GameObject is marked by the
62
121
  * first registered component it owns. For now this is **`Sound` only**; the
@@ -66,7 +125,7 @@ class CombosDevelopmentToolTarget extends Component {
66
125
  const MARKER_COMPONENTS = [
67
126
  { componentName: 'Sound', badgeColor: 0x8b5cf6, glyph: 'speaker' },
68
127
  ];
69
- /** Fixed badge size in the owner's local space. */
128
+ /** Fixed badge size in overlay CSS pixels. */
70
129
  const MARKER_ICON_SIZE = 28;
71
130
  /**
72
131
  * First registered marker def whose component is present on `go`, else `null`.
@@ -110,7 +169,7 @@ function drawDotGlyph(g, s) {
110
169
  g.circle(s * 0.5, s * 0.5, s * 0.18);
111
170
  g.fill({ color: GLYPH_COLOR, alpha: 0.96 });
112
171
  }
113
- /** Render a full marker (badge + glyph) into `g`, sized to `size` (local units). */
172
+ /** Render a full marker (badge + glyph) into `g`, sized to `size` (overlay CSS pixels). */
114
173
  function drawMarkerIcon(g, def, size = MARKER_ICON_SIZE) {
115
174
  drawBadge(g, def, size);
116
175
  switch (def.glyph) {
@@ -192,6 +251,85 @@ function mergeAllowedMessageOrigins(extra) {
192
251
  return result;
193
252
  }
194
253
 
254
+ const OVERLAY_Z_INDEX = '2147483000';
255
+ const OVERLAY_ATTR = 'data-combos-development-tool-overlay';
256
+ function ensureParent(gameCanvas, overlay) {
257
+ const parent = gameCanvas.parentElement ?? document.body;
258
+ if (overlay.parentElement !== parent) {
259
+ parent.appendChild(overlay);
260
+ }
261
+ }
262
+ /**
263
+ * Transparent HTML canvas stacked on the game canvas.
264
+ * Pick mode sets `pointer-events: auto` so game Event / Event3D do not receive the tap.
265
+ */
266
+ function createOverlayCanvas() {
267
+ if (typeof document === 'undefined') {
268
+ return null;
269
+ }
270
+ const canvas = document.createElement('canvas');
271
+ canvas.setAttribute(OVERLAY_ATTR, 'true');
272
+ canvas.style.position = 'fixed';
273
+ canvas.style.left = '0';
274
+ canvas.style.top = '0';
275
+ canvas.style.width = '0';
276
+ canvas.style.height = '0';
277
+ canvas.style.margin = '0';
278
+ canvas.style.padding = '0';
279
+ canvas.style.border = '0';
280
+ canvas.style.background = 'transparent';
281
+ canvas.style.pointerEvents = 'none';
282
+ canvas.style.touchAction = 'none';
283
+ canvas.style.userSelect = 'none';
284
+ canvas.style.zIndex = OVERLAY_Z_INDEX;
285
+ canvas.style.display = 'none';
286
+ const ctx = canvas.getContext('2d');
287
+ if (!ctx) {
288
+ return null;
289
+ }
290
+ let pickEnabled = false;
291
+ let visible = false;
292
+ const sync = (gameCanvas) => {
293
+ ensureParent(gameCanvas, canvas);
294
+ const rect = gameCanvas.getBoundingClientRect();
295
+ if (rect.width <= 0 || rect.height <= 0) {
296
+ canvas.style.display = 'none';
297
+ return null;
298
+ }
299
+ canvas.style.display = visible ? 'block' : 'none';
300
+ canvas.style.left = `${rect.left}px`;
301
+ canvas.style.top = `${rect.top}px`;
302
+ canvas.style.width = `${rect.width}px`;
303
+ canvas.style.height = `${rect.height}px`;
304
+ const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
305
+ const pixelW = Math.max(1, Math.round(rect.width * dpr));
306
+ const pixelH = Math.max(1, Math.round(rect.height * dpr));
307
+ if (canvas.width !== pixelW || canvas.height !== pixelH) {
308
+ canvas.width = pixelW;
309
+ canvas.height = pixelH;
310
+ }
311
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
312
+ return { width: rect.width, height: rect.height };
313
+ };
314
+ return {
315
+ canvas,
316
+ ctx,
317
+ sync,
318
+ setPickEnabled(on) {
319
+ pickEnabled = on;
320
+ canvas.style.pointerEvents = pickEnabled ? 'auto' : 'none';
321
+ canvas.style.cursor = pickEnabled ? 'crosshair' : 'default';
322
+ },
323
+ setVisible(on) {
324
+ visible = on;
325
+ canvas.style.display = on ? 'block' : 'none';
326
+ },
327
+ destroy() {
328
+ canvas.remove();
329
+ },
330
+ };
331
+ }
332
+
195
333
  function isSceneSourceAnchor(v) {
196
334
  return (!!v &&
197
335
  typeof v === 'object' &&
@@ -585,12 +723,295 @@ function isClearSelectionMessage(data) {
585
723
  return data.type === COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION;
586
724
  }
587
725
 
726
+ /** Screen-space pad used when a target has no positive size / projected extent. */
727
+ const DEFAULT_POINT_EXTENT = 32;
728
+ function identityMat() {
729
+ return { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
730
+ }
731
+ function multiplyMat(p, l) {
732
+ return {
733
+ a: p.a * l.a + p.c * l.b,
734
+ b: p.b * l.a + p.d * l.b,
735
+ c: p.a * l.c + p.c * l.d,
736
+ d: p.b * l.c + p.d * l.d,
737
+ tx: p.a * l.tx + p.c * l.ty + p.tx,
738
+ ty: p.b * l.tx + p.d * l.ty + p.ty,
739
+ };
740
+ }
741
+ function applyMat(m, x, y) {
742
+ return {
743
+ x: m.a * x + m.c * y + m.tx,
744
+ y: m.b * x + m.d * y + m.ty,
745
+ };
746
+ }
747
+ /**
748
+ * Local matrix matching `plugin-renderer` ContainerManager:
749
+ * position (+ parent size * anchor), rotation, scale, pivot = size * origin.
750
+ */
751
+ function localTransformMatrix(t) {
752
+ const x = t.position.x + (t.parent ? t.parent.size.width * t.anchor.x : 0);
753
+ const y = t.position.y + (t.parent ? t.parent.size.height * t.anchor.y : 0);
754
+ const pivotX = t.size.width * t.origin.x;
755
+ const pivotY = t.size.height * t.origin.y;
756
+ const cos = Math.cos(t.rotation);
757
+ const sin = Math.sin(t.rotation);
758
+ const a = cos * t.scale.x;
759
+ const b = sin * t.scale.x;
760
+ const c = -sin * t.scale.y;
761
+ const d = cos * t.scale.y;
762
+ return {
763
+ a,
764
+ b,
765
+ c,
766
+ d,
767
+ tx: x - (pivotX * a + pivotY * c),
768
+ ty: y - (pivotX * b + pivotY * d),
769
+ };
770
+ }
771
+ function worldTransformMatrix(t) {
772
+ const chain = [];
773
+ let cur = t;
774
+ while (cur) {
775
+ chain.push(cur);
776
+ cur = cur.parent;
777
+ }
778
+ chain.reverse();
779
+ let m = identityMat();
780
+ for (const node of chain) {
781
+ m = multiplyMat(m, localTransformMatrix(node));
782
+ }
783
+ return m;
784
+ }
785
+ function aabbFromPoints(points) {
786
+ if (points.length === 0)
787
+ return null;
788
+ let minX = Infinity;
789
+ let minY = Infinity;
790
+ let maxX = -Infinity;
791
+ let maxY = -Infinity;
792
+ for (const p of points) {
793
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y))
794
+ continue;
795
+ minX = Math.min(minX, p.x);
796
+ minY = Math.min(minY, p.y);
797
+ maxX = Math.max(maxX, p.x);
798
+ maxY = Math.max(maxY, p.y);
799
+ }
800
+ if (!Number.isFinite(minX) || !Number.isFinite(minY))
801
+ return null;
802
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
803
+ }
804
+ function designToCss(point, design, css) {
805
+ if (!design || design.width <= 0 || design.height <= 0) {
806
+ return { x: point.x, y: point.y };
807
+ }
808
+ return {
809
+ x: point.x * (css.width / design.width),
810
+ y: point.y * (css.height / design.height),
811
+ };
812
+ }
813
+ function cssToDesign(point, design, css) {
814
+ if (!design || design.width <= 0 || design.height <= 0 || css.width <= 0 || css.height <= 0) {
815
+ return { x: point.x, y: point.y };
816
+ }
817
+ return {
818
+ x: point.x * (design.width / css.width),
819
+ y: point.y * (design.height / css.height),
820
+ };
821
+ }
822
+ function readPositiveNumber(value) {
823
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
824
+ }
825
+ function resolveDesignSize(game) {
826
+ const renderer = game?.getSystem?.('Renderer');
827
+ const screen = renderer?.application?.screen;
828
+ const sw = readPositiveNumber(screen?.width);
829
+ const sh = readPositiveNumber(screen?.height);
830
+ if (sw && sh) {
831
+ return { width: sw, height: sh };
832
+ }
833
+ const pw = readPositiveNumber(renderer?.params?.width);
834
+ const ph = readPositiveNumber(renderer?.params?.height);
835
+ if (pw && ph) {
836
+ return { width: pw, height: ph };
837
+ }
838
+ return null;
839
+ }
840
+ function resolveCamera(game) {
841
+ const renderer = game?.getSystem?.('Renderer3DSystem');
842
+ return renderer?.threeContext?.camera ?? null;
843
+ }
844
+ function resolveObject3D(game, id) {
845
+ const renderer = game?.getSystem?.('Renderer3DSystem');
846
+ return renderer?.threeContext?.nodes?.get?.(id) ?? null;
847
+ }
848
+ function asTransform3D(value) {
849
+ if (!value || typeof value !== 'object')
850
+ return null;
851
+ const t = value;
852
+ const hasWorld = [t.worldPositionX, t.worldPositionY, t.worldPositionZ].some(v => typeof v === 'number' && Number.isFinite(v));
853
+ const hasLocal = [t.positionX, t.positionY, t.positionZ].some(v => typeof v === 'number' && Number.isFinite(v));
854
+ return hasWorld || hasLocal ? t : null;
855
+ }
856
+ function multiplyMat4Vec4(m, x, y, z, w) {
857
+ return [
858
+ m[0] * x + m[4] * y + m[8] * z + m[12] * w,
859
+ m[1] * x + m[5] * y + m[9] * z + m[13] * w,
860
+ m[2] * x + m[6] * y + m[10] * z + m[14] * w,
861
+ m[3] * x + m[7] * y + m[11] * z + m[15] * w,
862
+ ];
863
+ }
864
+ function projectWorldPoint(x, y, z, camera, css) {
865
+ const view = camera.matrixWorldInverse?.elements;
866
+ const proj = camera.projectionMatrix?.elements;
867
+ if (!view || view.length < 16 || !proj || proj.length < 16) {
868
+ return null;
869
+ }
870
+ if (css.width <= 0 || css.height <= 0) {
871
+ return null;
872
+ }
873
+ const viewed = multiplyMat4Vec4(view, x, y, z, 1);
874
+ const clip = multiplyMat4Vec4(proj, viewed[0], viewed[1], viewed[2], viewed[3]);
875
+ if (!Number.isFinite(clip[3]) || Math.abs(clip[3]) < 1e-8) {
876
+ return null;
877
+ }
878
+ if (clip[3] < 0) {
879
+ return null;
880
+ }
881
+ const ndcX = clip[0] / clip[3];
882
+ const ndcY = clip[1] / clip[3];
883
+ const ndcZ = clip[2] / clip[3];
884
+ return {
885
+ x: (ndcX + 1) * 0.5 * css.width,
886
+ y: (1 - ndcY) * 0.5 * css.height,
887
+ depth: ndcZ,
888
+ };
889
+ }
890
+ function transformMat4Point(m, x, y, z) {
891
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15];
892
+ const invW = Math.abs(w) < 1e-8 ? 1 : 1 / w;
893
+ return [
894
+ (m[0] * x + m[4] * y + m[8] * z + m[12]) * invW,
895
+ (m[1] * x + m[5] * y + m[9] * z + m[13]) * invW,
896
+ (m[2] * x + m[6] * y + m[10] * z + m[14]) * invW,
897
+ ];
898
+ }
899
+ function collectObject3DWorldCorners(node) {
900
+ const corners = [];
901
+ const visit = (obj) => {
902
+ const geo = obj.geometry;
903
+ if (!geo)
904
+ return;
905
+ geo.computeBoundingBox?.();
906
+ const box = geo.boundingBox;
907
+ const m = obj.matrixWorld?.elements;
908
+ if (!box || !m || m.length < 16)
909
+ return;
910
+ const { min, max } = box;
911
+ for (const x of [min.x, max.x]) {
912
+ for (const y of [min.y, max.y]) {
913
+ for (const z of [min.z, max.z]) {
914
+ corners.push(transformMat4Point(m, x, y, z));
915
+ }
916
+ }
917
+ }
918
+ };
919
+ node.updateWorldMatrix?.(true, true);
920
+ if (typeof node.traverse === 'function') {
921
+ node.traverse(visit);
922
+ }
923
+ else {
924
+ visit(node);
925
+ }
926
+ return corners;
927
+ }
928
+ function padAround(point, extent) {
929
+ const size = Math.max(1, extent);
930
+ return {
931
+ x: point.x - size / 2,
932
+ y: point.y - size / 2,
933
+ width: size,
934
+ height: size,
935
+ };
936
+ }
937
+ function projectTransform2D(transform, css, design) {
938
+ const matrix = worldTransformMatrix(transform);
939
+ const width = transform.size.width;
940
+ const height = transform.size.height;
941
+ const localCorners = width > 0 && height > 0
942
+ ? [
943
+ { x: 0, y: 0 },
944
+ { x: width, y: 0 },
945
+ { x: width, y: height },
946
+ { x: 0, y: height },
947
+ ]
948
+ : [{ x: 0, y: 0 }];
949
+ const world = localCorners.map(p => applyMat(matrix, p.x, p.y));
950
+ const screen = world.map(p => designToCss(p, design, css));
951
+ const aabb = aabbFromPoints(screen);
952
+ if (aabb && aabb.width > 0 && aabb.height > 0) {
953
+ return aabb;
954
+ }
955
+ const origin = screen[0] ?? { x: 0, y: 0 };
956
+ return padAround(origin, 1);
957
+ }
958
+ function projectTransform3D(transform, camera, css, object3D) {
959
+ if (object3D) {
960
+ const corners = collectObject3DWorldCorners(object3D);
961
+ const projected = corners
962
+ .map(([x, y, z]) => projectWorldPoint(x, y, z, camera, css))
963
+ .filter((p) => p != null);
964
+ const aabb = aabbFromPoints(projected);
965
+ if (aabb && aabb.width > 0 && aabb.height > 0) {
966
+ return aabb;
967
+ }
968
+ }
969
+ const x = transform.worldPositionX ?? transform.positionX ?? 0;
970
+ const y = transform.worldPositionY ?? transform.positionY ?? 0;
971
+ const z = transform.worldPositionZ ?? transform.positionZ ?? 0;
972
+ const projected = projectWorldPoint(x, y, z, camera, css);
973
+ if (!projected)
974
+ return null;
975
+ const scale = Math.max(Math.abs(transform.scaleX ?? 1), Math.abs(transform.scaleY ?? 1), Math.abs(transform.scaleZ ?? 1), 1);
976
+ return padAround(projected, DEFAULT_POINT_EXTENT * scale);
977
+ }
978
+ function projectGameObjectToScreen(go, opts) {
979
+ if (opts.css.width <= 0 || opts.css.height <= 0) {
980
+ return null;
981
+ }
982
+ const t3d = asTransform3D(go.getComponent('Transform3D'));
983
+ if (t3d && opts.camera) {
984
+ return projectTransform3D(t3d, opts.camera, opts.css, opts.object3D);
985
+ }
986
+ return projectTransform2D(go.transform, opts.css, opts.design ?? null);
987
+ }
988
+ function rectContains(rect, x, y) {
989
+ return x >= rect.x && y >= rect.y && x <= rect.x + rect.width && y <= rect.y + rect.height;
990
+ }
991
+ /** Smallest containing rect wins; later items win ties (front-most in walk order). */
992
+ function hitTestRects(x, y, items) {
993
+ let best = null;
994
+ for (let i = 0; i < items.length; i++) {
995
+ const item = items[i];
996
+ if (!rectContains(item.rect, x, y))
997
+ continue;
998
+ const area = Math.max(item.rect.width, 0) * Math.max(item.rect.height, 0);
999
+ if (!best || area < best.area || (area === best.area && i > best.index)) {
1000
+ best = { id: item.id, area, index: i };
1001
+ }
1002
+ }
1003
+ return best ? best.id : null;
1004
+ }
1005
+
588
1006
  const OUTLINE_GO_NAME = '__combosDevelopmentToolOutline';
589
1007
  const MARKER_GO_NAME_PREFIX = '__combosDevelopmentToolMarker:';
1008
+ const OUTLINE_COLOR = '#55ffaa';
590
1009
  /**
591
1010
  * **Off** at start. While enabled:
592
- * - soft-disables game `Event` hit targets (`container.interactive = false`, restored on disable);
593
- * - attaches pick `Event` + outline only on nodes with {@link CombosDevelopmentToolTarget}.
1011
+ * - a transparent HTML canvas overlay captures pointer events (game Event / Event3D
1012
+ * do not receive the tap);
1013
+ * - only nodes with {@link CombosDevelopmentToolTarget} are pickable;
1014
+ * - selection outline and markers are drawn with the Canvas 2D API (no renderer plugin).
594
1015
  *
595
1016
  * Turn on/off via:
596
1017
  * - `window.dispatchEvent(new CustomEvent(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, { detail: { enabled: true } }))`
@@ -605,7 +1026,7 @@ const MARKER_GO_NAME_PREFIX = '__combosDevelopmentToolMarker:';
605
1026
  * (`combos-game:set-playing` / `combos-game:state-changed`), not this tool.
606
1027
  *
607
1028
  * While enabled: `game.emit(COMBOS_DEVELOPMENT_TOOL_REFRESH)`, `window` event `combos-development-tool:refresh`,
608
- * or `postMessage({ type: 'combos-development-tool:refresh' })` to re-bind after adding/removing objects.
1029
+ * or `postMessage({ type: 'combos-development-tool:refresh' })` after adding/removing objects.
609
1030
  */
610
1031
  let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends System {
611
1032
  constructor() {
@@ -613,30 +1034,22 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
613
1034
  this.postMessageOrigin = '*';
614
1035
  this.allowedMessageOrigins = mergeAllowedMessageOrigins();
615
1036
  this.enabled = false;
616
- this.outlineGo = null;
617
1037
  this.selected = null;
618
- this.tapHandlers = new Map();
619
- this.tapOwners = new Map();
620
- /** Game `Event` hit targets soft-disabled while enabled (`container.interactive = false`). */
621
- this.disabledGameEventGoIds = new Set();
622
- /** Pick `Event` injected for {@link CombosDevelopmentToolTarget} nodes. */
623
- this.pickEvents = new Set();
1038
+ this.overlay = null;
1039
+ this.overlayCss = { width: 0, height: 0 };
1040
+ this.pointerDownGoId = null;
624
1041
  this.needsRescan = false;
625
- this.lastOutlineBounds = null;
626
1042
  /** Remembered mute when `SoundSystem` is not registered yet. */
627
1043
  this.mutedWithoutSoundSystem = false;
628
1044
  /**
629
1045
  * Marker overlay (see {@link markerLayer}): a visual-only icon layer, toggled
630
- * independently of pick mode + mute. Selection/persistence are NOT handled here
631
- * a marked object (e.g. Sound) carries its own `CombosDevelopmentToolTarget`
632
- * (added by the game code), so it is picked and serialized through the normal
633
- * Target path. The icon just makes the invisible object visible and, by adding
634
- * to the owner's rendered bounds, gives its Target pick a clickable hit area.
1046
+ * independently of pick mode + mute. Selection/persistence still go through
1047
+ * `CombosDevelopmentToolTarget` (added by the Vite plugin).
635
1048
  */
636
1049
  this.markerOverlayEnabled = false;
637
1050
  this.needsMarkerRescan = false;
638
- /** ownerGoId → its marker GO (icon child, Graphics only). */
639
- this.markerGoByOwner = new Map();
1051
+ /** ownerGoId → marker def currently shown. */
1052
+ this.markerOwners = new Set();
640
1053
  this.onWindowSetPickMode = (e) => {
641
1054
  const d = e.detail;
642
1055
  if (d && typeof d.enabled === 'boolean') {
@@ -689,11 +1102,41 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
689
1102
  this.onWindowRefresh = () => {
690
1103
  this.requestSceneRescan();
691
1104
  };
1105
+ this.onPointerDown = (e) => {
1106
+ if (!this.enabled)
1107
+ return;
1108
+ e.preventDefault();
1109
+ e.stopPropagation();
1110
+ try {
1111
+ this.overlay?.canvas.setPointerCapture(e.pointerId);
1112
+ }
1113
+ catch {
1114
+ /* ignore */
1115
+ }
1116
+ const hit = this.hitTestPointer(e);
1117
+ this.pointerDownGoId = hit?.id ?? null;
1118
+ };
1119
+ this.onPointerUp = (e) => {
1120
+ if (!this.enabled)
1121
+ return;
1122
+ e.preventDefault();
1123
+ e.stopPropagation();
1124
+ const hit = this.hitTestPointer(e);
1125
+ if (hit && hit.id === this.pointerDownGoId) {
1126
+ this.onSelect(hit, e);
1127
+ }
1128
+ this.pointerDownGoId = null;
1129
+ };
692
1130
  }
693
1131
  static { this.systemName = 'CombosDevelopmentToolSystem'; }
694
1132
  init(params) {
695
1133
  this.postMessageOrigin = params?.postMessageOrigin ?? '*';
696
1134
  this.allowedMessageOrigins = mergeAllowedMessageOrigins(params?.allowedMessageOrigins);
1135
+ this.overlay = createOverlayCanvas();
1136
+ if (this.overlay) {
1137
+ this.overlay.canvas.addEventListener('pointerdown', this.onPointerDown);
1138
+ this.overlay.canvas.addEventListener('pointerup', this.onPointerUp);
1139
+ }
697
1140
  if (typeof window !== 'undefined') {
698
1141
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, this.onWindowSetPickMode);
699
1142
  window.addEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
@@ -703,11 +1146,18 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
703
1146
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, this.onGameSetPickMode);
704
1147
  this.game.on(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
705
1148
  this.game.on(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
1149
+ this.syncOverlayMode();
706
1150
  if (this.enabled) {
707
1151
  this.needsRescan = true;
708
1152
  }
709
1153
  }
710
1154
  onDestroy() {
1155
+ if (this.overlay) {
1156
+ this.overlay.canvas.removeEventListener('pointerdown', this.onPointerDown);
1157
+ this.overlay.canvas.removeEventListener('pointerup', this.onPointerUp);
1158
+ this.overlay.destroy();
1159
+ this.overlay = null;
1160
+ }
711
1161
  if (typeof window !== 'undefined') {
712
1162
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, this.onWindowSetPickMode);
713
1163
  window.removeEventListener(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onWindowSetMarkerOverlay);
@@ -717,9 +1167,7 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
717
1167
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, this.onGameSetPickMode);
718
1168
  this.game.off(COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, this.onGameSetMarkerOverlay);
719
1169
  this.game.off(COMBOS_DEVELOPMENT_TOOL_REFRESH, this.onGameRefresh);
720
- this.detachAllPicks();
721
- this.detachAllMarkers();
722
- this.restoreDisabledGameEvents();
1170
+ this.markerOwners.clear();
723
1171
  this.clearSelectionAndNotify('pick-disabled');
724
1172
  }
725
1173
  /** Programmatic toggle (same effect as events). */
@@ -727,14 +1175,14 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
727
1175
  if (this.enabled === on)
728
1176
  return;
729
1177
  this.enabled = on;
1178
+ this.syncOverlayMode();
730
1179
  if (!on) {
731
1180
  this.clearSelectionAndNotify('pick-disabled');
732
- this.detachAllPicks();
733
- this.restoreDisabledGameEvents();
734
1181
  this.postSetSuccess(false);
735
1182
  }
736
1183
  else {
737
1184
  this.needsRescan = true;
1185
+ this.redrawOverlay();
738
1186
  }
739
1187
  }
740
1188
  get isEnabled() {
@@ -743,27 +1191,21 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
743
1191
  /**
744
1192
  * Toggle the marker overlay: pin a persistent icon on every object that owns a
745
1193
  * registered invisible component ({@link markerLayer}, Sound first). Independent
746
- * of pick mode / mute. This layer is purely visual the icon makes the object
747
- * visible and adds to its rendered bounds so the owner's own
748
- * `CombosDevelopmentToolTarget` pick gets a clickable hit area. Selecting and
749
- * persisting edits (e.g. volume) still go through that Target, exactly like any
750
- * other scene edit, so the object must carry a Target (added by the game code).
1194
+ * of pick mode / mute. Selecting and persisting edits still go through the
1195
+ * owner's `CombosDevelopmentToolTarget`.
751
1196
  */
752
1197
  setMarkerOverlay(on) {
753
1198
  if (this.markerOverlayEnabled === on)
754
1199
  return;
755
1200
  this.markerOverlayEnabled = on;
1201
+ this.syncOverlayMode();
756
1202
  if (on) {
757
- // `attachMarkers` requests the pick rescan itself, once the icons exist.
758
1203
  this.needsMarkerRescan = true;
759
1204
  }
760
1205
  else {
761
- this.detachAllMarkers();
1206
+ this.markerOwners.clear();
762
1207
  this.postMarkerOverlaySuccess(false, []);
763
- // Shrink owners' pick hit areas back now that the icons are gone.
764
- if (this.enabled) {
765
- this.needsRescan = true;
766
- }
1208
+ this.redrawOverlay();
767
1209
  }
768
1210
  }
769
1211
  get isMarkerOverlayEnabled() {
@@ -789,51 +1231,31 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
789
1231
  }
790
1232
  this.mutedWithoutSoundSystem = muted;
791
1233
  }
792
- update(e) {
1234
+ update() {
793
1235
  if (this.enabled && this.needsRescan) {
794
1236
  this.needsRescan = false;
795
- this.attachEditMode();
796
1237
  this.postSetSuccess(true);
797
1238
  }
798
- // Attaching markers is deferred to run *after* the pick rescan above so that a
799
- // newly drawn icon's rebuild lands on a later frame (see `attachMarkers`): the
800
- // icon child needs a rendered container before it contributes to the owner's
801
- // bounds, and only then does the owner's Target pick get a hit area over it.
802
1239
  if (this.markerOverlayEnabled && this.needsMarkerRescan) {
803
1240
  this.needsMarkerRescan = false;
804
1241
  this.attachMarkers();
805
1242
  }
806
- for (const go of [...this.tapOwners.values()]) {
807
- if (go.destroyed) {
808
- this.releasePick(go);
809
- }
810
- }
811
- // Drop markers whose owner disappeared while the overlay is on.
812
- for (const [ownerId, marker] of [...this.markerGoByOwner.entries()]) {
813
- const owner = this.findGameObjectById(ownerId);
814
- if (!owner || owner.destroyed || marker.destroyed) {
815
- this.removeMarker(ownerId);
816
- }
1243
+ if (this.selected?.destroyed) {
1244
+ this.clearSelectionAndNotify('target-removed');
817
1245
  }
818
- if (!this.enabled || !this.selected || !this.outlineGo)
819
- return;
820
- this.updateSelectionOutline();
1246
+ }
1247
+ lateUpdate(_e) {
1248
+ this.redrawOverlay();
821
1249
  }
822
1250
  componentChanged(changed) {
823
- if (!this.enabled)
824
- return;
825
1251
  const { type, gameObject, componentName } = changed;
826
1252
  if (!gameObject || componentName !== 'CombosDevelopmentToolTarget')
827
1253
  return;
828
- if (type === OBSERVER_TYPE.ADD) {
829
- this.stripGameEvent(gameObject);
830
- this.ensurePick(gameObject);
1254
+ if (type === OBSERVER_TYPE.REMOVE && this.selected === gameObject) {
1255
+ this.clearSelectionAndNotify('target-removed');
831
1256
  }
832
- else if (type === OBSERVER_TYPE.REMOVE) {
833
- this.releasePick(gameObject);
834
- if (this.selected === gameObject) {
835
- this.clearSelectionAndNotify('target-removed');
836
- }
1257
+ if (this.markerOverlayEnabled) {
1258
+ this.needsMarkerRescan = true;
837
1259
  }
838
1260
  }
839
1261
  /** Re-bind pick targets / markers after dynamic scene changes while active. */
@@ -845,24 +1267,10 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
845
1267
  this.needsMarkerRescan = true;
846
1268
  }
847
1269
  }
848
- attachEditMode() {
849
- this.detachAllPicks();
850
- const list = [];
851
- for (const tr of this.game.scene.transform.children) {
852
- this.collectGameObjects(tr.gameObject, list);
853
- }
854
- for (const go of list) {
855
- if (this.isIgnoredPickGo(go))
856
- continue;
857
- this.stripGameEvent(go);
858
- }
859
- for (const go of list) {
860
- if (this.isIgnoredPickGo(go))
861
- continue;
862
- if (!go.getComponent(CombosDevelopmentToolTarget))
863
- continue;
864
- this.ensurePick(go);
865
- }
1270
+ syncOverlayMode() {
1271
+ const active = this.enabled || this.markerOverlayEnabled;
1272
+ this.overlay?.setVisible(active);
1273
+ this.overlay?.setPickEnabled(this.enabled);
866
1274
  }
867
1275
  collectGameObjects(go, out) {
868
1276
  out.push(go);
@@ -870,203 +1278,134 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
870
1278
  this.collectGameObjects(tr.gameObject, out);
871
1279
  }
872
1280
  }
1281
+ listSceneObjects() {
1282
+ const list = [];
1283
+ const scene = this.game.scene;
1284
+ if (!scene?.transform?.children)
1285
+ return list;
1286
+ for (const tr of scene.transform.children) {
1287
+ this.collectGameObjects(tr.gameObject, list);
1288
+ }
1289
+ return list;
1290
+ }
873
1291
  isIgnoredPickGo(go) {
874
- return go.name === OUTLINE_GO_NAME || go === this.outlineGo || this.isMarkerGo(go);
1292
+ return go.name === OUTLINE_GO_NAME || this.isMarkerGo(go);
875
1293
  }
876
1294
  isMarkerGo(go) {
877
1295
  return typeof go.name === 'string' && go.name.startsWith(MARKER_GO_NAME_PREFIX);
878
1296
  }
879
- stripGameEvent(go) {
880
- if (this.disabledGameEventGoIds.has(go.id))
881
- return;
882
- const ev = go.getComponent(Event);
883
- if (!ev)
884
- return;
885
- const container = this.getRendererContainer(go);
886
- if (!container)
887
- return;
888
- try {
889
- container.interactive = false;
890
- this.disabledGameEventGoIds.add(go.id);
891
- }
892
- catch {
893
- /* ignore */
894
- }
1297
+ projectGo(go) {
1298
+ return projectGameObjectToScreen(go, {
1299
+ css: this.overlayCss,
1300
+ design: resolveDesignSize(this.game),
1301
+ camera: resolveCamera(this.game),
1302
+ object3D: resolveObject3D(this.game, go.id),
1303
+ });
895
1304
  }
896
- restoreDisabledGameEvents() {
897
- for (const id of [...this.disabledGameEventGoIds]) {
898
- const go = this.findGameObjectById(id);
899
- if (!go || go.destroyed) {
900
- this.disabledGameEventGoIds.delete(id);
1305
+ collectOverlayHits() {
1306
+ const hits = [];
1307
+ for (const go of this.listSceneObjects()) {
1308
+ if (go.destroyed || this.isIgnoredPickGo(go))
901
1309
  continue;
1310
+ if (!go.getComponent(CombosDevelopmentToolTarget))
1311
+ continue;
1312
+ const rect = this.projectGo(go);
1313
+ if (rect) {
1314
+ hits.push({ go, rect });
902
1315
  }
903
- const container = this.getRendererContainer(go);
904
- if (container) {
905
- try {
906
- container.interactive = true;
907
- }
908
- catch {
909
- /* ignore */
1316
+ if (this.markerOverlayEnabled && resolveMarkerDef(go)) {
1317
+ const markerRect = this.markerRectFor(go, rect);
1318
+ if (markerRect) {
1319
+ hits.push({ go, rect: markerRect });
910
1320
  }
911
1321
  }
912
- this.disabledGameEventGoIds.delete(id);
913
- }
914
- }
915
- detachAllPicks() {
916
- for (const go of [...this.tapOwners.values()]) {
917
- this.releasePick(go);
918
1322
  }
1323
+ return hits;
919
1324
  }
920
- ensurePick(go) {
921
- if (this.tapHandlers.has(go.id))
922
- return;
923
- const container = this.getRendererContainer(go);
924
- if (container) {
925
- try {
926
- container.interactive = true;
927
- }
928
- catch {
929
- /* ignore */
930
- }
931
- }
932
- let ev = go.getComponent(Event);
933
- if (!ev) {
934
- ev = go.addComponent(this.createPickEvent(go));
935
- this.pickEvents.add(go.id);
936
- }
937
- const handler = payload => this.onSelect(go, payload);
938
- this.tapHandlers.set(go.id, handler);
939
- this.tapOwners.set(go.id, go);
940
- ev.on('tap', handler);
941
- }
942
- createPickEvent(go) {
943
- const bounds = this.resolvePickBounds(go);
944
- if (bounds.width > 0 && bounds.height > 0) {
945
- return new Event({
946
- hitArea: {
947
- type: HIT_AREA_TYPE.Rect,
948
- style: {
949
- x: bounds.x,
950
- y: bounds.y,
951
- width: bounds.width,
952
- height: bounds.height,
953
- },
954
- },
955
- });
956
- }
957
- return new Event();
958
- }
959
- resolvePickBounds(go) {
960
- if (this.shouldPreferRenderedBounds(go)) {
961
- const fromOwnGraphics = this.resolveOwnGraphicsLocalBounds(go);
962
- if (fromOwnGraphics) {
963
- return fromOwnGraphics;
964
- }
965
- const fromGraphics = this.resolveContainerLocalBounds(go);
966
- if (fromGraphics) {
967
- return fromGraphics;
968
- }
969
- }
970
- const { width, height } = go.transform.size;
971
- if (width > 0 && height > 0) {
972
- return { x: 0, y: 0, width, height };
973
- }
974
- const fromContainer = this.resolveContainerLocalBounds(go);
975
- if (fromContainer) {
976
- return fromContainer;
977
- }
978
- return { x: 0, y: 0, width: 1, height: 1 };
979
- }
980
- shouldPreferRenderedBounds(go) {
981
- return go.getComponent(Graphics) != null;
982
- }
983
- /** Bounds of this GO's own Graphics only — excludes child GOs such as the selection outline. */
984
- resolveOwnGraphicsLocalBounds(go) {
985
- const gfx = go.getComponent(Graphics);
986
- if (!gfx?.graphics)
1325
+ markerRectFor(go, objectRect) {
1326
+ const rect = objectRect ?? this.projectGo(go);
1327
+ if (!rect)
987
1328
  return null;
988
- try {
989
- const bounds = gfx.graphics.getLocalBounds();
990
- if (bounds.width > 0 && bounds.height > 0) {
991
- return {
992
- x: bounds.x,
993
- y: bounds.y,
994
- width: bounds.width,
995
- height: bounds.height,
996
- };
997
- }
1329
+ if (go.getComponent('Transform3D')) {
1330
+ return {
1331
+ x: rect.x + rect.width / 2 - MARKER_ICON_SIZE / 2,
1332
+ y: rect.y + rect.height / 2 - MARKER_ICON_SIZE / 2,
1333
+ width: MARKER_ICON_SIZE,
1334
+ height: MARKER_ICON_SIZE,
1335
+ };
998
1336
  }
999
- catch {
1000
- /* ignore */
1001
- }
1002
- return null;
1003
- }
1004
- resolveContainerLocalBounds(go) {
1005
- const container = this.getRendererContainer(go);
1006
- if (!container)
1007
- return null;
1008
- const outlineContainer = this.getOutlineContainerIfChildOf(go);
1009
- let outlineDetached = false;
1010
- if (outlineContainer?.parent === container) {
1011
- container.removeChild(outlineContainer);
1012
- outlineDetached = true;
1013
- }
1014
- try {
1015
- const bounds = container.getLocalBounds();
1016
- if (bounds.width > 0 && bounds.height > 0) {
1017
- return {
1018
- x: bounds.x,
1019
- y: bounds.y,
1020
- width: bounds.width,
1021
- height: bounds.height,
1022
- };
1023
- }
1024
- }
1025
- catch {
1026
- /* ignore */
1027
- }
1028
- finally {
1029
- if (outlineDetached && outlineContainer) {
1030
- container.addChild(outlineContainer);
1031
- }
1032
- }
1033
- return null;
1337
+ return {
1338
+ x: rect.x,
1339
+ y: rect.y,
1340
+ width: MARKER_ICON_SIZE,
1341
+ height: MARKER_ICON_SIZE,
1342
+ };
1034
1343
  }
1035
- /** Outline is parented under the selected GO; exclude it when measuring content bounds. */
1036
- getOutlineContainerIfChildOf(go) {
1037
- if (!this.outlineGo || this.outlineGo.parent !== go) {
1038
- return null;
1344
+ overlayPoint(e) {
1345
+ const canvas = this.overlay?.canvas;
1346
+ if (!canvas)
1347
+ return { x: 0, y: 0 };
1348
+ const rect = canvas.getBoundingClientRect();
1349
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
1350
+ }
1351
+ hitTestPointer(e) {
1352
+ const pt = this.overlayPoint(e);
1353
+ const hits = this.collectOverlayHits();
1354
+ return hitTestRects(pt.x, pt.y, hits.map(h => ({ id: h.go, rect: h.rect })));
1355
+ }
1356
+ redrawOverlay() {
1357
+ const overlay = this.overlay;
1358
+ if (!overlay)
1359
+ return;
1360
+ const active = this.enabled || this.markerOverlayEnabled;
1361
+ if (!active) {
1362
+ overlay.setVisible(false);
1363
+ return;
1039
1364
  }
1040
- return this.getRendererContainer(this.outlineGo);
1365
+ const gameCanvas = this.game.canvas ?? this.game.scene?.canvas;
1366
+ if (!gameCanvas)
1367
+ return;
1368
+ const css = overlay.sync(gameCanvas);
1369
+ if (!css)
1370
+ return;
1371
+ this.overlayCss = css;
1372
+ overlay.ctx.clearRect(0, 0, css.width, css.height);
1373
+ this.drawMarkers(overlay.ctx);
1374
+ this.drawSelectionOutline(overlay.ctx);
1041
1375
  }
1042
- getRendererContainer(go) {
1043
- const rendererSystem = this.game.getSystem(RendererSystem);
1044
- if (!rendererSystem?.containerManager)
1045
- return null;
1046
- try {
1047
- return rendererSystem.containerManager.getContainer(go.id);
1048
- }
1049
- catch {
1050
- return null;
1376
+ drawMarkers(ctx) {
1377
+ if (!this.markerOverlayEnabled)
1378
+ return;
1379
+ const gfx = new CanvasMarkerGraphics(ctx);
1380
+ for (const go of this.listSceneObjects()) {
1381
+ if (go.destroyed || this.isIgnoredPickGo(go))
1382
+ continue;
1383
+ const def = resolveMarkerDef(go);
1384
+ if (!def)
1385
+ continue;
1386
+ const objectRect = this.projectGo(go);
1387
+ const rect = this.markerRectFor(go, objectRect);
1388
+ if (!rect)
1389
+ continue;
1390
+ ctx.save();
1391
+ ctx.translate(rect.x, rect.y);
1392
+ drawMarkerIcon(gfx, def, MARKER_ICON_SIZE);
1393
+ ctx.restore();
1051
1394
  }
1052
1395
  }
1053
- releasePick(go) {
1054
- const handler = this.tapHandlers.get(go.id);
1055
- if (!handler)
1396
+ drawSelectionOutline(ctx) {
1397
+ if (!this.enabled || !this.selected || this.selected.destroyed)
1056
1398
  return;
1057
- const ev = go.getComponent(Event);
1058
- ev?.off('tap', handler);
1059
- this.tapHandlers.delete(go.id);
1060
- this.tapOwners.delete(go.id);
1061
- if (this.pickEvents.has(go.id)) {
1062
- try {
1063
- go.removeComponent(Event);
1064
- }
1065
- catch {
1066
- /* ignore */
1067
- }
1068
- this.pickEvents.delete(go.id);
1069
- }
1399
+ const rect = this.projectGo(this.selected);
1400
+ if (!rect)
1401
+ return;
1402
+ const design = resolveDesignSize(this.game);
1403
+ const scale = design ? this.overlayCss.width / design.width : 1;
1404
+ ctx.save();
1405
+ ctx.strokeStyle = OUTLINE_COLOR;
1406
+ ctx.lineWidth = Math.max(2, 4 * (Number.isFinite(scale) && scale > 0 ? scale : 1));
1407
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
1408
+ ctx.restore();
1070
1409
  }
1071
1410
  postSetSuccess(enabled) {
1072
1411
  const payload = {
@@ -1078,21 +1417,16 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1078
1417
  }
1079
1418
  this.game.emit(COMBOS_DEVELOPMENT_TOOL_PICK_MODE_SUCCESS, { enabled });
1080
1419
  }
1081
- /** (Re)build markers for every object owning a registered marker component. */
1082
1420
  attachMarkers() {
1083
- this.detachAllMarkers();
1084
- const list = [];
1085
- for (const tr of this.game.scene.transform.children) {
1086
- this.collectGameObjects(tr.gameObject, list);
1087
- }
1421
+ this.markerOwners.clear();
1088
1422
  const counts = new Map();
1089
- for (const go of list) {
1423
+ for (const go of this.listSceneObjects()) {
1090
1424
  if (this.isIgnoredPickGo(go))
1091
1425
  continue;
1092
1426
  const def = resolveMarkerDef(go);
1093
1427
  if (!def)
1094
1428
  continue;
1095
- this.createMarker(go, def);
1429
+ this.markerOwners.add(go.id);
1096
1430
  counts.set(def.componentName, (counts.get(def.componentName) ?? 0) + 1);
1097
1431
  }
1098
1432
  const markers = [...counts.entries()].map(([componentName, count]) => ({
@@ -1100,43 +1434,7 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1100
1434
  count,
1101
1435
  }));
1102
1436
  this.postMarkerOverlaySuccess(true, markers);
1103
- // Rebuild picks next frame: the icons just added need a rendered container
1104
- // before they enlarge their owners' bounds into a clickable hit area. Runs on
1105
- // a later frame because the pick rescan block already executed above.
1106
- if (this.enabled) {
1107
- this.needsRescan = true;
1108
- }
1109
- }
1110
- createMarker(owner, def) {
1111
- if (this.markerGoByOwner.has(owner.id))
1112
- return;
1113
- const bounds = this.resolvePickBounds(owner);
1114
- const marker = new GameObject(MARKER_GO_NAME_PREFIX + owner.id, {
1115
- size: { width: MARKER_ICON_SIZE, height: MARKER_ICON_SIZE },
1116
- position: { x: bounds.x, y: bounds.y },
1117
- origin: { x: 0, y: 0 },
1118
- });
1119
- const gfx = marker.addComponent(new Graphics());
1120
- if (gfx?.graphics) {
1121
- drawMarkerIcon(gfx.graphics, def, MARKER_ICON_SIZE);
1122
- }
1123
- owner.addChild(marker);
1124
- this.markerGoByOwner.set(owner.id, marker);
1125
- }
1126
- removeMarker(ownerId) {
1127
- const marker = this.markerGoByOwner.get(ownerId);
1128
- if (!marker)
1129
- return;
1130
- this.markerGoByOwner.delete(ownerId);
1131
- if (!marker.destroyed) {
1132
- marker.destroy();
1133
- }
1134
- }
1135
- detachAllMarkers() {
1136
- for (const ownerId of [...this.markerGoByOwner.keys()]) {
1137
- this.removeMarker(ownerId);
1138
- }
1139
- this.markerGoByOwner.clear();
1437
+ this.redrawOverlay();
1140
1438
  }
1141
1439
  postMarkerOverlaySuccess(enabled, markers) {
1142
1440
  const total = markers.reduce((sum, m) => sum + m.count, 0);
@@ -1160,59 +1458,33 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1160
1458
  }
1161
1459
  return system;
1162
1460
  }
1163
- ensureOutline() {
1164
- if (this.outlineGo)
1165
- return;
1166
- const go = new GameObject(OUTLINE_GO_NAME, {
1167
- size: { width: 1, height: 1 },
1168
- position: { x: 0, y: 0 },
1169
- origin: { x: 0, y: 0 },
1170
- });
1171
- go.addComponent(new Graphics());
1172
- this.outlineGo = go;
1173
- }
1174
- onSelect(go, tap) {
1461
+ onSelect(go, e) {
1175
1462
  if (!this.enabled)
1176
1463
  return;
1177
- tap?.stopPropagation?.();
1178
- this.ensureOutline();
1179
- if (!this.outlineGo)
1180
- return;
1181
1464
  this.selected = go;
1182
- if (this.outlineGo.parent) {
1183
- this.outlineGo.remove();
1184
- }
1185
- go.addChild(this.outlineGo);
1186
- this.invalidateOutlineBounds();
1187
- this.updateSelectionOutline();
1465
+ this.redrawOverlay();
1466
+ const overlayPt = this.overlayPoint(e);
1467
+ const pointer = cssToDesign(overlayPt, resolveDesignSize(this.game), this.overlayCss);
1188
1468
  const snapshot = buildGameObjectSnapshot(go);
1189
1469
  const payload = {
1190
1470
  type: COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED,
1191
1471
  snapshot,
1192
- pointer: tap?.data?.position ?? null,
1472
+ pointer,
1193
1473
  };
1194
1474
  if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
1195
1475
  window.parent.postMessage(payload, this.postMessageOrigin);
1196
1476
  }
1197
1477
  }
1198
1478
  findGameObjectById(id) {
1199
- const stack = [];
1200
- for (const tr of this.game.scene.transform.children) {
1201
- this.collectGameObjects(tr.gameObject, stack);
1202
- }
1203
- return stack.find(go => go.id === id) ?? null;
1479
+ return this.listSceneObjects().find(go => go.id === id) ?? null;
1204
1480
  }
1205
1481
  findGameObjectBySource(source) {
1206
- const stack = [];
1207
- for (const tr of this.game.scene.transform.children) {
1208
- this.collectGameObjects(tr.gameObject, stack);
1209
- }
1210
1482
  const wantFile = source.file.trim();
1211
1483
  const wantAnchor = (source.anchor ?? '').trim();
1212
1484
  if (!wantAnchor) {
1213
1485
  return null;
1214
1486
  }
1215
- for (const go of stack) {
1487
+ for (const go of this.listSceneObjects()) {
1216
1488
  const anchor = readSourceAnchor(go);
1217
1489
  if (!anchor || anchor.file !== wantFile) {
1218
1490
  continue;
@@ -1256,8 +1528,7 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1256
1528
  if (!applied)
1257
1529
  return;
1258
1530
  if (this.selected?.id === go.id) {
1259
- this.invalidateOutlineBounds();
1260
- this.updateSelectionOutline();
1531
+ this.redrawOverlay();
1261
1532
  this.postSnapshotUpdate(go);
1262
1533
  }
1263
1534
  }
@@ -1274,7 +1545,8 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1274
1545
  }
1275
1546
  clearSelectionAndNotify(reason) {
1276
1547
  const hadSelection = this.selected != null;
1277
- this.clearSelection();
1548
+ this.selected = null;
1549
+ this.redrawOverlay();
1278
1550
  if (!shouldNotifyGameObjectDeselected(hadSelection)) {
1279
1551
  return;
1280
1552
  }
@@ -1287,44 +1559,6 @@ let CombosDevelopmentToolSystem = class CombosDevelopmentToolSystem extends Syst
1287
1559
  }
1288
1560
  this.game.emit(payload.type, { reason });
1289
1561
  }
1290
- clearSelection() {
1291
- this.selected = null;
1292
- this.invalidateOutlineBounds();
1293
- if (this.outlineGo?.parent) {
1294
- this.outlineGo.remove();
1295
- }
1296
- const gfx = this.outlineGo?.getComponent(Graphics);
1297
- if (gfx?.graphics) {
1298
- gfx.graphics.clear();
1299
- }
1300
- }
1301
- /** Redraw the green outline only when content bounds change (movement follows via child transform). */
1302
- updateSelectionOutline() {
1303
- if (!this.enabled || !this.selected || !this.outlineGo)
1304
- return;
1305
- const gfxComp = this.outlineGo.getComponent(Graphics);
1306
- if (!gfxComp?.graphics)
1307
- return;
1308
- const bounds = this.resolvePickBounds(this.selected);
1309
- if (this.lastOutlineBounds && this.pickBoundsEqual(this.lastOutlineBounds, bounds)) {
1310
- return;
1311
- }
1312
- this.lastOutlineBounds = bounds;
1313
- const g = gfxComp.graphics;
1314
- g.clear();
1315
- g.rect(bounds.x, bounds.y, bounds.width, bounds.height);
1316
- g.stroke({ width: 4, color: 0x55ffaa, alpha: 1 });
1317
- }
1318
- invalidateOutlineBounds() {
1319
- this.lastOutlineBounds = null;
1320
- }
1321
- pickBoundsEqual(a, b) {
1322
- const eps = 0.01;
1323
- return (Math.abs(a.x - b.x) < eps &&
1324
- Math.abs(a.y - b.y) < eps &&
1325
- Math.abs(a.width - b.width) < eps &&
1326
- Math.abs(a.height - b.height) < eps);
1327
- }
1328
1562
  };
1329
1563
  CombosDevelopmentToolSystem = __decorate([
1330
1564
  decorators.componentObserver({
@@ -1336,8 +1570,51 @@ var CombosDevelopmentToolSystem$1 = CombosDevelopmentToolSystem;
1336
1570
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
1337
1571
  Object.assign(CombosDevelopmentToolSystem$1, {
1338
1572
  packageName: "@combos-fun/plugin-development-tool",
1339
- packageVersion: "0.0.48",
1573
+ packageVersion: "0.0.49",
1340
1574
  });
1341
1575
 
1342
- export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, COMBOS_DEVELOPMENT_TOOL_PICK_MODE_SUCCESS, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem$1 as CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, MARKER_COMPONENTS, MARKER_ICON_SIZE, applyPropertyValue, applyPropertyWithHooks, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, drawMarkerIcon, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, resolveMarkerDef, shouldNotifyGameObjectDeselected };
1576
+ function isInternalEditorGo(go) {
1577
+ return typeof go.name === 'string' && go.name.startsWith('__');
1578
+ }
1579
+ function isSceneLike(go) {
1580
+ return Array.isArray(go.gameObjects);
1581
+ }
1582
+ function sourceIsComplete(raw) {
1583
+ if (!isSceneSourceAnchor(raw))
1584
+ return false;
1585
+ return Boolean(raw.file.trim()) && Boolean((raw.anchor ?? '').trim());
1586
+ }
1587
+ /**
1588
+ * Marks `go` for editor pick / persist. Idempotent: existing Targets keep a
1589
+ * complete `payload.source`; missing identity is filled from `source`.
1590
+ *
1591
+ * Inserted by `@combos-fun/plugin-development-tool/vite`. Game code should
1592
+ * not call this by hand.
1593
+ */
1594
+ function attachDevelopmentTarget(go, source) {
1595
+ if (!go || go.destroyed || isInternalEditorGo(go) || isSceneLike(go)) {
1596
+ return go;
1597
+ }
1598
+ const file = source.file.trim();
1599
+ const anchor = source.anchor.trim();
1600
+ if (!file || !anchor) {
1601
+ return go;
1602
+ }
1603
+ const existing = go.getComponent(CombosDevelopmentToolTarget);
1604
+ if (existing) {
1605
+ if (!sourceIsComplete(existing.payload?.source)) {
1606
+ existing.payload = {
1607
+ ...(existing.payload ?? {}),
1608
+ source: { file, anchor },
1609
+ };
1610
+ }
1611
+ return go;
1612
+ }
1613
+ go.addComponent(new CombosDevelopmentToolTarget({
1614
+ payload: { source: { file, anchor } },
1615
+ }));
1616
+ return go;
1617
+ }
1618
+
1619
+ export { COMBOS_DEVELOPMENT_TOOL_APPLY_PROPERTY, COMBOS_DEVELOPMENT_TOOL_CLEAR_SELECTION, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_DESELECTED, COMBOS_DEVELOPMENT_TOOL_GAMEOBJECT_SELECTED, COMBOS_DEVELOPMENT_TOOL_MARKER_OVERLAY_SUCCESS, COMBOS_DEVELOPMENT_TOOL_PICK_MODE_SUCCESS, COMBOS_DEVELOPMENT_TOOL_READY, COMBOS_DEVELOPMENT_TOOL_REFRESH, COMBOS_DEVELOPMENT_TOOL_SET_MARKER_OVERLAY, COMBOS_DEVELOPMENT_TOOL_SET_MUTED, COMBOS_DEVELOPMENT_TOOL_SET_PICK_MODE, COMBOS_DEVELOPMENT_TOOL_STATE_CHANGED, CombosDevelopmentToolSystem$1 as CombosDevelopmentToolSystem, CombosDevelopmentToolTarget, DEFAULT_ALLOWED_MESSAGE_HOST_SUFFIXES, MARKER_COMPONENTS, MARKER_ICON_SIZE, applyPropertyValue, applyPropertyWithHooks, attachDevelopmentTarget, buildGameObjectDeselectedPayload, buildGameObjectSnapshot, drawMarkerIcon, isAllowedMessageOrigin, isClearSelectionMessage, mergeAllowedMessageOrigins, readPropertyValue, readSourceAnchor, resolveMarkerDef, shouldNotifyGameObjectDeselected };
1343
1620
  //# sourceMappingURL=plugin-development-tool.esm.js.map