Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 55 additions & 49 deletions packages/exojs-particles/src/renderers/WebGpuParticleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,55 +393,61 @@ export class WebGpuParticleRenderer extends AbstractWebGpuRenderer<ParticleSyste
const uvMaxX = (texCoords[2] & 0xffff) / 0xffff;
const uvMaxY = ((texCoords[2] >>> 16) & 0xffff) / 0xffff;

this._uniformData.set([
projection[0],
projection[1],
0,
0,
projection[3],
projection[4],
0,
0,
0,
0,
1,
0,
projection[6],
projection[7],
0,
projection[8],

transform[0],
transform[1],
0,
0,
transform[3],
transform[4],
0,
0,
0,
0,
1,
0,
transform[6],
transform[7],
0,
transform[8],

shouldPremultiplySample ? 1 : 0,
0,
0,
0,

quadMinX,
quadMinY,
quadSizeX,
quadSizeY,
uvMinX,
uvMinY,
uvMaxX,
uvMaxY,
]);
const u = this._uniformData;

// projection mat4 (col-major, padded to 4×4)
u[0] = projection[0];
u[1] = projection[1];
u[2] = 0;
u[3] = 0;
u[4] = projection[3];
u[5] = projection[4];
u[6] = 0;
u[7] = 0;
u[8] = 0;
u[9] = 0;
u[10] = 1;
u[11] = 0;
u[12] = projection[6];
u[13] = projection[7];
u[14] = 0;
u[15] = projection[8];

// transform mat4 (col-major, padded to 4×4)
u[16] = transform[0];
u[17] = transform[1];
u[18] = 0;
u[19] = 0;
u[20] = transform[3];
u[21] = transform[4];
u[22] = 0;
u[23] = 0;
u[24] = 0;
u[25] = 0;
u[26] = 1;
u[27] = 0;
u[28] = transform[6];
u[29] = transform[7];
u[30] = 0;
u[31] = transform[8];

// flags vec4
u[32] = shouldPremultiplySample ? 1 : 0;
u[33] = 0;
u[34] = 0;
u[35] = 0;

// localBounds vec4
u[36] = quadMinX;
u[37] = quadMinY;
u[38] = quadSizeX;
u[39] = quadSizeY;

// uvBounds vec4
u[40] = uvMinX;
u[41] = uvMinY;
u[42] = uvMaxX;
u[43] = uvMaxY;
}

private _writeInstanceData(system: ParticleSystem): number {
Expand Down
3 changes: 1 addition & 2 deletions site/src/content/api/polygon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ symbol: "Polygon"
kind: "class"
subsystem: "math"
importPath: "@codexo/exojs"
memberCount: 21
memberCount: 20
tier: "stable"
sections: ["Import", "Constructors", "Methods", "Properties", "Source"]
sourcePath: "src/math/Polygon.ts"
Expand Down Expand Up @@ -52,7 +52,6 @@ cached lazily and invalidated on any positional or point mutation.
- `position: Vector`
- `x: number`
- `y: number`
- `temp: Polygon`

## Source

Expand Down
3 changes: 1 addition & 2 deletions site/src/content/api/time.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ symbol: "Time"
kind: "class"
subsystem: "core"
importPath: "@codexo/exojs"
memberCount: 35
memberCount: 34
tier: "stable"
sections: ["Import", "Constructors", "Methods", "Properties", "Source"]
sourcePath: "src/core/Time.ts"
Expand Down Expand Up @@ -68,7 +68,6 @@ reference across calls.
- `milliseconds: number`
- `minutes: number`
- `seconds: number`
- `temp: Time`

## Source

Expand Down
18 changes: 12 additions & 6 deletions src/core/SceneNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,15 +555,21 @@ export class SceneNode implements Collidable, ObservableVectorOwner {

/** Mark own Bounds dirty AND propagate up to Container ancestors' Bounds. */
public _invalidateBoundsCascade(): void {
// Mark own bounds + notify interaction for THIS node unconditionally —
// the manager filters to tracked interactive nodes so this call is O(1)
// for the common case (non-interactive node — fast Set.has miss).
this.flags.push(SceneNodeTransformFlags.BoundsRect);

// Notify the InteractionManager so it can mark the quadtree entry stale.
// The manager filters to only tracked interactive nodes so this call is
// O(1) for the common case (non-interactive node — fast Set.has miss).
this._stage?.interaction._notifyBoundsInvalidated(this as unknown as RenderNode);

if (this._parentNode) {
this._parentNode._invalidateBoundsCascade();
// Walk up, but stop at the first ancestor already flagged dirty: if a parent
// is already BoundsRect-dirty, its whole ancestor chain was already cascaded,
// so re-walking it is redundant work.
let ancestor = this._parentNode;

while (ancestor !== null && !ancestor.flags.has(SceneNodeTransformFlags.BoundsRect)) {
ancestor.flags.push(SceneNodeTransformFlags.BoundsRect);
ancestor._stage?.interaction._notifyBoundsInvalidated(ancestor);
ancestor = ancestor.parent;
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/core/Time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ export class Time implements Cloneable {
public static readonly oneMinute = new Time(1, Time.minutes);
public static readonly oneHour = new Time(1, Time.hours);

/**
* Shared scratch {@link Time} instance for intermediate calculations. Never
* retain the reference across frames or async boundaries.
* @internal
*/
public static get temp(): Time {
if (temp === null) {
temp = new Time();
Expand Down
34 changes: 21 additions & 13 deletions src/input/InputManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class InputManager implements System {
private readonly _app: Application;
private readonly canvas: HTMLCanvasElement;
private readonly channels: Float32Array = new Float32Array(ChannelSize.Container);
private readonly pointers: Record<number, Pointer> = {};
private readonly pointers = new Map<number, Pointer>();
private readonly _gamepads: readonly [Gamepad, Gamepad, Gamepad, Gamepad];
private readonly gamepadsByBrowserIndex = new Map<number, Gamepad>();
private readonly bindings: Set<InputBinding> = new Set<InputBinding>();
Expand Down Expand Up @@ -194,13 +194,13 @@ export class InputManager implements System {
* no active pointer is present. Used by debug layers to show cursor info.
*/
public getPrimaryPointerPosition(): { x: number; y: number } | null {
for (const pointer of Object.values(this.pointers)) {
for (const pointer of this.pointers.values()) {
if (pointer.isPrimary && pointer.currentState !== PointerState.Cancelled) {
return { x: pointer.x, y: pointer.y };
}
}

for (const pointer of Object.values(this.pointers)) {
for (const pointer of this.pointers.values()) {
if (pointer.currentState !== PointerState.Cancelled) {
return { x: pointer.x, y: pointer.y };
}
Expand All @@ -210,7 +210,13 @@ export class InputManager implements System {
}

public get pointersInCanvas(): boolean {
return Object.values(this.pointers).some(pointer => pointer.currentState !== PointerState.OutsideCanvas && pointer.currentState !== PointerState.Cancelled);
for (const pointer of this.pointers.values()) {
if (pointer.currentState !== PointerState.OutsideCanvas && pointer.currentState !== PointerState.Cancelled) {
return true;
}
}

return false;
}

public get canvasFocused(): boolean {
Expand Down Expand Up @@ -345,10 +351,12 @@ export class InputManager implements System {
this.removeEventListeners();
this.gestureRecognizer.destroy();

for (const pointer of Object.values(this.pointers)) {
for (const pointer of this.pointers.values()) {
pointer.destroy();
}

this.pointers.clear();

for (const pad of this._gamepads) {
pad.destroy();
}
Expand Down Expand Up @@ -490,12 +498,12 @@ export class InputManager implements System {
return;
}

this.pointers[event.pointerId] = new Pointer(event, this._app, this.canvas, this.channels, slot);
this.pointers.set(event.pointerId, new Pointer(event, this._app, this.canvas, this.channels, slot));
this.flags.push(InputManagerFlag.PointerUpdate);
}

private handlePointerLeave(event: PointerEvent): void {
const pointer = this.pointers[event.pointerId];
const pointer = this.pointers.get(event.pointerId);

if (!pointer) {
return;
Expand All @@ -511,7 +519,7 @@ export class InputManager implements System {
this.canvas.focus();
this.canvasFocusedValue = true;

const pointer = this.pointers[event.pointerId];
const pointer = this.pointers.get(event.pointerId);

if (!pointer) {
return;
Expand All @@ -525,7 +533,7 @@ export class InputManager implements System {
}

private handlePointerMove(event: PointerEvent): void {
const pointer = this.pointers[event.pointerId];
const pointer = this.pointers.get(event.pointerId);

if (!pointer) {
return;
Expand All @@ -537,7 +545,7 @@ export class InputManager implements System {
}

private handlePointerUp(event: PointerEvent): void {
const pointer = this.pointers[event.pointerId];
const pointer = this.pointers.get(event.pointerId);

if (!pointer) {
return;
Expand All @@ -551,7 +559,7 @@ export class InputManager implements System {
}

private handlePointerCancel(event: PointerEvent): void {
const pointer = this.pointers[event.pointerId];
const pointer = this.pointers.get(event.pointerId);

if (!pointer) {
return;
Expand Down Expand Up @@ -803,7 +811,7 @@ export class InputManager implements System {
}

private updatePointerEvents(): void {
for (const pointer of Object.values(this.pointers)) {
for (const pointer of this.pointers.values()) {
const { stateFlags } = pointer;

if (stateFlags.value === PointerStateFlag.None) {
Expand Down Expand Up @@ -844,7 +852,7 @@ export class InputManager implements System {

if (stateFlags.pop(PointerStateFlag.Leave)) {
this.onPointerLeave.dispatch(pointer);
delete this.pointers[pointer.id];
this.pointers.delete(pointer.id);
}
}
}
Expand Down
25 changes: 22 additions & 3 deletions src/math/Polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,24 @@ export class Polygon implements ShapeLike {
}

public project(axis: Vector, result: Interval = new Interval()): Interval {
const normal = axis.clone().normalize();
const projections = this._points.map(point => normal.dot(point.x, point.y));
// Normalize the axis into scalars (project() is public and may receive an
// unnormalized axis; SAT already passes unit normals, so this is a no-op there).
const length = Math.sqrt(axis.x * axis.x + axis.y * axis.y) || 1;
const nx = axis.x / length;
const ny = axis.y / length;

return result.set(Math.min(...projections), Math.max(...projections));
const points = this._points;
let min = Infinity;
let max = -Infinity;

for (let i = 0; i < points.length; i++) {
const projection = nx * points[i].x + ny * points[i].y;

if (projection < min) min = projection;
if (projection > max) max = projection;
}

return result.set(min, max);
}

public contains(x: number, y: number): boolean {
Expand Down Expand Up @@ -288,6 +302,11 @@ export class Polygon implements ShapeLike {
this._edges.length = 0;
}

/**
* Shared scratch `Polygon` instance for intermediate calculations. Never
* retain the reference across frames or async boundaries.
* @internal
*/
public static get temp(): Polygon {
if (temp === null) {
temp = new Polygon();
Expand Down
4 changes: 4 additions & 0 deletions src/math/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export * from './Circle';
export * from './CircleLike';
// `./Collision` provides the collision TYPES (CollisionType, Collidable,
// CollisionResponse) — it does NOT export a `Collision` value despite the
// filename. The `Collision` VALUE below is the query-namespace facade
// (intersects.*/resolve.*) from `./collision-detection`. Keep these distinct.
export * from './Collision';
export { Collision } from './collision-detection';
export * from './Ellipse';
Expand Down
10 changes: 9 additions & 1 deletion src/rendering/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,15 @@ export class Container extends RenderNode {
}

public override contains(x: number, y: number): boolean {
return this._children.some(child => child.contains(x, y));
const children = this._children;

for (let i = 0; i < children.length; i++) {
if (children[i].contains(x, y)) {
return true;
}
}

return false;
}

protected override _invalidateChildrenTransform(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/rendering/webgl2/WebGl2RepeatingSpriteRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,9 @@ export class WebGl2RepeatingSpriteRenderer extends AbstractWebGl2Renderer<Repeat

const view = backend.view;

if (this._currentView !== view || this._currentViewId !== (view as unknown as { updateId: number }).updateId) {
if (this._currentView !== view || this._currentViewId !== view.updateId) {
this._currentView = view;
this._currentViewId = (view as unknown as { updateId: number }).updateId;
this._currentViewId = view.updateId;
const proj = view.getTransform().toArray(false);
this._shaderPathShader.getUniform('u_projection').setValue(proj);
this._geoPathShader.getUniform('u_projection').setValue(proj);
Expand Down
3 changes: 1 addition & 2 deletions src/resources/AssetDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ export type AnyAssetConfig = {
[K in keyof AssetDefinitions]: { type: K } & AssetDefinitions[K]['config'];
}[keyof AssetDefinitions];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AssetInput = AnyAssetConfig | Asset<any>;
export type AssetInput = AnyAssetConfig | Asset<unknown>;

export type InferAssetResource<I extends AssetInput> =
I extends Asset<infer T> ? T : I extends { type: infer K extends keyof AssetDefinitions } ? AssetDefinitions[K]['resource'] : never;
Loading
Loading