BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeTransform — Documentazione Tecnica

Trasformata 2D affine (TRS + pivot): locali vs mondo, cache dirty, grafo parent/child

BeeEngine Mascot

📁 Panoramica

BeeTransform è la geometria. Vive in src/core/BeeTransform.js, il barrel la re-esporta con BEE_TRANSFORM_DEFAULTS. BeeEntity ne possiede una (entity.transform) e inoltra x/y/worldX/rotation/scale. Non è un offset anni ’90 (world = parent.x + x): è una matrice affine

T(pos) · R · S · T(-pivot), composta col parent: world = parentWorld · local.

Quello che non è: un nodo di scena, un body, un renderer. Non ha update, non conosce il canvas oltre applyWorldTo(ctx), non integra velocità. Tiene posa, cache e albero di trasformate. La world matrix è lazy: si ricalcola solo se dirty, senza allocare oggetti nuovi nel tick.

import { BeeTransform, BEE_TRANSFORM_DEFAULTS } from ‘beeengine’; const hub = new BeeTransform({ x: 200, y: 160 }); const satellite = new BeeTransform({ x: 48, y: 0 }); satellite.setParent(hub); ctx.save(); hub.applyWorldTo(ctx); ctx.fillRect(0, 0, 32, 32); ctx.restore();

📁 Locali vs mondo — il contratto

x, y, rotation (radianti), scaleX/scaleY, pivotX/pivotY sono sempre locali al parent. x/y sono la posizione del pivot nello spazio del parent, non necessariamente lo spigolo in alto a sinistra.

worldX / worldY sono tx/ty della world matrix: il punto mondo in cui cade l’origine locale (0, 0). Se il pivot non è zero, worldX non è la posizione mondo del pivot. Quella si ottiene con transformPoint(pivotX, pivotY).

Spazio Campi Significato
Locale x y rotation scaleX/Y pivotX/Y Scrittura: marca dirty. x/y = posa del pivot nel parent
Mondo worldX worldY worldRotation worldScaleX/Y Lettura lazy. worldX/Y = origine locale (0, 0) in mondo

Scrivere worldX / worldY (o setWorldOrigin) non fa x = world - parent.x. Inverte la catena del parent e poi ricava i locali tenendo conto di rotazione, scala e pivot.

Un solo pezzo del contratto: la scrittura non ricalcola il mondo. Lo fa la prima lettura dopo che la catena è sporca.

  1. 1. set x / y Assegna i locali. Se il valore è uguale, esce.
  2. 2. markDirty Flag mondo + inversa. Propaga ai figli. Chiama onDirty.
  3. 3. nessun sync La matrix mondo resta vecchia finché nessuno la chiede.
  4. 4. get worldX Prima lettura: #syncWorld dal parent in giù, poi cache.
const padre = new BeeTransform({ x: 100, y: 50 }); const figlio = new BeeTransform({ x: 10, y: 5 }); figlio.setParent(padre); figlio.x; // 10 — locale figlio.worldX; // 110 se R/S sono identità (pivot 0) padre.rotation = Math.PI / 2; figlio.worldX; // NON è più 110: il parent ha ruotato figlio.worldX = 130; // inverte il parent, riscrive x locale

📁 BEE_TRANSFORM_DEFAULTS

Oggetto frozen. Il costruttore fa { ...defaults, ...options }. zIndex è l’unico campo pubblico senza cache: non entra nella matrice.

Parametro Default Significato
x / y 0 Posizione del pivot nello spazio del parent
rotation 0 Radianti. Alias pratico: rotationDegrees
scaleX / scaleY 1 Scala locale. 0 e negativi (flip) sono ammessi
pivotX / pivotY 0 Punto locale attorno a cui ruoti e scali
zIndex 0 Ordinamento; la classe non lo usa in #syncWorld

📁 Dirty cache — tre flag, zero alloc

Tre sporcizie, non una. #worldDirty per la world matrix, #inverseDirty per l’inversa (pick, setWorldOrigin), #trigDirty per cos/sin della sola rotazione. #local, #world, #inverse, #point, #aabb sono oggetti riutilizzati: nel tick non nasce nulla.

markDirty() esce subito se era già sporco — non ripercorre lo stesso ramo, e non richiama onDirty. Poi marca sé, l’inversa, i figli della transform, e (se è una funzione) onDirty. BeeEntity ci aggancia #cascadeDirty: marca i transform dei figli entity. Se i due alberi sono allineati, i figli ricevono due markDirty; il secondo è no-op.

La local matrix non ha un flag proprio: #writeLocal la riscrive ogni volta che serve (sync mondo o getter localMatrix). Costa quattro float e due trig cache-ati. Non mutare l’oggetto restituito da localMatrix / worldMatrix: è lo stesso buffer interno, e il prossimo sync lo sovrascrive.

📁 Parent / child

setParent(transform) è difensivo: ignora sé stesso, è idempotente se il parent è già quello, rifiuta i cicli (#hasAncestor). Stacca dal parent precedente, aggancia, markDirty, ritorna this. setParent(null) stacca. Non esiste detach() sulla transform: quello sta su BeeEntity.

parent e children sono getter. children è l’array interno vivo, non una copia. Push/splice a mano aggirano stacco, anti-ciclo e dirty. Per cambiare il grafo usa sempre setParent.

Due alberi. BeeEntity.addChild aggiorna entrambi (entity.#parent + transform.setParent). Se chiami solo entity.transform.setParent, il mondo segue la transform, ma update cammina i figli entity: i grafi divergono.

// Grafo geometria da solo satellite.setParent(hub); satellite.setParent(null); // Con le entity: un solo addChild, due alberi allineati hubEntity.addChild(satEntity); satEntity.detach();

📁 setWorldOrigin e i setter mondo

Contratto dichiarato nel sorgente: posiziona l’origine locale (0, 0) in un punto mondo. Se c’è un parent, parent.inverseTransformPoint porta il target in spazio parent; poi i locali assorbono il pivot (x = origin + local · pivot).

worldX = v è setWorldOrigin(v, this.worldY). Legge worldY prima (sync), poi scrive. Due assegnamenti separati vanno bene; non è un set atomico (x, y) se in mezzo muovi il parent.

Se la world matrix è singolare (scala 0, o parent schiacciato), invert fallisce in silenzio e lascia l’identità. setWorldOrigin / inverseTransformPoint trattano allora il mondo come se fosse già locale: i numeri tornano, la posa no. È un limite, non un throw.

📁 Punti, AABB, canvas, lookAt

transformPoint(localX, localY, out?) applica la world. inverseTransformPoint l’inversa, ricalcolata solo se #inverseDirty. Default out è #point condiviso: tieni le coordinate, non il riferimento.

getWorldAABB(width, height, out?) è l’AABB mondo del rettangolo locale (0, 0)–(width, height) dopo rotazione/scala (i quattro spigoli). Default out è #aabb condiviso. Ladybug usa questi bounds per l’OBB; il culling anche.

applyWorldTo(ctx) chiama ctx.transform(...)moltiplica la CTM, non la sostituisce. Serve save/restore. Non è setTransform.

lookAt(worldX, worldY) punta l’asse locale X dal pivot (non da (0, 0)) verso il target mondo, e scrive rotation in radianti.

const p = t.transformPoint(t.pivotX, t.pivotY); // copia .x/.y subito const box = t.getWorldAABB(32, 48, { x: 0, y: 0, width: 0, height: 0 }); t.lookAt(nemico.worldX, nemico.worldY); t.setPivotNormalized(0.5, 0.5, 32, 32); // centro sprite t.setScale(1, 1); // flip X

📁 Integrazione BeeEntity

Nel costruttore: this.transform = new BeeTransform({ x, y }) e onDirty = () => this.#cascadeDirty(). I getter/setter di posa dell’entity sono proxy. getWorldAABB() dell’entity, se non c’è body, delega a transform.getWorldAABB(width, height). applyWorldTransform(ctx) è applyWorldTo. integrate scrive transform.x/y/rotation (setter → dirty). destroy azzera onDirty e setParent(null) sui figli.

Il solver piattaforme scrive worldX/worldY usando width/height grezzi, non l’AABB ruotata e non il pivot. Con pivot ≠ 0 o rotazione non nulla l’atterraggio “worldY = py - height” non coincide più col bordo visivo. Limite del solver, non della matrice.

Sovrascrivere transform.onDirty dopo la construct spegne la cascata entity. Se ti serve un hook, incatena: chiama prima il callback esistente.

📁 Dove è facile sbagliare

Locali vs mondo. x è il pivot nel parent. worldX è l’origine (0, 0) in mondo. Con pivot al centro dello sprite i due numeri non coincidono. Per “dove sta il centro” usa transformPoint(pivotX, pivotY).

Non è più parent.x + x. Appena il parent ruota o scala, worldX non è una somma. I figli orbitano. Se ti aspetti l’offset anni ’90, stai leggendo la classe sbagliata (o una docs di BeeEntity precedente alla 2.5).

Dirty ≠ sync. Dopo x = … la world matrix è invalida, non aggiornata. Va bene: il costo sta sulla lettura. Non cache-are tu un worldX letto all’inizio del frame se poi muovi il parent.

Due grafi. Parentare solo la transform (o solo l’entity) le desincronizza. Per i nodi di gioco: addChild / detach.

children vivo, matrici vive, #point vivo. Non mutare quegli array/oggetti. Copia i numeri. Passa un out tuo a getWorldAABB se tieni il box oltre la prossima chiamata.

applyWorldTo moltiplica. Senza save/restore la CTM si accumula ogni sprite.

Decomposizione mondo. worldScaleX/Y sono lunghezze di colonna (sempre ≥ 0): il flip si perde. worldRotation è atan2(b, a): con scala negativa o shear (parent non uniforme + figlio ruotato) non ricostruisce TRS in modo unico.

Inversa singolare. Scala 0 → inversa = identità, senza errore. Number(value) || 0 su x/y/rotation/pivot normalizza i falsy a 0; la scala invece accetta 0 perché usa Number.isFinite.

📁 Esempio: hub, satellite, draw locale

import { BeeEntity } from ‘beeengine’; const hub = new BeeEntity(220, 180, 72, 72); hub.transform.setPivot(36, 36); hub.angularVelocity = 0.8; const satellite = new BeeEntity(56, 0, 20, 20); hub.addChild(satellite); // x/y restano locali function draw(ctx, entity) { ctx.save(); entity.applyWorldTransform(ctx); ctx.fillRect(0, 0, entity.width, entity.height); ctx.restore(); } satellite.worldX; // segue il hub senza codice extra satellite.worldX = 400; // tiene il world; ricalcola il locale

📁 Riepilogo API

Fluent: setScale, setPivot, setPivotNormalized, setParent, setWorldOrigin, applyWorldTo, lookAt ritornano this. I default sono in BEE_TRANSFORM_DEFAULTS (frozen).

Membro Ritorna Scopo
BEE_TRANSFORM_DEFAULTS oggetto frozen x/y 0 · rotation 0 · scaleX/Y 1 · pivotX/Y 0 · zIndex 0.
new BeeTransform(options) BeeTransform Copia default + options. Parte già dirty. onDirty è null.
x / y number Locali (pivot nel parent). Setter: Number(value) || 0, poi markDirty.
rotation / rotationDegrees number Radianti / gradi. Il setter gradi scrive rotation.
scaleX / scaleY / setScale(x, y = x) number / this Scala locale. Non finito → 1. Accetta 0 e negativi.
pivotX / pivotY / setPivot(x, y) number / this Punto locale di rotazione/scala.
setPivotNormalized(nx, ny, w, h) this pivot = (nx * w, ny * h). Centro sprite: (0.5, 0.5, width, height).
parent / children BeeTransform|null / array Sola lettura. children è l’array vivo.
setParent(transform) this Reparent + anti-ciclo. null stacca.
markDirty() void Invalida mondo/inversa, cascata figli, onDirty. No-op se già dirty.
onDirty function|null Hook pubblico. BeeEntity lo sovrascrive con la cascata entity.
localMatrix / worldMatrix oggetto {a,b,c,d,tx,ty} Buffer interni. La world sync-a prima. Non mutarli.
worldX / worldY number Origine locale in mondo. I setter chiamano setWorldOrigin.
worldRotation / worldScaleX / worldScaleY number Solo lettura. Scala = hypot delle colonne (senza segno di flip).
setWorldOrigin(worldX, worldY) this Mette (0, 0) locale su un punto mondo. Inverte il parent, non sottrae.
transformPoint(lx, ly, out?) {x, y} Locale → mondo. Default out = #point condiviso.
inverseTransformPoint(wx, wy, out?) {x, y} Mondo → locale. Inversa lazy. Matrice singolare → identità.
getWorldAABB(w, h, out?) {x, y, width, height} AABB mondo del rettangolo locale. Default out = #aabb.
applyWorldTo(ctx) this ctx.transform (multiply). Abbinalo a save/restore.
lookAt(worldX, worldY) this Orienta l’asse X locale dal pivot verso il target mondo.
zIndex number Pubblico, senza dirty. La classe non ordina il draw.