@pixi-ui-editor/runtime-pixi 0.5.1 → 0.6.1
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/dist/controls.d.ts +19 -0
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +40 -0
- package/dist/controls.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layoutGroups.js.map +1 -1
- package/dist/pageGroup.d.ts +17 -0
- package/dist/pageGroup.d.ts.map +1 -0
- package/dist/pageGroup.js +20 -0
- package/dist/pageGroup.js.map +1 -0
- package/dist/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +57 -12
- package/dist/particles.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +43 -2
- package/dist/scene.js.map +1 -1
- package/dist/views/NodeView.d.ts +2 -2
- package/dist/views/NodeView.d.ts.map +1 -1
- package/dist/views/NodeView.js.map +1 -1
- package/dist/views/PageGroupNodeView.d.ts +42 -0
- package/dist/views/PageGroupNodeView.d.ts.map +1 -0
- package/dist/views/PageGroupNodeView.js +72 -0
- package/dist/views/PageGroupNodeView.js.map +1 -0
- package/dist/views/PaginationNodeView.d.ts +59 -0
- package/dist/views/PaginationNodeView.d.ts.map +1 -0
- package/dist/views/PaginationNodeView.js +238 -0
- package/dist/views/PaginationNodeView.js.map +1 -0
- package/dist/views/ParticleEmitterNodeView.d.ts +13 -0
- package/dist/views/ParticleEmitterNodeView.d.ts.map +1 -1
- package/dist/views/ParticleEmitterNodeView.js +164 -32
- package/dist/views/ParticleEmitterNodeView.js.map +1 -1
- package/dist/views/SpineNodeView.d.ts +23 -2
- package/dist/views/SpineNodeView.d.ts.map +1 -1
- package/dist/views/SpineNodeView.js +131 -2
- package/dist/views/SpineNodeView.js.map +1 -1
- package/dist/views/basic.d.ts.map +1 -1
- package/dist/views/basic.js +3 -1
- package/dist/views/basic.js.map +1 -1
- package/dist/views/createNodeView.d.ts.map +1 -1
- package/dist/views/createNodeView.js +9 -0
- package/dist/views/createNodeView.js.map +1 -1
- package/package.json +5 -3
- package/dist/assets/spine.test.d.ts +0 -2
- package/dist/assets/spine.test.d.ts.map +0 -1
- package/dist/assets/spine.test.js +0 -42
- package/dist/assets/spine.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -485
- package/dist/index.test.js.map +0 -1
- package/dist/particles.test.d.ts +0 -2
- package/dist/particles.test.d.ts.map +0 -1
- package/dist/particles.test.js +0 -210
- package/dist/particles.test.js.map +0 -1
- package/dist/test.setup.d.ts +0 -2
- package/dist/test.setup.d.ts.map +0 -1
- package/dist/test.setup.js +0 -6
- package/dist/test.setup.js.map +0 -1
- package/dist/views/ParticleEmitterNodeView.test.d.ts +0 -2
- package/dist/views/ParticleEmitterNodeView.test.d.ts.map +0 -1
- package/dist/views/ParticleEmitterNodeView.test.js +0 -148
- package/dist/views/ParticleEmitterNodeView.test.js.map +0 -1
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { Texture } from "pixi.js";
|
|
3
|
-
import { ParticleEmitterNodeView } from "./ParticleEmitterNodeView.js";
|
|
4
|
-
const assetIds = { a: "10000000-0000-4000-8000-000000000001", b: "10000000-0000-4000-8000-000000000002" };
|
|
5
|
-
const textures = new Map([[assetIds.a, Texture.WHITE], [assetIds.b, Texture.EMPTY]]);
|
|
6
|
-
function effect(overrides = {}) {
|
|
7
|
-
return {
|
|
8
|
-
id: "effect-1", name: "Effect", type: "particles", maxParticles: 30, seed: 7,
|
|
9
|
-
emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 24 }] },
|
|
10
|
-
particle: {
|
|
11
|
-
lifetime: { min: 100, max: 100 },
|
|
12
|
-
spawnShape: { type: "point" },
|
|
13
|
-
movement: { speed: { min: 0, max: 0 }, directionDegrees: 0, spreadDegrees: 0, accelerationX: 0, accelerationY: 0, drag: 0 },
|
|
14
|
-
visual: {
|
|
15
|
-
source: { type: "single", assetId: assetIds.a },
|
|
16
|
-
alpha: { start: 1, end: 0 },
|
|
17
|
-
scale: { start: { min: 2, max: 2 }, end: { min: 0.5, max: 0.5 } },
|
|
18
|
-
tint: { start: "#FFFFFF", end: "#FFFFFF" },
|
|
19
|
-
rotation: { initialDegrees: { min: 0, max: 0 }, angularVelocityDegrees: { min: 0, max: 0 } },
|
|
20
|
-
blendMode: "normal",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
...overrides,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function emitterNode(overrides = {}) {
|
|
27
|
-
return {
|
|
28
|
-
id: "node-1", name: "Emitter", parentId: null, children: [], visible: true,
|
|
29
|
-
transform: { x: 0, y: 0, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
30
|
-
type: "particle-emitter", effectId: "effect-1", autoplay: true, simulationSpace: "local",
|
|
31
|
-
...overrides,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function spawnedSprites(view) {
|
|
35
|
-
const contentRoot = view.children[0];
|
|
36
|
-
return contentRoot.children.filter((sprite) => sprite.visible);
|
|
37
|
-
}
|
|
38
|
-
function build(def, node, size = { width: 1000, height: 1000 }) {
|
|
39
|
-
const view = new ParticleEmitterNodeView(def, textures);
|
|
40
|
-
view.update(node, "desktop", size);
|
|
41
|
-
return view;
|
|
42
|
-
}
|
|
43
|
-
describe("ParticleEmitterNodeView spawn shapes and appearance", () => {
|
|
44
|
-
it("samples rectangle spawn points inside the node's logical rect", () => {
|
|
45
|
-
const view = build(effect({ particle: { ...effect().particle, spawnShape: { type: "rectangle" } } }), emitterNode());
|
|
46
|
-
view.step();
|
|
47
|
-
const sprites = spawnedSprites(view);
|
|
48
|
-
expect(sprites).toHaveLength(24);
|
|
49
|
-
for (const sprite of sprites) {
|
|
50
|
-
expect(sprite.position.x).toBeGreaterThanOrEqual(0);
|
|
51
|
-
expect(sprite.position.x).toBeLessThanOrEqual(100);
|
|
52
|
-
expect(sprite.position.y).toBeGreaterThanOrEqual(0);
|
|
53
|
-
expect(sprite.position.y).toBeLessThanOrEqual(60);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
it("samples circle spawn points inside the ellipse inscribed in the rect", () => {
|
|
57
|
-
const view = build(effect({ particle: { ...effect().particle, spawnShape: { type: "circle" } } }), emitterNode());
|
|
58
|
-
view.step();
|
|
59
|
-
for (const sprite of spawnedSprites(view)) {
|
|
60
|
-
const nx = (sprite.position.x - 50) / 50, ny = (sprite.position.y - 30) / 30;
|
|
61
|
-
expect(nx * nx + ny * ny).toBeLessThanOrEqual(1 + 1e-9);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
it("picks among random source assets and interpolates alpha/scale toward their end values by the particle's last frame", () => {
|
|
65
|
-
const random = effect({
|
|
66
|
-
particle: { ...effect().particle, lifetime: { min: 1, max: 1 }, visual: { ...effect().particle.visual, source: { type: "random", assetIds: [assetIds.a, assetIds.b] } } },
|
|
67
|
-
});
|
|
68
|
-
const view = build(random, emitterNode());
|
|
69
|
-
view.step();
|
|
70
|
-
const seen = new Set(spawnedSprites(view).map((sprite) => sprite.texture));
|
|
71
|
-
expect(seen.has(Texture.WHITE)).toBe(true);
|
|
72
|
-
expect(seen.has(Texture.EMPTY)).toBe(true);
|
|
73
|
-
// Advance by fixed steps (update()'s per-call catch-up cap would otherwise clip a large single
|
|
74
|
-
// delta) to 60% through the 1s lifetime, and check the interpolated end values from above.
|
|
75
|
-
for (let i = 0; i < 36; i++)
|
|
76
|
-
view.step();
|
|
77
|
-
const [sprite] = spawnedSprites(view);
|
|
78
|
-
expect(sprite.alpha).toBeCloseTo(0.4, 5); // 60% through life: lerp(1, 0, 0.6) = 0.4
|
|
79
|
-
expect(sprite.scale.x).toBeCloseTo(1.1, 5); // lerp(2, 0.5, 0.6) = 1.1
|
|
80
|
-
});
|
|
81
|
-
it("keeps a non-loop sequence on its last frame once the sequence has fully played", () => {
|
|
82
|
-
const sequence = effect({
|
|
83
|
-
emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 1 }] },
|
|
84
|
-
particle: { ...effect().particle, visual: { ...effect().particle.visual, source: { type: "sequence", assetIds: [assetIds.a, assetIds.b], fps: 10, loop: false, randomStartFrame: false } } },
|
|
85
|
-
});
|
|
86
|
-
const view = build(sequence, emitterNode());
|
|
87
|
-
view.step();
|
|
88
|
-
for (let i = 0; i < 30; i++)
|
|
89
|
-
view.updateParticles(1 / 10); // far more than 2 frames at 10fps
|
|
90
|
-
const [sprite] = spawnedSprites(view);
|
|
91
|
-
expect(sprite.texture).toBe(Texture.EMPTY); // stays on the last frame (assetIds[1])
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe("ParticleEmitterNodeView local/world space", () => {
|
|
95
|
-
it("keeps a local particle following the emitter, while a world particle keeps its world trajectory", () => {
|
|
96
|
-
const def = effect({ emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 1 }] } });
|
|
97
|
-
const localView = build(def, emitterNode({ simulationSpace: "local" }));
|
|
98
|
-
localView.step();
|
|
99
|
-
const worldView = build(def, emitterNode({ simulationSpace: "world" }));
|
|
100
|
-
worldView.step();
|
|
101
|
-
const worldPositionOf = (view) => {
|
|
102
|
-
const [sprite] = spawnedSprites(view);
|
|
103
|
-
return view.worldTransform.apply({ x: sprite.position.x, y: sprite.position.y });
|
|
104
|
-
};
|
|
105
|
-
const localPositionOf = (view) => {
|
|
106
|
-
const [sprite] = spawnedSprites(view);
|
|
107
|
-
return { x: sprite.position.x, y: sprite.position.y };
|
|
108
|
-
};
|
|
109
|
-
const localBefore = localPositionOf(localView);
|
|
110
|
-
const worldBefore = worldPositionOf(worldView);
|
|
111
|
-
// Move both emitters, then resync render (a zero-delta tick just re-projects, it doesn't age).
|
|
112
|
-
localView.update(emitterNode({ simulationSpace: "local", transform: { x: 400, y: 250, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 } }), "desktop", { width: 1000, height: 1000 });
|
|
113
|
-
worldView.update(emitterNode({ simulationSpace: "world", transform: { x: 400, y: 250, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 } }), "desktop", { width: 1000, height: 1000 });
|
|
114
|
-
localView.updateParticles(0);
|
|
115
|
-
worldView.updateParticles(0);
|
|
116
|
-
// Local: the particle's own local coordinates are untouched by the emitter's move.
|
|
117
|
-
expect(localPositionOf(localView)).toEqual(localBefore);
|
|
118
|
-
// World: local coordinates are recomputed so the particle's world position stays put.
|
|
119
|
-
const worldAfter = worldPositionOf(worldView);
|
|
120
|
-
expect(worldAfter.x).toBeCloseTo(worldBefore.x, 5);
|
|
121
|
-
expect(worldAfter.y).toBeCloseTo(worldBefore.y, 5);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
describe("ParticleEmitterNodeView incremental sync and disposal", () => {
|
|
125
|
-
it("does not restart on an unrelated definition clone and adopts a new maxParticles live", () => {
|
|
126
|
-
const def = effect({ maxParticles: 2, emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 2 }] } });
|
|
127
|
-
const view = build(def, emitterNode());
|
|
128
|
-
view.step();
|
|
129
|
-
expect(spawnedSprites(view)).toHaveLength(2);
|
|
130
|
-
const renamed = structuredClone(def);
|
|
131
|
-
renamed.name = "Renamed";
|
|
132
|
-
view.syncEffect(renamed);
|
|
133
|
-
expect(spawnedSprites(view)).toHaveLength(2); // untouched by the sync
|
|
134
|
-
expect(view.getDiagnostics()).toMatchObject({ active: 2, dropped: 0 });
|
|
135
|
-
});
|
|
136
|
-
it("dispose() removes every prewarmed sprite from the render tree and is safe to call from destroy()", () => {
|
|
137
|
-
const def = effect({ maxParticles: 4 });
|
|
138
|
-
const view = build(def, emitterNode());
|
|
139
|
-
view.step();
|
|
140
|
-
expect(spawnedSprites(view)).toHaveLength(24 > 4 ? 4 : 24); // burst count clamps to maxParticles
|
|
141
|
-
const contentRoot = view.children[0];
|
|
142
|
-
expect(contentRoot.children.length).toBeGreaterThan(0);
|
|
143
|
-
view.destroy();
|
|
144
|
-
expect(contentRoot.children).toHaveLength(0);
|
|
145
|
-
expect(view.getDiagnostics()).toMatchObject({ active: 0, disposed: true });
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
//# sourceMappingURL=ParticleEmitterNodeView.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ParticleEmitterNodeView.test.js","sourceRoot":"","sources":["../../src/views/ParticleEmitterNodeView.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from \"vitest\";\r\nimport type { ParticleEffectDefinition, UINode } from \"@pixi-ui-editor/schema\";\r\nimport { Sprite, Texture } from \"pixi.js\";\r\nimport { ParticleEmitterNodeView } from \"./ParticleEmitterNodeView.js\";\r\n\r\nconst assetIds = { a: \"10000000-0000-4000-8000-000000000001\", b: \"10000000-0000-4000-8000-000000000002\" };\r\nconst textures = new Map([[assetIds.a, Texture.WHITE], [assetIds.b, Texture.EMPTY]]);\r\n\r\nfunction effect(overrides: Partial<ParticleEffectDefinition> = {}): ParticleEffectDefinition {\r\n return {\r\n id: \"effect-1\", name: \"Effect\", type: \"particles\", maxParticles: 30, seed: 7,\r\n emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 24 }] },\r\n particle: {\r\n lifetime: { min: 100, max: 100 },\r\n spawnShape: { type: \"point\" },\r\n movement: { speed: { min: 0, max: 0 }, directionDegrees: 0, spreadDegrees: 0, accelerationX: 0, accelerationY: 0, drag: 0 },\r\n visual: {\r\n source: { type: \"single\", assetId: assetIds.a },\r\n alpha: { start: 1, end: 0 },\r\n scale: { start: { min: 2, max: 2 }, end: { min: 0.5, max: 0.5 } },\r\n tint: { start: \"#FFFFFF\", end: \"#FFFFFF\" },\r\n rotation: { initialDegrees: { min: 0, max: 0 }, angularVelocityDegrees: { min: 0, max: 0 } },\r\n blendMode: \"normal\",\r\n },\r\n },\r\n ...overrides,\r\n } as ParticleEffectDefinition;\r\n}\r\n\r\nfunction emitterNode(overrides: Partial<Extract<UINode, { type: \"particle-emitter\" }>> = {}): UINode {\r\n return {\r\n id: \"node-1\", name: \"Emitter\", parentId: null, children: [], visible: true,\r\n transform: { x: 0, y: 0, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 },\r\n type: \"particle-emitter\", effectId: \"effect-1\", autoplay: true, simulationSpace: \"local\",\r\n ...overrides,\r\n } as UINode;\r\n}\r\n\r\nfunction spawnedSprites(view: ParticleEmitterNodeView): Sprite[] {\r\n const contentRoot = view.children[0] as unknown as { children: Sprite[] };\r\n return contentRoot.children.filter((sprite) => sprite.visible);\r\n}\r\n\r\nfunction build(def: ParticleEffectDefinition, node: UINode, size = { width: 1000, height: 1000 }): ParticleEmitterNodeView {\r\n const view = new ParticleEmitterNodeView(def, textures);\r\n view.update(node, \"desktop\", size);\r\n return view;\r\n}\r\n\r\ndescribe(\"ParticleEmitterNodeView spawn shapes and appearance\", () => {\r\n it(\"samples rectangle spawn points inside the node's logical rect\", () => {\r\n const view = build(effect({ particle: { ...effect().particle, spawnShape: { type: \"rectangle\" } } }), emitterNode());\r\n view.step();\r\n const sprites = spawnedSprites(view);\r\n expect(sprites).toHaveLength(24);\r\n for (const sprite of sprites) {\r\n expect(sprite.position.x).toBeGreaterThanOrEqual(0);\r\n expect(sprite.position.x).toBeLessThanOrEqual(100);\r\n expect(sprite.position.y).toBeGreaterThanOrEqual(0);\r\n expect(sprite.position.y).toBeLessThanOrEqual(60);\r\n }\r\n });\r\n\r\n it(\"samples circle spawn points inside the ellipse inscribed in the rect\", () => {\r\n const view = build(effect({ particle: { ...effect().particle, spawnShape: { type: \"circle\" } } }), emitterNode());\r\n view.step();\r\n for (const sprite of spawnedSprites(view)) {\r\n const nx = (sprite.position.x - 50) / 50, ny = (sprite.position.y - 30) / 30;\r\n expect(nx * nx + ny * ny).toBeLessThanOrEqual(1 + 1e-9);\r\n }\r\n });\r\n\r\n it(\"picks among random source assets and interpolates alpha/scale toward their end values by the particle's last frame\", () => {\r\n const random = effect({\r\n particle: { ...effect().particle, lifetime: { min: 1, max: 1 }, visual: { ...effect().particle.visual, source: { type: \"random\", assetIds: [assetIds.a, assetIds.b] } } },\r\n });\r\n const view = build(random, emitterNode());\r\n view.step();\r\n const seen = new Set(spawnedSprites(view).map((sprite) => sprite.texture));\r\n expect(seen.has(Texture.WHITE)).toBe(true);\r\n expect(seen.has(Texture.EMPTY)).toBe(true);\r\n\r\n // Advance by fixed steps (update()'s per-call catch-up cap would otherwise clip a large single\r\n // delta) to 60% through the 1s lifetime, and check the interpolated end values from above.\r\n for (let i = 0; i < 36; i++) view.step();\r\n const [sprite] = spawnedSprites(view);\r\n expect(sprite!.alpha).toBeCloseTo(0.4, 5); // 60% through life: lerp(1, 0, 0.6) = 0.4\r\n expect(sprite!.scale.x).toBeCloseTo(1.1, 5); // lerp(2, 0.5, 0.6) = 1.1\r\n });\r\n\r\n it(\"keeps a non-loop sequence on its last frame once the sequence has fully played\", () => {\r\n const sequence = effect({\r\n emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 1 }] },\r\n particle: { ...effect().particle, visual: { ...effect().particle.visual, source: { type: \"sequence\", assetIds: [assetIds.a, assetIds.b], fps: 10, loop: false, randomStartFrame: false } } },\r\n });\r\n const view = build(sequence, emitterNode());\r\n view.step();\r\n for (let i = 0; i < 30; i++) view.updateParticles(1 / 10); // far more than 2 frames at 10fps\r\n const [sprite] = spawnedSprites(view);\r\n expect(sprite!.texture).toBe(Texture.EMPTY); // stays on the last frame (assetIds[1])\r\n });\r\n});\r\n\r\ndescribe(\"ParticleEmitterNodeView local/world space\", () => {\r\n it(\"keeps a local particle following the emitter, while a world particle keeps its world trajectory\", () => {\r\n const def = effect({ emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 1 }] } });\r\n\r\n const localView = build(def, emitterNode({ simulationSpace: \"local\" }));\r\n localView.step();\r\n const worldView = build(def, emitterNode({ simulationSpace: \"world\" }));\r\n worldView.step();\r\n\r\n const worldPositionOf = (view: ParticleEmitterNodeView) => {\r\n const [sprite] = spawnedSprites(view);\r\n return view.worldTransform.apply({ x: sprite!.position.x, y: sprite!.position.y });\r\n };\r\n const localPositionOf = (view: ParticleEmitterNodeView) => {\r\n const [sprite] = spawnedSprites(view);\r\n return { x: sprite!.position.x, y: sprite!.position.y };\r\n };\r\n\r\n const localBefore = localPositionOf(localView);\r\n const worldBefore = worldPositionOf(worldView);\r\n\r\n // Move both emitters, then resync render (a zero-delta tick just re-projects, it doesn't age).\r\n localView.update(emitterNode({ simulationSpace: \"local\", transform: { x: 400, y: 250, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 } }), \"desktop\", { width: 1000, height: 1000 });\r\n worldView.update(emitterNode({ simulationSpace: \"world\", transform: { x: 400, y: 250, width: 100, height: 60, scaleX: 1, scaleY: 1, rotation: 0 } }), \"desktop\", { width: 1000, height: 1000 });\r\n localView.updateParticles(0);\r\n worldView.updateParticles(0);\r\n\r\n // Local: the particle's own local coordinates are untouched by the emitter's move.\r\n expect(localPositionOf(localView)).toEqual(localBefore);\r\n // World: local coordinates are recomputed so the particle's world position stays put.\r\n const worldAfter = worldPositionOf(worldView);\r\n expect(worldAfter.x).toBeCloseTo(worldBefore.x, 5);\r\n expect(worldAfter.y).toBeCloseTo(worldBefore.y, 5);\r\n });\r\n});\r\n\r\ndescribe(\"ParticleEmitterNodeView incremental sync and disposal\", () => {\r\n it(\"does not restart on an unrelated definition clone and adopts a new maxParticles live\", () => {\r\n const def = effect({ maxParticles: 2, emission: { delay: 0, duration: 10, loop: true, rate: 0, bursts: [{ time: 0, count: 2 }] } });\r\n const view = build(def, emitterNode());\r\n view.step();\r\n expect(spawnedSprites(view)).toHaveLength(2);\r\n\r\n const renamed = structuredClone(def);\r\n renamed.name = \"Renamed\";\r\n view.syncEffect(renamed);\r\n expect(spawnedSprites(view)).toHaveLength(2); // untouched by the sync\r\n\r\n expect(view.getDiagnostics()).toMatchObject({ active: 2, dropped: 0 });\r\n });\r\n\r\n it(\"dispose() removes every prewarmed sprite from the render tree and is safe to call from destroy()\", () => {\r\n const def = effect({ maxParticles: 4 });\r\n const view = build(def, emitterNode());\r\n view.step();\r\n expect(spawnedSprites(view)).toHaveLength(24 > 4 ? 4 : 24); // burst count clamps to maxParticles\r\n\r\n const contentRoot = view.children[0] as unknown as { children: Sprite[] };\r\n expect(contentRoot.children.length).toBeGreaterThan(0);\r\n view.destroy();\r\n expect(contentRoot.children).toHaveLength(0);\r\n expect(view.getDiagnostics()).toMatchObject({ active: 0, disposed: true });\r\n });\r\n});\r\n"]}
|