bee-ladybug 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # BeeLadybug
2
+
3
+ Universal telemetry and visual inspection overlay.
4
+
5
+ BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
6
+ core that accepts raw packets from any source — Canvas 2D games, DOM pages,
7
+ headless bots, Python processes, AI pipelines — and renders them in one
8
+ dark console.
9
+
10
+ > Il Core non sa cosa sta monitorando. Riceve solo dati grezzi.
11
+
12
+ ## Perché questa struttura
13
+
14
+ Il file storico `BeeLadybug` era accoppiato a un engine di gioco (entità,
15
+ hitbox, `ctx.drawOverlay`). Quello non scala a un sito HTML o a un modello
16
+ Python. Per questo il progetto riparte da zero con due strati:
17
+
18
+ | Strato | Responsabilità |
19
+ | --- | --- |
20
+ | `src/core/` | Orologio fittizio, buffer, grafici, overlay cyberpunk |
21
+ | `src/adapters/` | Traduttori. Convertono un mondo specifico in `sendData()` |
22
+
23
+ L'overlay **non** è disegnato sul Canvas. È un pannello HTML in Shadow DOM:
24
+ funziona sopra un gioco, sopra una pagina, o da solo in una sandbox.
25
+
26
+ ```
27
+ bee-ladybug/
28
+ ├── src/
29
+ │ ├── index.js # API pubblica
30
+ │ ├── core/
31
+ │ │ ├── BeeLadybugCore.js # stato, clock, sendData
32
+ │ │ └── UIOverlay.js # console scura + sparkline
33
+ │ └── adapters/
34
+ │ ├── CanvasAdapter.js # hitbox / FPS / collisioni Canvas 2D
35
+ │ ├── WebDOMAdapter.js # box model / hover / mutazioni HTML
36
+ │ └── python/
37
+ │ └── PythonBridge.js # futuro: WebSocket → sendData
38
+ ├── examples/
39
+ │ ├── core-sandbox.js # demo senza adapter
40
+ │ ├── core.html
41
+ │ ├── canvas-sandbox.js # demo CanvasAdapter
42
+ │ ├── dom-sandbox.js
43
+ │ └── dom.html
44
+ ├── index.html
45
+ ├── package.json
46
+ └── README.md
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ Servire la cartella con un server statico (i moduli ES non partono da `file://`):
52
+
53
+ ```bash
54
+ python -m http.server 8080
55
+ ```
56
+
57
+ Aprire `http://localhost:8080`. Premere `F2` / `F3` / `F4`. In console:
58
+
59
+ ```js
60
+ bee.sendData('log', { source: 'human', message: 'hello core' })
61
+ bee.sendData('fps', { value: 61 })
62
+ bee.sendData('error', { source: 'ai', message: 'timeout' })
63
+ ```
64
+
65
+ Uso minimo in un'app:
66
+
67
+ ```js
68
+ import { BeeLadybugCore } from './src/index.js';
69
+
70
+ const bee = new BeeLadybugCore();
71
+ bee.sendData('log', { message: 'boot', source: 'app' });
72
+ ```
73
+
74
+ Core headless (bot, test, Node con DOM assente):
75
+
76
+ ```js
77
+ const bee = new BeeLadybugCore({ ui: false, mount: false, autoAttach: false });
78
+ bee.sendData('metric', { name: 'loss', value: 0.12 });
79
+ ```
80
+
81
+ ## Contratto `sendData(type, payload)`
82
+
83
+ Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
84
+
85
+ | `type` | `payload` | Effetto |
86
+ | --- | --- | --- |
87
+ | `fps` | `{ value }` | HUD + grafico storico |
88
+ | `metric` | `{ name, value, unit? }` | HUD + serie storica |
89
+ | `coord` | `{ label?, x, y }` | HUD |
90
+ | `state` | `{ key, value }` | HUD |
91
+ | `log` | `{ message, level?, source? }` | console |
92
+ | `warn` | `{ message, source? }` | console ambra |
93
+ | `error` | `{ message, source? }` | console rossa |
94
+ | `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
95
+ | `sys` | emesso dal Core | freeze / visibilità / scala |
96
+
97
+ Ogni pacchetto viene stampato con:
98
+
99
+ - `wallTime` — orologio reale
100
+ - `simTime` — tempo fittizio del Core
101
+ - `frame` — frame-count indipendente dall'host
102
+
103
+ Il freeze **non** ferma l'overlay. Ferma solo il clock fittizio. Gli adapter
104
+ possono ascoltare `type: 'sys'` e mettere in pausa il loro mondo.
105
+
106
+ ```js
107
+ bee.subscribe((packet) => {
108
+ if (packet.type === 'sys' && packet.payload.op === 'freeze') {
109
+ host.paused = packet.payload.frozen;
110
+ }
111
+ });
112
+ ```
113
+
114
+ ## Tasti
115
+
116
+ | Tasto | Azione |
117
+ | --- | --- |
118
+ | `F2` | mostra / nascondi overlay (e hitbox canvas) |
119
+ | `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
120
+ | `F4` | freeze / run del tempo fittizio |
121
+
122
+ Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
123
+
124
+ ## CanvasAdapter
125
+
126
+ ```js
127
+ import { BeeLadybugCore, CanvasAdapter } from './src/index.js';
128
+
129
+ const bee = new BeeLadybugCore();
130
+ const adapter = new CanvasAdapter(bee, {
131
+ canvas, // HTMLCanvasElement del gioco
132
+ entities, // array di AABB, tenuto per riferimento
133
+ overlay: true, // hitbox su canvas stacked (default)
134
+ computeCollisions: true
135
+ });
136
+ adapter.attach();
137
+
138
+ function loop() {
139
+ if (!adapter.frozen) updateGame(adapter.timeScale);
140
+ drawGame();
141
+ adapter.pump(); // una volta per frame host
142
+ requestAnimationFrame(loop);
143
+ }
144
+ ```
145
+
146
+ L'adapter accetta oggetti qualsiasi con `x/y/width/height` (o `worldX/worldY`).
147
+ Disegna le hitbox su un canvas trasparente sopra il gioco: verde = ok, rosso = collide.
148
+ Espone `frozen` e `timeScale` così l'host può mettere in pausa la simulazione.
149
+ Pacchetti: `fps`, `metric entities`, `metric hits`, `coord` del player, `warn` all'ingresso collisione.
150
+
151
+ ## WebDOMAdapter
152
+
153
+ ```js
154
+ import { BeeLadybugCore, WebDOMAdapter } from './src/index.js';
155
+
156
+ const bee = new BeeLadybugCore();
157
+ const adapter = new WebDOMAdapter(bee, {
158
+ root: document.body, // sottoalbero da ispezionare
159
+ watch: ['.card', '#cta'],
160
+ overlay: true,
161
+ autoTick: true // non serve un game loop
162
+ });
163
+ adapter.attach();
164
+ ```
165
+
166
+ Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
167
+
168
+ ## API Core
169
+
170
+ ```js
171
+ const bee = new BeeLadybugCore({
172
+ toggleKey: 'F2',
173
+ slowKey: 'F3',
174
+ freezeKey: 'F4',
175
+ slowScale: 0.25,
176
+ maxLogs: 250,
177
+ historySize: 120,
178
+ mount: 'auto', // document.body, HTMLElement, o false
179
+ ui: true,
180
+ autoAttach: true,
181
+ autoStart: true
182
+ });
183
+
184
+ bee.sendData(type, payload);
185
+ bee.subscribe(handler); // ritorna unsubscribe
186
+ bee.getState();
187
+ bee.getLogs();
188
+ bee.getHistory('fps');
189
+ bee.getHud();
190
+ bee.show(); bee.hide(); bee.toggle();
191
+ bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
192
+ bee.applySlowMo(); bee.restoreRealtime();
193
+ bee.destroy();
194
+ ```
195
+
196
+ ## Roadmap
197
+
198
+ 1. **Ora** — Core + overlay + CanvasAdapter + WebDOMAdapter. Fatto.
199
+ 2. **PythonBridge** — WebSocket JSON `{ type, payload }` verso `sendData`.
200
+ 3. **Pacchetto npm** — `import { BeeLadybugCore } from 'bee-ladybug'`.
201
+
202
+ ## Come contribuire
203
+
204
+ Il Core resta cieco. Se una feature ha bisogno di conoscere Canvas, DOM o
205
+ Python, vive in un adapter. Patch al Core solo per clock, buffer, protocollo
206
+ o UI condivisa.
207
+
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "bee-ladybug",
3
+ "version": "0.1.0",
4
+ "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, bots, Python and AI flows.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "license": "MIT",
15
+ "keywords": [
16
+ "debug",
17
+ "telemetry",
18
+ "overlay",
19
+ "observability",
20
+ "canvas",
21
+ "devtools"
22
+ ]
23
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * CanvasAdapter — Canvas 2D / game translator.
3
+ *
4
+ * Reads generic AABB entities, measures host FPS, computes collisions,
5
+ * and pushes packets into the core. Hitboxes are drawn on a stacked
6
+ * overlay canvas so the game pixels stay untouched.
7
+ *
8
+ * The core must never import this file.
9
+ *
10
+ * Entity contract (any extra fields are ignored):
11
+ * { name?, id?, x|worldX, y|worldY, width|w, height|h, active?, colliding? }
12
+ *
13
+ * @example
14
+ * const adapter = new CanvasAdapter(core, { canvas, entities });
15
+ * adapter.attach();
16
+ * // after updating entity positions each host frame:
17
+ * adapter.pump();
18
+ */
19
+
20
+ export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
21
+ overlay: true,
22
+ computeCollisions: true,
23
+ colorActive: '#3dff6a',
24
+ colorColliding: '#ff3b3b',
25
+ colorInactive: '#8a8a8a',
26
+ source: 'canvas'
27
+ });
28
+
29
+ export class CanvasAdapter {
30
+ /**
31
+ * @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
32
+ * @param {Partial<typeof CANVAS_ADAPTER_DEFAULTS> & {
33
+ * canvas?: HTMLCanvasElement,
34
+ * entities?: object[]
35
+ * }} [options]
36
+ */
37
+ constructor(core, options = {}) {
38
+ const cfg = { ...CANVAS_ADAPTER_DEFAULTS, ...options };
39
+ this.core = core;
40
+ this.canvas = options.canvas ?? null;
41
+ this.entities = options.entities ?? [];
42
+ this.overlayEnabled = cfg.overlay;
43
+ this.computeCollisions = cfg.computeCollisions;
44
+ this.colorActive = cfg.colorActive;
45
+ this.colorColliding = cfg.colorColliding;
46
+ this.colorInactive = cfg.colorInactive;
47
+ this.source = cfg.source;
48
+
49
+ /** Host should pause simulation when the core freezes. */
50
+ this.frozen = false;
51
+ /** Host should scale its dt by this factor (F3). */
52
+ this.timeScale = 1;
53
+
54
+ this.#attached = false;
55
+ this.#overlay = null;
56
+ this.#unsubscribe = null;
57
+ this.#lastPump = 0;
58
+ this.#fps = 0;
59
+ this.#wasColliding = new Set();
60
+ this.#onResize = () => this.#syncOverlay();
61
+ }
62
+
63
+ #attached;
64
+ #overlay;
65
+ #unsubscribe;
66
+ #lastPump;
67
+ #fps;
68
+ #wasColliding;
69
+ #onResize;
70
+
71
+ attach() {
72
+ if (this.#attached) return this;
73
+ this.#attached = true;
74
+ this.frozen = Boolean(this.core?.frozen);
75
+ this.timeScale = this.core?.timeScale ?? 1;
76
+
77
+ if (this.core && typeof this.core.subscribe === 'function') {
78
+ this.#unsubscribe = this.core.subscribe((packet) => this.#onCore(packet));
79
+ }
80
+
81
+ if (this.overlayEnabled) this.#mountOverlay();
82
+ if (typeof window !== 'undefined') {
83
+ window.addEventListener('resize', this.#onResize);
84
+ window.addEventListener('scroll', this.#onResize, true);
85
+ }
86
+
87
+ this.core?.sendData('log', {
88
+ source: this.source,
89
+ message: `CanvasAdapter attached (${this.entities.length} entities).`
90
+ });
91
+ this.core?.sendData('state', {
92
+ source: this.source,
93
+ key: 'adapter',
94
+ value: 'canvas'
95
+ });
96
+ return this;
97
+ }
98
+
99
+ detach() {
100
+ if (!this.#attached) return this;
101
+ this.#attached = false;
102
+ if (this.#unsubscribe) this.#unsubscribe();
103
+ this.#unsubscribe = null;
104
+ this.#unmountOverlay();
105
+ if (typeof window !== 'undefined') {
106
+ window.removeEventListener('resize', this.#onResize);
107
+ window.removeEventListener('scroll', this.#onResize, true);
108
+ }
109
+ return this;
110
+ }
111
+
112
+ /** @param {HTMLCanvasElement | null} canvas */
113
+ setCanvas(canvas) {
114
+ this.canvas = canvas;
115
+ this.#syncOverlay();
116
+ return this;
117
+ }
118
+
119
+ /** @param {object[]} entities */
120
+ setEntities(entities) {
121
+ this.entities = entities ?? [];
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * Call once per host frame, after entity positions are updated.
127
+ * Pass a 2D context only if overlay is disabled and you want hitboxes
128
+ * drawn onto the game canvas itself.
129
+ * @param {CanvasRenderingContext2D} [ctx]
130
+ */
131
+ pump(ctx) {
132
+ if (!this.#attached || !this.core) return this;
133
+
134
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
135
+ if (this.#lastPump) {
136
+ const dt = Math.min(Math.max(now - this.#lastPump, 0), 100);
137
+ const instant = dt > 0 ? 1000 / dt : 0;
138
+ this.#fps = this.#fps ? this.#fps * 0.85 + instant * 0.15 : instant;
139
+ }
140
+ this.#lastPump = now;
141
+
142
+ const boxes = this.#collectBoxes();
143
+ this.#markCollisions(boxes);
144
+ this.#emitTelemetry(boxes);
145
+ this.#emitCollisionEdges(boxes);
146
+ this.#drawHitboxes(boxes, ctx);
147
+ return this;
148
+ }
149
+
150
+ #onCore(packet) {
151
+ if (packet.type !== 'sys') return;
152
+ const op = packet.payload.op;
153
+ if (op === 'freeze') this.frozen = Boolean(packet.payload.frozen);
154
+ if (op === 'timescale') {
155
+ const scale = Number(packet.payload.scale);
156
+ if (Number.isFinite(scale)) this.timeScale = scale;
157
+ }
158
+ if (op === 'visibility') this.#syncOverlay();
159
+ }
160
+
161
+ #collectBoxes() {
162
+ const list = this.entities;
163
+ const boxes = [];
164
+ for (let i = 0; i < list.length; i++) {
165
+ const box = aabbOf(list[i], i);
166
+ if (box) boxes.push(box);
167
+ }
168
+ return boxes;
169
+ }
170
+
171
+ #markCollisions(boxes) {
172
+ for (let i = 0; i < boxes.length; i++) boxes[i].hit = Boolean(boxes[i].entity.colliding);
173
+
174
+ if (!this.computeCollisions) return;
175
+
176
+ for (let i = 0; i < boxes.length; i++) {
177
+ const a = boxes[i];
178
+ for (let j = i + 1; j < boxes.length; j++) {
179
+ const b = boxes[j];
180
+ if (!overlaps(a, b)) continue;
181
+ a.hit = true;
182
+ b.hit = true;
183
+ }
184
+ }
185
+ }
186
+
187
+ #emitTelemetry(boxes) {
188
+ let hits = 0;
189
+ let tracked = null;
190
+ for (let i = 0; i < boxes.length; i++) {
191
+ if (boxes[i].hit) hits += 1;
192
+ if (!tracked && isPrimary(boxes[i].entity)) tracked = boxes[i];
193
+ }
194
+ if (!tracked && boxes[0]) tracked = boxes[0];
195
+
196
+ if (this.#fps > 0) {
197
+ this.core.sendData('fps', { value: this.#fps, source: this.source });
198
+ }
199
+ this.core.sendData('metric', {
200
+ name: 'entities',
201
+ value: boxes.length,
202
+ source: this.source
203
+ });
204
+ this.core.sendData('metric', {
205
+ name: 'hits',
206
+ value: hits,
207
+ source: this.source
208
+ });
209
+ this.core.sendData('state', {
210
+ source: this.source,
211
+ key: 'hits',
212
+ value: `${hits} / ${boxes.length}`
213
+ });
214
+
215
+ if (tracked) {
216
+ this.core.sendData('coord', {
217
+ source: this.source,
218
+ label: tracked.name,
219
+ x: tracked.x,
220
+ y: tracked.y
221
+ });
222
+ }
223
+ }
224
+
225
+ #emitCollisionEdges(boxes) {
226
+ const now = new Set();
227
+ for (let i = 0; i < boxes.length; i++) {
228
+ if (!boxes[i].hit) continue;
229
+ now.add(boxes[i].id);
230
+ if (!this.#wasColliding.has(boxes[i].id)) {
231
+ this.core.sendData('warn', {
232
+ source: this.source,
233
+ message: `${boxes[i].name} entered collision`
234
+ });
235
+ }
236
+ }
237
+ for (const id of this.#wasColliding) {
238
+ if (now.has(id)) continue;
239
+ const name = idFromKey(id);
240
+ this.core.sendData('log', {
241
+ source: this.source,
242
+ message: `${name} left collision`
243
+ });
244
+ }
245
+ this.#wasColliding = now;
246
+ }
247
+
248
+ #drawHitboxes(boxes, hostCtx) {
249
+ const overlayCtx = this.#overlay ? this.#overlay.getContext('2d') : null;
250
+ if (overlayCtx) this.#syncOverlay();
251
+
252
+ const ctx = overlayCtx || hostCtx;
253
+ if (!ctx) return;
254
+
255
+ const w = ctx.canvas.width;
256
+ const h = ctx.canvas.height;
257
+ if (overlayCtx) ctx.clearRect(0, 0, w, h);
258
+
259
+ if (this.core && this.core.visible === false) return;
260
+
261
+ ctx.save();
262
+ ctx.lineWidth = 1.5;
263
+ ctx.font = '11px ui-monospace, Consolas, monospace';
264
+ ctx.textBaseline = 'bottom';
265
+
266
+ for (let i = 0; i < boxes.length; i++) {
267
+ const box = boxes[i];
268
+ const active = box.entity.active !== false;
269
+ const color = box.hit
270
+ ? this.colorColliding
271
+ : (active ? this.colorActive : this.colorInactive);
272
+
273
+ ctx.strokeStyle = color;
274
+ ctx.fillStyle = box.hit ? 'rgba(255, 59, 59, 0.16)' : 'rgba(61, 255, 106, 0.08)';
275
+ ctx.fillRect(box.x, box.y, box.w, box.h);
276
+ ctx.strokeRect(box.x + 0.5, box.y + 0.5, box.w, box.h);
277
+
278
+ ctx.fillStyle = color;
279
+ ctx.fillText(box.name, box.x + 3, box.y - 3);
280
+ }
281
+
282
+ ctx.restore();
283
+ }
284
+
285
+ #mountOverlay() {
286
+ if (typeof document === 'undefined' || this.#overlay) return;
287
+ const canvas = document.createElement('canvas');
288
+ canvas.setAttribute('data-bee-ladybug-canvas', '');
289
+ canvas.style.pointerEvents = 'none';
290
+ canvas.style.position = 'fixed';
291
+ canvas.style.zIndex = '2147483645';
292
+ canvas.style.margin = '0';
293
+ document.body.appendChild(canvas);
294
+ this.#overlay = canvas;
295
+ this.#syncOverlay();
296
+ }
297
+
298
+ #unmountOverlay() {
299
+ if (this.#overlay?.parentNode) this.#overlay.parentNode.removeChild(this.#overlay);
300
+ this.#overlay = null;
301
+ }
302
+
303
+ #syncOverlay() {
304
+ const host = this.canvas;
305
+ const overlay = this.#overlay;
306
+ if (!host || !overlay) return;
307
+ const rect = host.getBoundingClientRect();
308
+ overlay.width = host.width;
309
+ overlay.height = host.height;
310
+ overlay.style.left = `${rect.left}px`;
311
+ overlay.style.top = `${rect.top}px`;
312
+ overlay.style.width = `${rect.width}px`;
313
+ overlay.style.height = `${rect.height}px`;
314
+ overlay.style.display = this.core && this.core.visible === false ? 'none' : 'block';
315
+ }
316
+ }
317
+
318
+ function aabbOf(entity, index) {
319
+ if (!entity) return null;
320
+ const x = numberOr(entity.worldX, entity.x);
321
+ const y = numberOr(entity.worldY, entity.y);
322
+ const w = numberOr(entity.width, entity.w);
323
+ const h = numberOr(entity.height, entity.h);
324
+ if (x == null || y == null || !w || !h || w <= 0 || h <= 0) return null;
325
+ const name = String(entity.name ?? entity.label ?? entity.id ?? `#${index}`);
326
+ const id = entity.id != null ? `id:${entity.id}` : `idx:${index}:${name}`;
327
+ return { entity, x, y, w, h, name, id, hit: false };
328
+ }
329
+
330
+ function numberOr(a, b) {
331
+ if (typeof a === 'number' && Number.isFinite(a)) return a;
332
+ if (typeof b === 'number' && Number.isFinite(b)) return b;
333
+ return null;
334
+ }
335
+
336
+ function overlaps(a, b) {
337
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
338
+ }
339
+
340
+ function isPrimary(entity) {
341
+ const key = String(entity.name ?? entity.label ?? entity.role ?? '').toLowerCase();
342
+ return key === 'player' || key === 'hero' || entity.primary === true;
343
+ }
344
+
345
+ function idFromKey(id) {
346
+ const parts = String(id).split(':');
347
+ return parts[parts.length - 1] || id;
348
+ }