babylon.quarks 0.27.0 → 0.28.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +425 -0
  2. package/README.md +3 -1
  3. package/dist/babylon.quarks.cjs +1 -1
  4. package/dist/babylon.quarks.cjs.map +1 -1
  5. package/dist/babylon.quarks.esm.js +1 -1
  6. package/dist/babylon.quarks.esm.js.map +1 -1
  7. package/dist/babylon.quarks.umd.min.js +1 -1
  8. package/dist/babylon.quarks.umd.min.js.map +1 -1
  9. package/dist/types/index.d.cts +4256 -831
  10. package/dist/types/index.d.ts +4256 -831
  11. package/package.json +7 -2
  12. package/src/BatchedRenderer.ts +93 -78
  13. package/src/EffectPool.ts +91 -12
  14. package/src/MeshSurfaceEmitter.ts +24 -13
  15. package/src/ParticleEmitter.ts +31 -3
  16. package/src/ParticleSystem.ts +92 -25
  17. package/src/QuarksLoader.ts +289 -171
  18. package/src/QuarksPrefab.ts +301 -94
  19. package/src/QuarksSerializer.ts +43 -15
  20. package/src/QuarksUtil.ts +64 -9
  21. package/src/SpriteBatch.ts +21 -6
  22. package/src/TrailBatch.ts +12 -6
  23. package/src/VFXBatch.ts +70 -528
  24. package/src/batchKey.ts +4 -2
  25. package/src/batchSettings.ts +546 -0
  26. package/src/effectDocumentJSON.ts +261 -0
  27. package/src/effectResources.ts +21 -24
  28. package/src/envAtlas.ts +8 -2
  29. package/src/geometryUtil.ts +1 -1
  30. package/src/index.ts +39 -4
  31. package/src/nodeId.ts +25 -2
  32. package/src/shaderProgram.ts +110 -16
  33. package/src/shaderVariants.ts +24 -4
  34. package/src/shaders/chunks/blend.ts +31 -0
  35. package/src/shaders/chunks/fog.ts +8 -3
  36. package/src/shaders/chunks/soft.ts +6 -5
  37. package/src/shaders/chunks/surface.ts +2 -2
  38. package/src/shaders/particle_frag.glsl.ts +3 -1
  39. package/src/shaders/particle_frag.wgsl.ts +2 -0
  40. package/src/shaders/particle_physics_frag.glsl.ts +5 -2
  41. package/src/shaders/particle_physics_frag.wgsl.ts +2 -0
  42. package/src/shaders/trail_frag.glsl.ts +4 -1
  43. package/src/shaders/trail_frag.wgsl.ts +2 -0
  44. package/src/system/EmissionController.ts +2 -4
  45. package/src/system/EventDispatcher.ts +26 -10
  46. package/src/system/geometry.ts +22 -29
  47. package/src/system/materialResolve.ts +56 -7
  48. package/src/system/normalize.ts +6 -1
  49. package/src/system/serialization.ts +54 -33
  50. package/src/system/settingsSchema.ts +32 -10
  51. package/src/system/types.ts +67 -28
  52. package/src/trailSettings.ts +9 -6
@@ -8,10 +8,12 @@ import {Geometry} from '@babylonjs/core/Meshes/geometry';
8
8
  import {InstancedMesh} from '@babylonjs/core/Meshes/instancedMesh';
9
9
  import {Mesh} from '@babylonjs/core/Meshes/mesh';
10
10
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
11
+ import type {GeometryEntry, QuarksMaterialJSON} from './effectDocumentJSON';
11
12
  import {ensureTriangleIndices} from './geometryUtil';
12
13
  import {quarksNodeId} from './nodeId';
13
14
  import {ParticleEmitter} from './ParticleEmitter';
14
15
  import type {BabylonMetaData, ParticleSystem, ParticleSystemJSONParameters} from './ParticleSystem';
16
+ import type {AnimationJSON} from './QuarksPrefab';
15
17
  import {QuarksPrefab} from './QuarksPrefab';
16
18
  import {textureSourceUrl} from './textureSource';
17
19
 
@@ -83,10 +85,13 @@ export interface QuarksNodeJSON {
83
85
  type: 'Group' | 'ParticleEmitter' | 'Mesh' | 'QuarksPrefab' | string;
84
86
  /** The node's name. */
85
87
  name?: string;
86
- /** Local transform, column-major, as Babylon's `Matrix.asArray()`. */
87
- matrix: number[];
88
- /** Written only when the node is disabled; loads it disabled, with everything under it. */
89
- visible?: false;
88
+ /**
89
+ * Local transform, column-major, as Babylon's `Matrix.asArray()`. Always written; a three.js
90
+ * file without it places the node by `position`, `quaternion` or `rotation`, and `scale`.
91
+ */
92
+ matrix?: number[];
93
+ /** Written only when the node is disabled (false); loads it disabled, with everything under it. */
94
+ visible?: boolean;
90
95
  /** `Mesh`: written only when the mesh is not drawn (`isVisible`), which leaves its children drawn. */
91
96
  isVisible?: false;
92
97
  /** Child nodes, in order. */
@@ -95,9 +100,20 @@ export interface QuarksNodeJSON {
95
100
  ps?: ParticleSystemJSONParameters;
96
101
  /** `Mesh`: the {@link QuarksGeometryJSON} uuid. */
97
102
  geometry?: string;
98
- /** `Mesh`: the material uuid. */
99
- material?: string;
100
- [key: string]: unknown;
103
+ /** `Mesh`: the material uuid; a three.js mesh may list several, of which the first is used. */
104
+ material?: string | string[];
105
+ /** `QuarksPrefab`: the timeline's speed, written only when not 1. */
106
+ timeScale?: number;
107
+ /** `QuarksPrefab`: the timeline's tracks. */
108
+ animationData?: AnimationJSON[];
109
+ /** three.js files without `matrix`: the position as `[x, y, z]`. */
110
+ position?: number[];
111
+ /** three.js files without `matrix`: the scale as `[x, y, z]`. */
112
+ scale?: number[];
113
+ /** three.js files without `matrix`: the rotation as a quaternion `[x, y, z, w]`. */
114
+ quaternion?: number[];
115
+ /** three.js files without `matrix` or `quaternion`: Euler angles `[x, y, z]` in radians. */
116
+ rotation?: number[];
101
117
  }
102
118
 
103
119
  /** A whole effect, as {@link serializeEffect} writes it and `QuarksLoader` reads it. */
@@ -116,7 +132,7 @@ export interface QuarksEffectJSON {
116
132
  /** Meshes the nodes and particle systems use. */
117
133
  geometries: QuarksGeometryJSON[];
118
134
  /** Materials the particle systems and `Mesh` nodes use, referred to by uuid. */
119
- materials: Array<Record<string, unknown>>;
135
+ materials: QuarksMaterialJSON[];
120
136
  /** Textures the particle systems use. */
121
137
  textures: QuarksTextureJSON[];
122
138
  /** Image files the textures load. */
@@ -182,7 +198,7 @@ class EffectWriter {
182
198
  readonly meta: BabylonMetaData = {geometries: {}, materials: {}, textures: {}};
183
199
  private readonly meshGeometries = new Map<string, QuarksGeometryJSON>();
184
200
  private readonly geometryIds = new Map<Geometry, string>();
185
- private readonly meshMaterials: Array<{json: Record<string, unknown>; map: BaseTexture | null}> = [];
201
+ private readonly meshMaterials: Array<{json: QuarksMaterialJSON; map: BaseTexture | null}> = [];
186
202
  private readonly meshMaterialIds = new Map<StandardMaterial, string>();
187
203
  private readonly extraTextures = new Map<BaseTexture, string>();
188
204
  private nextId = 0;
@@ -329,7 +345,7 @@ class EffectWriter {
329
345
  this.warn(`Mesh "${mesh.name}": its material's alpha and colours are not written; it loads opaque white.`);
330
346
  }
331
347
  // Each field only when it differs from what the loader gives a material without it.
332
- const json: Record<string, unknown> = {uuid, type: 'MeshBasicMaterial'};
348
+ const json: QuarksMaterialJSON = {uuid, type: 'MeshBasicMaterial'};
333
349
  if (material.alphaMode !== Constants.ALPHA_COMBINE) json.alphaMode = material.alphaMode;
334
350
  if (material.backFaceCulling) json.side = 0;
335
351
  if (material.alphaCutOff !== 0) json.alphaTest = material.alphaCutOff;
@@ -398,7 +414,7 @@ class EffectWriter {
398
414
 
399
415
  const materials = Object.values(this.meta.materials).map((material) => {
400
416
  // The live Babylon material is for in-memory reuse only.
401
- const {sourceMaterial: _sourceMaterial, ...serializable} = material as Record<string, unknown>;
417
+ const {sourceMaterial: _sourceMaterial, ...serializable} = material;
402
418
  return serializable;
403
419
  });
404
420
 
@@ -409,7 +425,10 @@ class EffectWriter {
409
425
  generator: this.options.generator ?? 'babylon.quarks',
410
426
  quarksFormat: QUARKS_FORMAT_VERSION,
411
427
  },
412
- geometries: [...Object.values(this.meta.geometries), ...this.meshGeometries.values()],
428
+ geometries: [
429
+ ...Object.values(this.meta.geometries).filter(isQuarksGeometryJSON),
430
+ ...this.meshGeometries.values(),
431
+ ],
413
432
  materials: [...materials, ...meshMaterials],
414
433
  textures,
415
434
  images,
@@ -418,18 +437,27 @@ class EffectWriter {
418
437
  }
419
438
  }
420
439
 
440
+ /** Whether a system's geometry entry is inline quarks data, the form its `toJSON` writes. */
441
+ function isQuarksGeometryJSON(entry: GeometryEntry): entry is QuarksGeometryJSON {
442
+ return 'type' in entry && entry.type === 'QuarksGeometry' && Array.isArray(entry.positions);
443
+ }
444
+
421
445
  /** Ids a written document links to by uuid, by the kind of link. */
422
446
  function* links(node: QuarksNodeJSON): Generator<[string, string]> {
423
- const ps = node.ps as {behaviors?: Array<{type?: string; subParticleSystem?: unknown}>; shape?: any} | undefined;
447
+ const ps = node.ps;
424
448
  for (const behavior of ps?.behaviors ?? []) {
425
- if (behavior.type === 'EmitSubParticleSystem' && typeof behavior.subParticleSystem === 'string') {
449
+ if (
450
+ behavior.type === 'EmitSubParticleSystem' &&
451
+ 'subParticleSystem' in behavior &&
452
+ typeof behavior.subParticleSystem === 'string'
453
+ ) {
426
454
  yield ['a sub emitter', behavior.subParticleSystem];
427
455
  }
428
456
  }
429
457
  if (ps?.shape?.type === 'mesh_surface' && typeof ps.shape.mesh === 'string') {
430
458
  yield ['a mesh surface shape', ps.shape.mesh];
431
459
  }
432
- for (const animation of (node.animationData as Array<{targetUUID?: unknown}> | undefined) ?? []) {
460
+ for (const animation of node.animationData ?? []) {
433
461
  if (typeof animation.targetUUID === 'string') {
434
462
  yield ['a prefab animation', animation.targetUUID];
435
463
  }
package/src/QuarksUtil.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import {InstancedMesh} from '@babylonjs/core/Meshes/instancedMesh';
2
+ import {Mesh} from '@babylonjs/core/Meshes/mesh';
1
3
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
4
  import {Node} from '@babylonjs/core/node';
3
5
  import {ConstantValue, EmitSubParticleSystem, FunctionValueGenerator, IntervalValue, ValueGenerator} from 'quarks.core';
4
6
  import {BatchedRenderer} from './BatchedRenderer';
5
7
  import {shareEffectResources} from './effectResources';
8
+ import {MeshSurfaceEmitter} from './MeshSurfaceEmitter';
6
9
  import {ParticleEmitter} from './ParticleEmitter';
7
10
  import {ParticleSystem} from './ParticleSystem';
8
11
 
@@ -27,13 +30,26 @@ export class QuarksUtil {
27
30
  });
28
31
  }
29
32
 
30
- /** Resumes every system under `root`, see {@link ParticleSystem.play}. */
33
+ /**
34
+ * Plays every system under `root`, see {@link ParticleSystem.play}: paused ones resume and
35
+ * finished ones, a short burst inside a longer effect included, start over.
36
+ */
31
37
  static play(root: Node): void {
32
38
  QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
33
39
  (emitter.system as unknown as ParticleSystem).play();
34
40
  });
35
41
  }
36
42
 
43
+ /**
44
+ * Undoes {@link pause} on every system under `root`, see {@link ParticleSystem.resume}: the
45
+ * finished ones stay finished.
46
+ */
47
+ static resume(root: Node): void {
48
+ QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
49
+ (emitter.system as unknown as ParticleSystem).resume();
50
+ });
51
+ }
52
+
37
53
  /** Clears, rewinds and pauses every system under `root`, see {@link ParticleSystem.stop}. */
38
54
  static stop(root: Node): void {
39
55
  QuarksUtil.runOnAllParticleEmitters(root, (emitter) => {
@@ -90,16 +106,46 @@ export class QuarksUtil {
90
106
  /**
91
107
  * A deep copy of an effect: the node tree under `root` with every particle
92
108
  * system cloned, sub emitter links pointing at the copies, and the same
93
- * transforms and enabled state. The copy is not added to any renderer.
94
- * Nodes that are neither transform nodes nor emitters are skipped.
109
+ * transforms and enabled state. Meshes are cloned as meshes that share
110
+ * the source's geometry and material, instances of a mesh in the tree as
111
+ * instances of its copy, and a mesh surface shape that emits from a mesh
112
+ * in the tree emits from its copy. An instance of a mesh outside the tree
113
+ * stays an instance of that mesh, and goes when it is disposed. The copy holds the
114
+ * source's textures and materials, so disposing the source leaves the copy
115
+ * whole. It is not added to any renderer. Nodes that are neither transform
116
+ * nodes nor emitters, such as lights and cameras, are skipped.
95
117
  */
96
118
  static instantiate(root: TransformNode, name?: string): TransformNode {
97
119
  const copies = new Map<ParticleEmitter, ParticleEmitter>();
120
+ const meshCopies = new Map<Node, Mesh>();
121
+ const inTree = new Set<Node>([root, ...root.getDescendants(false)]);
122
+ // A mesh is copied when first needed: where it sits in the tree, or earlier, as the source
123
+ // of an instance copied before it. Its children are copied as every node's are.
124
+ const meshCopyOf = (mesh: Mesh): Mesh => {
125
+ let copy = meshCopies.get(mesh);
126
+ if (copy === undefined) {
127
+ copy = mesh.clone(mesh.name, null, true);
128
+ copy.setEnabled(mesh.isEnabled(false));
129
+ meshCopies.set(mesh, copy);
130
+ }
131
+ return copy;
132
+ };
98
133
  const copyNode = (node: Node, parent: TransformNode | null, copyName?: string): TransformNode | null => {
99
134
  let copy: TransformNode;
100
135
  if (node instanceof ParticleEmitter) {
101
136
  copy = node.clone(copyName);
102
137
  copies.set(node, copy as ParticleEmitter);
138
+ } else if (node instanceof Mesh) {
139
+ copy = meshCopyOf(node);
140
+ if (copyName !== undefined) {
141
+ copy.name = copyName;
142
+ }
143
+ } else if (node instanceof InstancedMesh) {
144
+ // An instance of a mesh in the tree becomes an instance of its copy, so the copy
145
+ // does not go when the source effect is disposed (a mesh disposes its instances).
146
+ const source = inTree.has(node.sourceMesh) ? meshCopyOf(node.sourceMesh) : undefined;
147
+ copy = node.clone(copyName ?? node.name, null, true, source)!;
148
+ copy.setEnabled(node.isEnabled(false));
103
149
  } else if (node instanceof TransformNode) {
104
150
  copy = new TransformNode(copyName ?? node.name, node.getScene());
105
151
  copy.position.copyFrom(node.position);
@@ -119,11 +165,12 @@ export class QuarksUtil {
119
165
  return copy;
120
166
  };
121
167
  const copy = copyNode(root, null, name) as TransformNode;
122
- // The copy draws with the source's textures, so QuarksLoader.disposeEffect(source) keeps them.
168
+ // The copy draws with the source's textures and materials, so disposeEffect(source) keeps them.
123
169
  shareEffectResources(root, copy);
124
- // Sub emitter behaviors on the copies still target the original emitters.
125
170
  for (const emitter of copies.values()) {
126
- for (const behavior of (emitter.system as unknown as ParticleSystem).behaviors) {
171
+ const system = emitter.system as unknown as ParticleSystem;
172
+ // Sub emitter behaviors on the copies still target the original emitters.
173
+ for (const behavior of system.behaviors) {
127
174
  if (behavior instanceof EmitSubParticleSystem) {
128
175
  const target = copies.get(behavior.subParticleSystem as unknown as ParticleEmitter);
129
176
  if (target !== undefined) {
@@ -131,6 +178,14 @@ export class QuarksUtil {
131
178
  }
132
179
  }
133
180
  }
181
+ // So does a mesh surface shape: it emits from the copy of its mesh when the tree has one.
182
+ const shape = system.emitterShape;
183
+ if (shape instanceof MeshSurfaceEmitter && shape.mesh !== undefined) {
184
+ const meshCopy = meshCopies.get(shape.mesh);
185
+ if (meshCopy !== undefined) {
186
+ shape.mesh = meshCopy;
187
+ }
188
+ }
134
189
  }
135
190
  return copy;
136
191
  }
@@ -145,7 +200,8 @@ export class QuarksUtil {
145
200
  * {@link endEmit}, or when one is frozen at playback speed 0.
146
201
  *
147
202
  * Random values count at their maximum, so this is an upper bound; curves
148
- * are sampled. Particles cut short by collisions or culling only make the
203
+ * are sampled. A runtime {@link ParticleSystem.timeDilation} is not counted:
204
+ * this is the effect's own length. Particles cut short by collisions or culling only make the
149
205
  * real effect shorter.
150
206
  */
151
207
  static getDuration(root: Node): number {
@@ -162,8 +218,7 @@ export class QuarksUtil {
162
218
  continue;
163
219
  }
164
220
  const target = (behavior.subParticleSystem as unknown as ParticleEmitter | undefined)?.system as
165
- | ParticleSystem
166
- | undefined;
221
+ ParticleSystem | undefined;
167
222
  if (target !== undefined && typeof target === 'object') {
168
223
  if (!systems.includes(target)) {
169
224
  systems.push(target);
@@ -37,6 +37,13 @@ export class SpriteBatch extends VFXBatch {
37
37
  texture = RawTexture.CreateRGBATexture(new Uint8Array([255, 255, 255, 255]), 1, 1, scene);
38
38
  texture.name = 'quarksMeshWhite';
39
39
  SpriteBatch.whiteTextureByScene.set(scene, texture);
40
+ // Disposed from outside (a scene-wide texture sweep): the next batch makes a new one.
41
+ const made = texture;
42
+ texture.onDisposeObservable.addOnce(() => {
43
+ if (SpriteBatch.whiteTextureByScene.get(scene) === made) {
44
+ SpriteBatch.whiteTextureByScene.delete(scene);
45
+ }
46
+ });
40
47
  }
41
48
  return texture;
42
49
  }
@@ -67,14 +74,14 @@ export class SpriteBatch extends VFXBatch {
67
74
  constructor(settings: VFXBatchSettings, scene: Scene, depth: DepthBinding | null = null) {
68
75
  super(settings, scene, depth);
69
76
  this.storeFixedLighting(defaultMeshLighting());
70
- this.maxParticles = 1000;
77
+ this.maxParticles = this.initialCapacity;
71
78
  this.setupBuffers();
72
79
  this.rebuildMaterial();
73
80
  }
74
81
 
75
82
  /** @internal */
76
83
  setupBuffers(): void {
77
- this.mesh.dispose();
84
+ this.mesh?.dispose();
78
85
  this.mesh = new Mesh('spriteBatch', this.scene);
79
86
  this.configureMesh();
80
87
 
@@ -178,11 +185,19 @@ export class SpriteBatch extends VFXBatch {
178
185
  this.mesh.doNotSyncBoundingInfo = true;
179
186
  }
180
187
 
188
+ /** @internal */
189
+ protected readonly initialCapacity = 1000;
190
+
181
191
  /** @internal */
182
192
  expandBuffers(target: number): void {
183
193
  while (target >= this.maxParticles) {
184
194
  this.maxParticles *= 2;
185
195
  }
196
+ this.resizeBuffers();
197
+ }
198
+
199
+ /** @internal */
200
+ protected resizeBuffers(): void {
186
201
  this.disposeExpandableBuffers();
187
202
  this.buildExpandableBuffers();
188
203
  this.sortScratch = undefined;
@@ -393,7 +408,9 @@ export class SpriteBatch extends VFXBatch {
393
408
 
394
409
  this.bindFog(mat);
395
410
  this.bindSurface(mat);
396
- mat.backFaceCulling = false;
411
+ // Billboards, beams and the like are flat sheets: both faces, whatever the setting says.
412
+ mat.backFaceCulling = this.settings.renderFace !== 'both';
413
+ mat.cullBackFaces = this.settings.renderFace !== 'back';
397
414
 
398
415
  if (this.settings.materialTransparent) {
399
416
  mat.alphaMode = this.settings.materialBlendMode;
@@ -532,9 +549,7 @@ export class SpriteBatch extends VFXBatch {
532
549
  for (let i = 0; i < visibleSystems.length; i++) {
533
550
  particleCount += visibleSystems[i].particleNum;
534
551
  }
535
- if (particleCount > this.maxParticles) {
536
- this.expandBuffers(particleCount);
537
- }
552
+ this.fitBuffers(particleCount);
538
553
 
539
554
  for (let s = 0; s < visibleSystems.length; s++) {
540
555
  const system = visibleSystems[s];
package/src/TrailBatch.ts CHANGED
@@ -121,14 +121,14 @@ export class TrailBatch extends VFXBatch {
121
121
  // For a trail batch `maxParticles` counts vertices, two per drawn point: the buffers are
122
122
  // the largest per-batch allocation in the library, so start small and let expandBuffers
123
123
  // grow to whatever the batch actually needs.
124
- this.maxParticles = 1024;
124
+ this.maxParticles = this.initialCapacity;
125
125
  this.setupBuffers();
126
126
  this.rebuildMaterial();
127
127
  }
128
128
 
129
129
  /** @internal */
130
130
  setupBuffers(): void {
131
- this.mesh.dispose();
131
+ this.mesh?.dispose();
132
132
  this.mesh = new Mesh('trailBatch', this.scene);
133
133
  this.configureMesh();
134
134
  this.buildGeometryBuffers();
@@ -210,11 +210,19 @@ export class TrailBatch extends VFXBatch {
210
210
  }
211
211
  }
212
212
 
213
+ /** @internal */
214
+ protected readonly initialCapacity = 1024;
215
+
213
216
  /** Grows the buffers to hold more than `target` vertices. @internal */
214
217
  expandBuffers(target: number): void {
215
218
  while (target >= this.maxParticles) {
216
219
  this.maxParticles *= 2;
217
220
  }
221
+ this.resizeBuffers();
222
+ }
223
+
224
+ /** @internal */
225
+ protected resizeBuffers(): void {
218
226
  // `Geometry.setVerticesBuffer` disposes the buffer it replaces; these custom attributes
219
227
  // are not in the geometry's map, so they are released here.
220
228
  this.previousVB?.dispose();
@@ -518,9 +526,7 @@ export class TrailBatch extends VFXBatch {
518
526
  particleCount += (historyCount * (tessellation + 1) + 1) * 2;
519
527
  }
520
528
  }
521
- if (particleCount > this.maxParticles) {
522
- this.expandBuffers(particleCount);
523
- }
529
+ this.fitBuffers(particleCount);
524
530
 
525
531
  for (let s = 0; s < visibleSystems.length; s++) {
526
532
  const system = visibleSystems[s];
@@ -969,7 +975,7 @@ export class TrailBatch extends VFXBatch {
969
975
  private adoptMeshBounds(): void {
970
976
  const meshBoundingInfo = this.mesh.getBoundingInfo();
971
977
  for (const subMesh of this.mesh.subMeshes) {
972
- (subMesh as any)._boundingInfo = meshBoundingInfo;
978
+ subMesh.setBoundingInfo(meshBoundingInfo);
973
979
  }
974
980
  }
975
981