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

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