@multiplekex/shallot 0.1.7 → 0.1.10

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.
@@ -1,3 +1,4 @@
1
+ import { Not } from "bitecs";
1
2
  import type { Plugin, State, System } from "../../core";
2
3
  import { MAX_ENTITIES, resource } from "../../core";
3
4
  import { Compute, ComputePlugin } from "../compute";
@@ -6,33 +7,48 @@ import { Camera, Tonemap, FXAA, Vignette, uploadCamera } from "./camera";
6
7
  import { AmbientLight, DirectionalLight, packLightUniforms } from "./light";
7
8
  import {
8
9
  Mesh,
10
+ MeshShapes,
9
11
  MeshColors,
10
12
  MeshSizes,
11
- collectByShape,
13
+ MeshPBR,
14
+ MeshEmission,
15
+ collectByShapeAndSurface,
12
16
  updateBatches,
17
+ MAX_BATCH_SLOTS,
13
18
  type ShapeBatch,
14
19
  type MeshBuffers,
15
20
  } from "./mesh";
21
+ import { Surface, SurfaceIds, SurfaceType } from "./surface";
16
22
  import { createSceneBuffer, ensureRenderTextures } from "./scene";
17
23
  import { createForwardNode, createIndirectBuffer } from "./forward";
18
24
  import { createPostProcessNode, type PostProcessUniforms } from "./postprocess";
25
+ import { createTransparentNode } from "./transparent";
26
+ import { Draws, type DrawState } from "./pass";
19
27
 
20
28
  export * from "./camera";
21
29
  export * from "./light";
22
30
  export * from "./mesh";
31
+ export * from "./surface";
23
32
  export * from "./scene";
24
33
  export * from "./forward";
25
34
  export * from "./postprocess";
35
+ export * from "./transparent";
36
+ export * from "./pass";
26
37
 
27
38
  export interface RenderState {
28
39
  scene: GPUBuffer;
29
40
  matrices: GPUBuffer;
30
41
  colors: GPUBuffer;
31
42
  sizes: GPUBuffer;
43
+ shapes: GPUBuffer;
44
+ pbr: GPUBuffer;
45
+ emission: GPUBuffer;
46
+ surfaceIds: GPUBuffer;
32
47
  indirect: GPUBuffer;
33
- batches: Map<number, ShapeBatch>;
48
+ batches: Map<string, ShapeBatch>;
34
49
  buffers: Map<number, MeshBuffers>;
35
50
  postProcess: PostProcessUniforms;
51
+ entityCount: number;
36
52
  }
37
53
 
38
54
  export const Render = resource<RenderState>("render");
@@ -60,7 +76,7 @@ const RenderSystem: System = {
60
76
 
61
77
  for (const eid of state.query([Camera])) {
62
78
  if (Camera.active[eid]) {
63
- uploadCamera(device, render.scene, eid, width / height);
79
+ uploadCamera(device, render.scene, eid, width, height);
64
80
 
65
81
  render.postProcess.tonemap = state.hasComponent(eid, Tonemap);
66
82
  if (render.postProcess.tonemap) {
@@ -71,6 +87,8 @@ const RenderSystem: System = {
71
87
  render.postProcess.vignetteStrength = Vignette.strength[eid];
72
88
  render.postProcess.vignetteInner = Vignette.inner[eid];
73
89
  render.postProcess.vignetteOuter = Vignette.outer[eid];
90
+ } else {
91
+ render.postProcess.vignetteStrength = 0;
74
92
  }
75
93
  break;
76
94
  }
@@ -107,23 +125,57 @@ const RenderSystem: System = {
107
125
  const lightUniforms = packLightUniforms(ambientData, directionalData);
108
126
  device.queue.writeBuffer(render.scene, 128, lightUniforms as Float32Array<ArrayBuffer>);
109
127
 
128
+ render.entityCount = state.maxEid + 1;
129
+ const uploadCount = render.entityCount;
110
130
  device.queue.writeBuffer(
111
131
  render.matrices,
112
132
  0,
113
- WorldTransform.data as Float32Array<ArrayBuffer>
133
+ WorldTransform.data as Float32Array<ArrayBuffer>,
134
+ 0,
135
+ uploadCount * 64
114
136
  );
115
137
 
116
138
  const meshEntities = state.query([Mesh, WorldTransform]);
117
- const byShape = collectByShape(meshEntities);
118
- device.queue.writeBuffer(render.colors, 0, MeshColors.data);
119
- device.queue.writeBuffer(render.sizes, 0, MeshSizes.data);
120
- updateBatches(device, byShape, render, render.indirect);
139
+ device.queue.writeBuffer(render.colors, 0, MeshColors.data, 0, uploadCount * 16);
140
+ device.queue.writeBuffer(render.sizes, 0, MeshSizes.data, 0, uploadCount * 16);
141
+ device.queue.writeBuffer(render.shapes, 0, MeshShapes.data, 0, uploadCount * 4);
142
+ device.queue.writeBuffer(render.pbr, 0, MeshPBR.data, 0, uploadCount * 16);
143
+ device.queue.writeBuffer(render.emission, 0, MeshEmission.data, 0, uploadCount * 16);
144
+
145
+ for (const eid of state.query([Surface])) {
146
+ SurfaceIds.data[eid] = Surface.type[eid];
147
+ }
148
+ device.queue.writeBuffer(render.surfaceIds, 0, SurfaceIds.data, 0, uploadCount * 4);
149
+
150
+ const byShapeAndSurface = collectByShapeAndSurface(
151
+ meshEntities,
152
+ (eid) => Surface.type[eid] ?? SurfaceType.Default
153
+ );
154
+ updateBatches(device, byShapeAndSurface, render, render.indirect);
155
+ },
156
+ };
157
+
158
+ const DefaultSurfaceSystem: System = {
159
+ group: "setup",
160
+ update(state: State) {
161
+ for (const eid of state.query([Mesh, Not(Surface)])) {
162
+ state.addComponent(eid, Surface);
163
+ }
121
164
  },
122
165
  };
123
166
 
124
167
  export const RenderPlugin: Plugin = {
125
- systems: [RenderSystem],
126
- components: { Camera, Mesh, AmbientLight, DirectionalLight, Tonemap, FXAA, Vignette },
168
+ systems: [DefaultSurfaceSystem, RenderSystem],
169
+ components: {
170
+ Camera,
171
+ Mesh,
172
+ Surface,
173
+ AmbientLight,
174
+ DirectionalLight,
175
+ Tonemap,
176
+ FXAA,
177
+ Vignette,
178
+ },
127
179
  dependencies: [ComputePlugin],
128
180
 
129
181
  initialize(state: State) {
@@ -144,9 +196,14 @@ export const RenderPlugin: Plugin = {
144
196
  matrices: createPropertyBuffer(MAX_ENTITIES * 64),
145
197
  colors: createPropertyBuffer(MAX_ENTITIES * 16),
146
198
  sizes: createPropertyBuffer(MAX_ENTITIES * 16),
147
- indirect: createIndirectBuffer(device, 16),
199
+ shapes: createPropertyBuffer(MAX_ENTITIES * 4),
200
+ pbr: createPropertyBuffer(MAX_ENTITIES * 16),
201
+ emission: createPropertyBuffer(MAX_ENTITIES * 16),
202
+ surfaceIds: createPropertyBuffer(MAX_ENTITIES * 4),
203
+ indirect: createIndirectBuffer(device, MAX_BATCH_SLOTS),
148
204
  batches: new Map(),
149
205
  buffers: new Map(),
206
+ entityCount: 1,
150
207
  postProcess: {
151
208
  tonemap: false,
152
209
  exposure: 1.0,
@@ -159,17 +216,33 @@ export const RenderPlugin: Plugin = {
159
216
 
160
217
  state.setResource(Render, renderState);
161
218
 
219
+ const drawState: DrawState = {
220
+ draws: new Map(),
221
+ };
222
+ state.setResource(Draws, drawState);
223
+
162
224
  compute.graph.add(
163
225
  createForwardNode({
226
+ state,
164
227
  scene: renderState.scene,
165
228
  matrices: renderState.matrices,
166
229
  colors: renderState.colors,
167
230
  sizes: renderState.sizes,
231
+ shapes: renderState.shapes,
232
+ pbr: renderState.pbr,
233
+ emission: renderState.emission,
168
234
  indirect: renderState.indirect,
169
235
  batches: renderState.batches,
170
236
  })
171
237
  );
172
238
 
173
- compute.graph.add(createPostProcessNode(renderState.postProcess));
239
+ compute.graph.add(createTransparentNode({ state }));
240
+
241
+ compute.graph.add(
242
+ createPostProcessNode({
243
+ state,
244
+ uniforms: renderState.postProcess,
245
+ })
246
+ );
174
247
  },
175
248
  };
@@ -6,6 +6,10 @@ import { createBox } from "./box";
6
6
  import { createSphere } from "./sphere";
7
7
  import { createPlane } from "./plane";
8
8
 
9
+ export const MAX_SURFACES = 16;
10
+ export const MAX_BATCH_SLOTS = 64;
11
+ export const INVALID_SHAPE = 0xffffffff;
12
+
9
13
  export interface MeshData {
10
14
  vertices: Float32Array<ArrayBuffer>;
11
15
  indices: Uint16Array<ArrayBuffer>;
@@ -13,23 +17,43 @@ export interface MeshData {
13
17
  indexCount: number;
14
18
  }
15
19
 
20
+ const meshes: MeshData[] = [];
21
+
22
+ function initBuiltIns(): void {
23
+ if (meshes.length === 0) {
24
+ meshes.push(createBox());
25
+ meshes.push(createSphere());
26
+ meshes.push(createPlane());
27
+ }
28
+ }
29
+
30
+ initBuiltIns();
31
+
16
32
  export const MeshShape = {
17
33
  Box: 0,
18
34
  Sphere: 1,
19
35
  Plane: 2,
20
36
  } as const;
21
37
 
22
- export function createGeometry(shape: number): MeshData {
23
- switch (shape) {
24
- case MeshShape.Sphere:
25
- return createSphere();
26
- case MeshShape.Plane:
27
- return createPlane();
28
- default:
29
- return createBox();
30
- }
38
+ export function mesh(data: MeshData): number {
39
+ const id = meshes.length;
40
+ meshes.push(data);
41
+ return id;
42
+ }
43
+
44
+ export function getMesh(id: number): MeshData | undefined {
45
+ return meshes[id];
31
46
  }
32
47
 
48
+ export function clearMeshes(): void {
49
+ meshes.length = 0;
50
+ initBuiltIns();
51
+ }
52
+
53
+ export const MeshShapes = {
54
+ data: new Uint32Array(MAX_ENTITIES).fill(INVALID_SHAPE),
55
+ };
56
+
33
57
  export const MeshColors = {
34
58
  data: new Float32Array(MAX_ENTITIES * 4),
35
59
  };
@@ -38,6 +62,21 @@ export const MeshSizes = {
38
62
  data: new Float32Array(MAX_ENTITIES * 4),
39
63
  };
40
64
 
65
+ export const MeshPBR = {
66
+ data: new Float32Array(MAX_ENTITIES * 4),
67
+ };
68
+
69
+ export const MeshEmission = {
70
+ data: new Float32Array(MAX_ENTITIES * 4),
71
+ };
72
+
73
+ function initPBRDefaults(): void {
74
+ for (let i = 0; i < MAX_ENTITIES; i++) {
75
+ MeshPBR.data[i * 4] = 0.9;
76
+ MeshPBR.data[i * 4 + 1] = 0.0;
77
+ }
78
+ }
79
+
41
80
  interface ColorProxy extends Array<number>, FieldAccessor {}
42
81
 
43
82
  function colorProxy(): ColorProxy {
@@ -76,6 +115,36 @@ function colorProxy(): ColorProxy {
76
115
  });
77
116
  }
78
117
 
118
+ interface ColorChannelProxy extends Array<number>, FieldAccessor {}
119
+
120
+ function colorChannelProxy(channelIndex: number): ColorChannelProxy {
121
+ const data = MeshColors.data;
122
+
123
+ function getValue(eid: number): number {
124
+ return data[eid * 4 + channelIndex];
125
+ }
126
+
127
+ function setValue(eid: number, value: number): void {
128
+ data[eid * 4 + channelIndex] = value;
129
+ }
130
+
131
+ return new Proxy([] as unknown as ColorChannelProxy, {
132
+ get(_, prop) {
133
+ if (prop === "get") return getValue;
134
+ if (prop === "set") return setValue;
135
+ const eid = Number(prop);
136
+ if (Number.isNaN(eid)) return undefined;
137
+ return getValue(eid);
138
+ },
139
+ set(_, prop, value) {
140
+ const eid = Number(prop);
141
+ if (Number.isNaN(eid)) return false;
142
+ setValue(eid, value);
143
+ return true;
144
+ },
145
+ });
146
+ }
147
+
79
148
  interface SizeProxy extends Array<number>, FieldAccessor {}
80
149
 
81
150
  function sizeProxy(component: number): SizeProxy {
@@ -106,18 +175,128 @@ function sizeProxy(component: number): SizeProxy {
106
175
  });
107
176
  }
108
177
 
178
+ interface PBRProxy extends Array<number>, FieldAccessor {}
179
+
180
+ function pbrProxy(component: number, defaultValue: number): PBRProxy {
181
+ const data = MeshPBR.data;
182
+
183
+ function getValue(eid: number): number {
184
+ const val = data[eid * 4 + component];
185
+ return val === 0 && component === 0 ? defaultValue : val;
186
+ }
187
+
188
+ function setValue(eid: number, value: number): void {
189
+ data[eid * 4 + component] = value;
190
+ }
191
+
192
+ return new Proxy([] as unknown as PBRProxy, {
193
+ get(_, prop) {
194
+ if (prop === "get") return getValue;
195
+ if (prop === "set") return setValue;
196
+ const eid = Number(prop);
197
+ if (Number.isNaN(eid)) return undefined;
198
+ return getValue(eid);
199
+ },
200
+ set(_, prop, value) {
201
+ const eid = Number(prop);
202
+ if (Number.isNaN(eid)) return false;
203
+ setValue(eid, value);
204
+ return true;
205
+ },
206
+ });
207
+ }
208
+
209
+ interface EmissionProxy extends Array<number>, FieldAccessor {}
210
+
211
+ function emissionProxy(): EmissionProxy {
212
+ const data = MeshEmission.data;
213
+
214
+ function getValue(eid: number): number {
215
+ const offset = eid * 4;
216
+ const r = Math.round(data[offset] * 255);
217
+ const g = Math.round(data[offset + 1] * 255);
218
+ const b = Math.round(data[offset + 2] * 255);
219
+ return (r << 16) | (g << 8) | b;
220
+ }
221
+
222
+ function setValue(eid: number, value: number): void {
223
+ const offset = eid * 4;
224
+ data[offset] = ((value >> 16) & 0xff) / 255;
225
+ data[offset + 1] = ((value >> 8) & 0xff) / 255;
226
+ data[offset + 2] = (value & 0xff) / 255;
227
+ }
228
+
229
+ return new Proxy([] as unknown as EmissionProxy, {
230
+ get(_, prop) {
231
+ if (prop === "get") return getValue;
232
+ if (prop === "set") return setValue;
233
+ const eid = Number(prop);
234
+ if (Number.isNaN(eid)) return undefined;
235
+ return getValue(eid);
236
+ },
237
+ set(_, prop, value) {
238
+ const eid = Number(prop);
239
+ if (Number.isNaN(eid)) return false;
240
+ setValue(eid, value);
241
+ return true;
242
+ },
243
+ });
244
+ }
245
+
246
+ function emissionIntensityProxy(): PBRProxy {
247
+ const data = MeshEmission.data;
248
+
249
+ function getValue(eid: number): number {
250
+ return data[eid * 4 + 3];
251
+ }
252
+
253
+ function setValue(eid: number, value: number): void {
254
+ data[eid * 4 + 3] = value;
255
+ }
256
+
257
+ return new Proxy([] as unknown as PBRProxy, {
258
+ get(_, prop) {
259
+ if (prop === "get") return getValue;
260
+ if (prop === "set") return setValue;
261
+ const eid = Number(prop);
262
+ if (Number.isNaN(eid)) return undefined;
263
+ return getValue(eid);
264
+ },
265
+ set(_, prop, value) {
266
+ const eid = Number(prop);
267
+ if (Number.isNaN(eid)) return false;
268
+ setValue(eid, value);
269
+ return true;
270
+ },
271
+ });
272
+ }
273
+
109
274
  export const Mesh: {
110
- shape: number[];
275
+ shape: Uint32Array;
111
276
  color: ColorProxy;
277
+ colorR: ColorChannelProxy;
278
+ colorG: ColorChannelProxy;
279
+ colorB: ColorChannelProxy;
112
280
  sizeX: SizeProxy;
113
281
  sizeY: SizeProxy;
114
282
  sizeZ: SizeProxy;
283
+ roughness: PBRProxy;
284
+ metallic: PBRProxy;
285
+ emission: EmissionProxy;
286
+ emissionIntensity: PBRProxy;
115
287
  } = {
116
- shape: [],
288
+ shape: MeshShapes.data,
117
289
  color: colorProxy(),
290
+ colorR: colorChannelProxy(0),
291
+ colorG: colorChannelProxy(1),
292
+ colorB: colorChannelProxy(2),
118
293
  sizeX: sizeProxy(0),
119
294
  sizeY: sizeProxy(1),
120
295
  sizeZ: sizeProxy(2),
296
+ roughness: pbrProxy(0, 0.9),
297
+ metallic: pbrProxy(1, 0.0),
298
+ emission: emissionProxy(),
299
+ emissionIntensity: emissionIntensityProxy(),
121
300
  };
122
301
 
123
302
  setTraits(Mesh, {
@@ -127,12 +306,23 @@ setTraits(Mesh, {
127
306
  sizeX: 1,
128
307
  sizeY: 1,
129
308
  sizeZ: 1,
309
+ roughness: 0.9,
310
+ metallic: 0.0,
311
+ emission: 0x000000,
312
+ emissionIntensity: 0.0,
130
313
  }),
131
314
  accessors: {
132
315
  color: Mesh.color,
316
+ colorR: Mesh.colorR,
317
+ colorG: Mesh.colorG,
318
+ colorB: Mesh.colorB,
133
319
  sizeX: Mesh.sizeX,
134
320
  sizeY: Mesh.sizeY,
135
321
  sizeZ: Mesh.sizeZ,
322
+ roughness: Mesh.roughness,
323
+ metallic: Mesh.metallic,
324
+ emission: Mesh.emission,
325
+ emissionIntensity: Mesh.emissionIntensity,
136
326
  },
137
327
  });
138
328
 
@@ -160,53 +350,84 @@ export function createMeshBuffers(device: GPUDevice, mesh: MeshData): MeshBuffer
160
350
  return { vertex, index, indexCount: mesh.indexCount };
161
351
  }
162
352
 
163
- export function collectByShape(entities: Iterable<number>): Map<number, number[]> {
164
- const byShape = new Map<number, number[]>();
353
+ export interface BatchKey {
354
+ shape: number;
355
+ surface: number;
356
+ }
357
+
358
+ export function batchKeyString(key: BatchKey): string {
359
+ return `${key.shape}:${key.surface}`;
360
+ }
361
+
362
+ export function collectByShapeAndSurface(
363
+ entities: Iterable<number>,
364
+ getSurface: (eid: number) => number
365
+ ): Map<string, { key: BatchKey; entities: number[] }> {
366
+ const batches = new Map<string, { key: BatchKey; entities: number[] }>();
165
367
  for (const eid of entities) {
166
368
  const shape = Mesh.shape[eid];
167
- let list = byShape.get(shape);
168
- if (!list) {
169
- list = [];
170
- byShape.set(shape, list);
369
+ const surface = getSurface(eid);
370
+ const key: BatchKey = { shape, surface };
371
+ const keyStr = batchKeyString(key);
372
+ let entry = batches.get(keyStr);
373
+ if (!entry) {
374
+ entry = { key, entities: [] };
375
+ batches.set(keyStr, entry);
171
376
  }
172
- list.push(eid);
377
+ entry.entities.push(eid);
173
378
  }
174
- return byShape;
379
+ return batches;
175
380
  }
176
381
 
177
382
  export interface ShapeBatch {
178
383
  index: number;
384
+ shape: number;
385
+ surface: number;
179
386
  buffers: MeshBuffers;
180
387
  entityIds: GPUBuffer;
181
388
  count: number;
182
389
  }
183
390
 
184
391
  export interface BatchState {
185
- batches: Map<number, ShapeBatch>;
392
+ batches: Map<string, ShapeBatch>;
186
393
  buffers: Map<number, MeshBuffers>;
187
394
  }
188
395
 
189
396
  export function updateBatches(
190
397
  device: GPUDevice,
191
- byShape: Map<number, number[]>,
398
+ byShapeAndSurface: Map<string, { key: BatchKey; entities: number[] }>,
192
399
  state: BatchState,
193
400
  indirect: GPUBuffer
194
401
  ): void {
195
- for (const [shape, entities] of byShape) {
196
- let batch = state.batches.get(shape);
402
+ const activeBatches = new Set<string>();
403
+
404
+ for (const [keyStr, { key, entities }] of byShapeAndSurface) {
405
+ activeBatches.add(keyStr);
406
+
407
+ let batch = state.batches.get(keyStr);
197
408
  if (!batch) {
198
- let buffers = state.buffers.get(shape);
409
+ const batchIndex = key.shape * MAX_SURFACES + key.surface;
410
+ if (batchIndex >= MAX_BATCH_SLOTS) {
411
+ console.warn(
412
+ `Batch index ${batchIndex} exceeds limit (${MAX_BATCH_SLOTS}), skipping batch`
413
+ );
414
+ continue;
415
+ }
416
+ let buffers = state.buffers.get(key.shape);
199
417
  if (!buffers) {
200
- buffers = createMeshBuffers(device, createGeometry(shape));
201
- state.buffers.set(shape, buffers);
418
+ const data = getMesh(key.shape) ?? getMesh(MeshShape.Box)!;
419
+ buffers = createMeshBuffers(device, data);
420
+ state.buffers.set(key.shape, buffers);
202
421
  }
203
422
  batch = {
204
- index: state.batches.size,
423
+ index: batchIndex,
424
+ shape: key.shape,
425
+ surface: key.surface,
205
426
  buffers,
206
427
  entityIds: createEntityIdBuffer(device, MAX_ENTITIES),
207
428
  count: 0,
208
429
  };
209
- state.batches.set(shape, batch);
430
+ state.batches.set(keyStr, batch);
210
431
  }
211
432
 
212
433
  device.queue.writeBuffer(batch.entityIds, 0, new Uint32Array(entities));
@@ -220,6 +441,12 @@ export function updateBatches(
220
441
  firstInstance: 0,
221
442
  });
222
443
  }
444
+
445
+ for (const keyStr of state.batches.keys()) {
446
+ if (!activeBatches.has(keyStr)) {
447
+ state.batches.delete(keyStr);
448
+ }
449
+ }
223
450
  }
224
451
 
225
452
  export { createBox } from "./box";
@@ -0,0 +1,63 @@
1
+ import { resource, type State } from "../../core";
2
+ import { Pass } from "../compute/pass";
3
+
4
+ export { Pass };
5
+
6
+ export interface DrawContext {
7
+ readonly device: GPUDevice;
8
+ readonly encoder: GPUCommandEncoder;
9
+ readonly format: GPUTextureFormat;
10
+ readonly width: number;
11
+ readonly height: number;
12
+
13
+ readonly sceneView: GPUTextureView;
14
+ readonly depthView: GPUTextureView;
15
+ readonly entityIdView: GPUTextureView;
16
+ readonly maskView: GPUTextureView;
17
+ readonly canvasView: GPUTextureView;
18
+
19
+ readonly inputView?: GPUTextureView;
20
+ readonly outputView?: GPUTextureView;
21
+ }
22
+
23
+ export interface SharedPassContext {
24
+ readonly device: GPUDevice;
25
+ readonly format: GPUTextureFormat;
26
+ readonly maskFormat: GPUTextureFormat;
27
+ }
28
+
29
+ export interface Draw {
30
+ readonly id: string;
31
+ readonly pass: Pass;
32
+ readonly order: number;
33
+ execute(ctx: DrawContext): void;
34
+ draw?(pass: GPURenderPassEncoder, ctx: SharedPassContext): void;
35
+ }
36
+
37
+ export interface DrawState {
38
+ draws: Map<string, Draw>;
39
+ }
40
+
41
+ export const Draws = resource<DrawState>("draws");
42
+
43
+ export function registerDraw(state: State, draw: Draw): void {
44
+ const draws = Draws.from(state);
45
+ if (draws) {
46
+ draws.draws.set(draw.id, draw);
47
+ }
48
+ }
49
+
50
+ export function unregisterDraw(state: State, id: string): void {
51
+ const draws = Draws.from(state);
52
+ if (draws) {
53
+ draws.draws.delete(id);
54
+ }
55
+ }
56
+
57
+ export function getDrawsByPass(state: State, pass: Pass): Draw[] {
58
+ const draws = Draws.from(state);
59
+ if (!draws) return [];
60
+ return Array.from(draws.draws.values())
61
+ .filter((d) => d.pass === pass)
62
+ .sort((a, b) => a.order - b.order);
63
+ }