@genome-spy/core 0.20.0 → 0.21.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.
package/src/marks/mark.js CHANGED
@@ -4,10 +4,11 @@ import {
4
4
  createUniformBlockInfo,
5
5
  createVertexArrayInfo,
6
6
  setAttribInfoBufferFromArray,
7
+ setBlockUniforms,
7
8
  setUniformBlock,
8
9
  setUniforms,
9
10
  } from "twgl.js";
10
- import { isDiscrete } from "vega-scale";
11
+ import { isContinuous, isDiscrete } from "vega-scale";
11
12
  import createEncoders, {
12
13
  isChannelDefWithScale,
13
14
  isDatumDef,
@@ -32,6 +33,7 @@ import { getCachedOrCall } from "../utils/propertyCacher";
32
33
  import { createProgram } from "../gl/webGLHelper";
33
34
  import coalesceProperties from "../utils/propertyCoalescer";
34
35
  import { isScalar } from "../utils/variableTools";
36
+ import { InternMap } from "internmap";
35
37
 
36
38
  export const SAMPLE_FACET_UNIFORM = "SAMPLE_FACET_UNIFORM";
37
39
  export const SAMPLE_FACET_TEXTURE = "SAMPLE_FACET_TEXTURE";
@@ -77,6 +79,12 @@ export default class Mark {
77
79
  /** @type {import("twgl.js").UniformBlockInfo} WebGL buffers */
78
80
  this.domainUniformInfo = undefined;
79
81
 
82
+ /** @type {import("twgl.js").UniformBlockInfo} WebGL buffers */
83
+ this.viewUniformInfo = undefined;
84
+
85
+ /** @type {RangeMap<any>} keep track of facet locations within the vertex array */
86
+ this.rangeMap = new RangeMap();
87
+
80
88
  // TODO: Implement https://vega.github.io/vega-lite/docs/config.html
81
89
  /** @type {MarkConfig} */
82
90
  this.defaultProperties = {
@@ -399,9 +407,21 @@ export default class Mark {
399
407
  );
400
408
  }
401
409
 
410
+ this.viewUniformInfo = createUniformBlockInfo(
411
+ this.gl,
412
+ this.programInfo,
413
+ "View"
414
+ );
415
+
402
416
  this.gl.useProgram(this.programInfo.program);
403
417
 
404
418
  this._setDatums();
419
+
420
+ setUniforms(this.programInfo, {
421
+ // left pos, left height, right pos, right height
422
+ uSampleFacet: [0, 1, 0, 1],
423
+ uTransitionOffset: 0.0,
424
+ });
405
425
  }
406
426
 
407
427
  _setDatums() {
@@ -535,21 +555,27 @@ export default class Mark {
535
555
  * and scales) and buffers.
536
556
  *
537
557
  * @param {import("../view/rendering").GlobalRenderingOptions} options
558
+ * @returns {(() => void)[]}
538
559
  */
539
560
  // eslint-disable-next-line complexity
540
561
  prepareRender(options) {
541
562
  const glHelper = this.glHelper;
542
563
  const gl = this.gl;
543
564
 
544
- if (!this.vertexArrayInfo) {
545
- this.vertexArrayInfo = createVertexArrayInfo(
546
- this.gl,
547
- this.programInfo,
548
- this.bufferInfo
549
- );
550
- }
565
+ /** @type {(() => void)[]} */
566
+ const ops = [];
551
567
 
552
- gl.useProgram(this.programInfo.program);
568
+ ops.push(() => {
569
+ if (!this.vertexArrayInfo) {
570
+ this.vertexArrayInfo = createVertexArrayInfo(
571
+ this.gl,
572
+ this.programInfo,
573
+ this.bufferInfo
574
+ );
575
+ }
576
+
577
+ gl.useProgram(this.programInfo.program);
578
+ });
553
579
 
554
580
  if (this.domainUniformInfo) {
555
581
  // TODO: Only update the domains that have changed
@@ -572,19 +598,24 @@ export default class Mark {
572
598
 
573
599
  if (resolution) {
574
600
  const scale = resolution.getScale();
575
- const domain = isDiscrete(scale.type)
576
- ? [0, scale.domain().length]
577
- : scale.domain();
578
-
579
- setter(
580
- isHighPrecisionScale(scale.type)
581
- ? toHighPrecisionDomainUniform(domain)
582
- : domain
583
- );
601
+
602
+ ops.push(() => {
603
+ const domain = isDiscrete(scale.type)
604
+ ? [0, scale.domain().length]
605
+ : scale.domain();
606
+
607
+ setter(
608
+ isHighPrecisionScale(scale.type)
609
+ ? toHighPrecisionDomainUniform(domain)
610
+ : domain
611
+ );
612
+ });
584
613
  }
585
614
  }
586
615
 
587
- setUniformBlock(gl, this.programInfo, this.domainUniformInfo);
616
+ ops.push(() =>
617
+ setUniformBlock(gl, this.programInfo, this.domainUniformInfo)
618
+ );
588
619
  }
589
620
 
590
621
  for (const [channel, channelDef] of Object.entries(this.encoding)) {
@@ -599,52 +630,56 @@ export default class Mark {
599
630
 
600
631
  const texture = glHelper.rangeTextures.get(resolution);
601
632
  if (texture) {
602
- setUniforms(this.programInfo, {
603
- [RANGE_TEXTURE_PREFIX + channel]: texture,
604
- });
633
+ ops.push(() =>
634
+ setUniforms(this.programInfo, {
635
+ [RANGE_TEXTURE_PREFIX + channel]: texture,
636
+ })
637
+ );
605
638
  }
606
639
  }
607
640
  }
608
641
 
609
642
  if (this.getSampleFacetMode() == SAMPLE_FACET_TEXTURE) {
610
- /** @type {WebGLTexture} */
611
- let facetTexture;
612
- for (const view of this.unitView.getAncestors()) {
613
- facetTexture = view.getSampleFacetTexture();
614
- if (facetTexture) {
615
- break;
643
+ ops.push(() => {
644
+ /** @type {WebGLTexture} */
645
+ let facetTexture;
646
+ for (const view of this.unitView.getAncestors()) {
647
+ facetTexture = view.getSampleFacetTexture();
648
+ if (facetTexture) {
649
+ break;
650
+ }
616
651
  }
617
- }
618
652
 
619
- if (!facetTexture) {
620
- throw new Error("No facet texture available. This is bug.");
621
- }
653
+ if (!facetTexture) {
654
+ throw new Error("No facet texture available. This is bug.");
655
+ }
622
656
 
623
- setUniforms(this.programInfo, {
624
- uSampleFacetTexture: facetTexture,
657
+ setUniforms(this.programInfo, {
658
+ uSampleFacetTexture: facetTexture,
659
+ });
625
660
  });
626
661
  }
627
662
 
628
- setUniforms(this.programInfo, {
629
- uDevicePixelRatio: this.glHelper.dpr,
630
- uViewOpacity: this.unitView.getEffectiveOpacity(),
631
- // TODO: Rendering of the mark should be completely skipped if it doesn't
632
- // participate picking
633
- uPickingEnabled:
634
- (options.picking ?? false) && this.isPickingParticipant(),
635
- });
636
-
637
- setUniforms(this.programInfo, {
638
- // left pos, left height, right pos, right height
639
- uSampleFacet: [0, 1, 0, 1],
640
- uTransitionOffset: 0.0,
641
- });
663
+ // TODO: Rendering of the mark should be completely skipped if it doesn't
664
+ // participate picking
665
+ const picking =
666
+ (options.picking ?? false) && this.isPickingParticipant();
667
+
668
+ // Note: the block is sent to GPU in setViewport(), which is repeated for each facet
669
+ ops.push(() =>
670
+ setBlockUniforms(this.viewUniformInfo, {
671
+ uViewOpacity: this.unitView.getEffectiveOpacity(),
672
+ uPickingEnabled: picking,
673
+ })
674
+ );
642
675
 
643
676
  if (this.opaque || options.picking) {
644
- gl.disable(gl.BLEND);
677
+ ops.push(() => gl.disable(gl.BLEND));
645
678
  } else {
646
- gl.enable(gl.BLEND);
679
+ ops.push(() => gl.enable(gl.BLEND));
647
680
  }
681
+
682
+ return ops;
648
683
  }
649
684
 
650
685
  /**
@@ -706,58 +741,56 @@ export default class Mark {
706
741
  /**
707
742
  * @param {DrawFunction} draw A function that draws a range of vertices
708
743
  * @param {import("./Mark").MarkRenderingOptions} options
709
- * @param {function():Map<string, import("../gl/dataToVertices").RangeEntry>} rangeMapSource
710
744
  */
711
- createRenderCallback(draw, options, rangeMapSource) {
745
+ createRenderCallback(draw, options) {
712
746
  // eslint-disable-next-line consistent-this
713
747
  const self = this;
714
748
 
715
749
  /** @type {function(import("../gl/dataToVertices").RangeEntry):void} rangeEntry */
716
750
  let drawWithRangeEntry;
717
751
 
718
- if (this.properties.buildIndex) {
719
- const scale = this.unitView.getScaleResolution("x")?.getScale();
720
-
721
- drawWithRangeEntry = (rangeEntry) => {
722
- if (scale && rangeEntry.xIndex) {
723
- const domain = scale.domain();
724
- const vertexIndices = rangeEntry.xIndex(
725
- domain[0],
726
- domain[1]
727
- );
728
- const offset = vertexIndices[0];
729
- const count = vertexIndices[1] - offset;
730
- if (count > 0) {
731
- draw(offset, count);
732
- }
733
- } else {
734
- draw(rangeEntry.offset, rangeEntry.count);
752
+ const scale = this.unitView.getScaleResolution("x")?.getScale();
753
+ const continuous = scale && isContinuous(scale.type);
754
+ const domainStartOffset = ["index", "locus"].includes(scale?.type)
755
+ ? -1
756
+ : 0;
757
+
758
+ /** @type {[number, number]} Recycle to ease garbage collector's work */
759
+ const arr = [0, 0];
760
+
761
+ drawWithRangeEntry = (rangeEntry) => {
762
+ if (continuous && rangeEntry.xIndex) {
763
+ const domain = scale.domain();
764
+ const vertexIndices = rangeEntry.xIndex(
765
+ domain[0] + domainStartOffset,
766
+ domain[1],
767
+ arr
768
+ );
769
+ const offset = vertexIndices[0];
770
+ const count = vertexIndices[1] - offset;
771
+ if (count > 0) {
772
+ draw(offset, count);
735
773
  }
736
- };
737
- } else {
738
- drawWithRangeEntry = (rangeEntry) =>
774
+ } else {
739
775
  draw(rangeEntry.offset, rangeEntry.count);
740
- }
741
-
742
- if (this.properties.dynamicData) {
743
- return function renderDynamic() {
744
- const rangeEntry = rangeMapSource().get(options.facetId);
745
- if (rangeEntry && rangeEntry.count) {
746
- if (self.prepareSampleFacetRendering(options)) {
747
- drawWithRangeEntry(rangeEntry);
748
- }
749
- }
750
- };
751
- } else {
752
- const rangeEntry = rangeMapSource()?.get(options.facetId);
753
- if (rangeEntry && rangeEntry.count) {
754
- return function renderStatic() {
755
- if (self.prepareSampleFacetRendering(options)) {
756
- drawWithRangeEntry(rangeEntry);
757
- }
758
- };
759
776
  }
760
- }
777
+ };
778
+
779
+ const rangeEntry = this.rangeMap.get(options.facetId);
780
+
781
+ return options.sampleFacetRenderingOptions
782
+ ? function renderSampleFacetRange() {
783
+ if (rangeEntry.count) {
784
+ if (self.prepareSampleFacetRendering(options)) {
785
+ drawWithRangeEntry(rangeEntry);
786
+ }
787
+ }
788
+ }
789
+ : function renderRange() {
790
+ if (rangeEntry.count) {
791
+ drawWithRangeEntry(rangeEntry);
792
+ }
793
+ };
761
794
  }
762
795
 
763
796
  /**
@@ -839,7 +872,7 @@ export default class Mark {
839
872
  uViewScale,
840
873
  };
841
874
  } else {
842
- // Viewport comprises of the full canvas
875
+ // Viewport comprises the full canvas
843
876
  gl.viewport(
844
877
  0,
845
878
  0,
@@ -862,15 +895,15 @@ export default class Mark {
862
895
  };
863
896
  }
864
897
 
865
- // TODO: Optimization: Use uniform buffer object
866
- setUniforms(this.programInfo, uniforms);
867
-
868
- setUniforms(this.programInfo, {
898
+ setBlockUniforms(this.viewUniformInfo, {
899
+ ...uniforms,
869
900
  uViewportSize: [coords.width, coords.height],
901
+ uDevicePixelRatio: this.glHelper.dpr,
870
902
  });
871
903
 
872
- // TODO: Optimize: don't set viewport and stuff if rect is outside clipRect or screen
904
+ setUniformBlock(this.gl, this.programInfo, this.viewUniformInfo);
873
905
 
906
+ // TODO: Optimize: don't set viewport and stuff if rect is outside clipRect or screen
874
907
  return clippedCoords.height > 0 && clippedCoords.width > 0;
875
908
  }
876
909
 
@@ -888,3 +921,39 @@ export default class Mark {
888
921
  // override
889
922
  }
890
923
  }
924
+
925
+ /**
926
+ * @augments {InternMap<K, import("../gl/dataToVertices").RangeEntry>}
927
+ * @template K
928
+ */
929
+ class RangeMap extends InternMap {
930
+ constructor() {
931
+ super([], JSON.stringify);
932
+ }
933
+
934
+ /**
935
+ * @param {K} key
936
+ */
937
+ get(key) {
938
+ let value = super.get(key);
939
+ if (value === undefined) {
940
+ value = {
941
+ offset: 0,
942
+ count: 0,
943
+ xIndex: undefined,
944
+ };
945
+ super.set(key, value);
946
+ }
947
+ return value;
948
+ }
949
+
950
+ /**
951
+ *
952
+ * @param {Map<K, import("../gl/dataToVertices").RangeEntry>} anotherMap
953
+ */
954
+ migrateEntries(anotherMap) {
955
+ for (const [key, value] of anotherMap.entries()) {
956
+ Object.assign(this.get(key), value);
957
+ }
958
+ }
959
+ }
@@ -1,6 +1,5 @@
1
1
  import { drawBufferInfo, setBuffersAndAttributes, setUniforms } from "twgl.js";
2
- import { bisector, quantileSorted } from "d3-array";
3
- import { zoomLinear } from "vega-util";
2
+ import { quantileSorted } from "d3-array";
4
3
  import { PointVertexBuilder } from "../gl/dataToVertices";
5
4
  import VERTEX_SHADER from "../gl/point.vertex.glsl";
6
5
  import FRAGMENT_SHADER from "../gl/point.fragment.glsl";
@@ -156,7 +155,7 @@ export default class PointMark extends Mark {
156
155
  builder.addBatches(collector.facetBatches);
157
156
 
158
157
  const vertexData = builder.toArrays();
159
- this.rangeMap = vertexData.rangeMap;
158
+ this.rangeMap.migrateEntries(vertexData.rangeMap);
160
159
  this.updateBufferInfo(vertexData);
161
160
  }
162
161
 
@@ -210,44 +209,25 @@ export default class PointMark extends Mark {
210
209
  * @param {import("../view/rendering").GlobalRenderingOptions} options
211
210
  */
212
211
  prepareRender(options) {
213
- super.prepareRender(options);
212
+ const ops = super.prepareRender(options);
214
213
 
215
- setUniforms(this.programInfo, {
216
- uMaxPointSize: this._getMaxPointSize(),
217
- uScaleFactor: this._getGeometricScaleFactor(),
218
- uSemanticThreshold: this.getSemanticThreshold(),
219
- });
214
+ ops.push(() =>
215
+ setUniforms(this.programInfo, {
216
+ uMaxPointSize: this._getMaxPointSize(),
217
+ uScaleFactor: this._getGeometricScaleFactor(),
218
+ uSemanticThreshold: this.getSemanticThreshold(),
219
+ })
220
+ );
220
221
 
221
- setBuffersAndAttributes(
222
- this.gl,
223
- this.programInfo,
224
- this.vertexArrayInfo
222
+ ops.push(() =>
223
+ setBuffersAndAttributes(
224
+ this.gl,
225
+ this.programInfo,
226
+ this.vertexArrayInfo
227
+ )
225
228
  );
226
229
 
227
- // Setup bisector that allows for searching the points that reside within the viewport.
228
- const xEncoder = this.encoders.x;
229
- if (xEncoder && !xEncoder.constant) {
230
- const bisect = bisector(xEncoder.accessor).left;
231
- const visibleDomain = this.unitView
232
- .getScaleResolution("x")
233
- .getScale()
234
- .domain();
235
-
236
- // A hack to include points that are just beyond the borders. TODO: Compute based on maxPointSize
237
- const paddedDomain = zoomLinear(visibleDomain, null, 1.01);
238
-
239
- /** @param {any[]} facetId */
240
- this._findIndices = (facetId) => {
241
- const data = this.unitView
242
- .getCollector()
243
- .facetBatches.get(facetId);
244
-
245
- return [
246
- bisect(data, paddedDomain[0]),
247
- bisect(data, paddedDomain[paddedDomain.length - 1]),
248
- ];
249
- };
250
- }
230
+ return ops;
251
231
  }
252
232
 
253
233
  /**
@@ -256,27 +236,16 @@ export default class PointMark extends Mark {
256
236
  render(options) {
257
237
  const gl = this.gl;
258
238
 
259
- return this.createRenderCallback(
260
- (offset, count) => {
261
- // TODO: findIndices is rather slow. Consider a more coarse-grained, "tiled" solution.
262
- const [lower, upper] = this._findIndices
263
- ? this._findIndices(options.facetId)
264
- : [0, count];
265
-
266
- const length = upper - lower;
267
-
268
- if (length) {
269
- drawBufferInfo(
270
- gl,
271
- this.vertexArrayInfo,
272
- gl.POINTS,
273
- length,
274
- offset + lower
275
- );
276
- }
277
- },
278
- options,
279
- () => this.rangeMap
280
- );
239
+ return this.createRenderCallback((offset, count) => {
240
+ if (count) {
241
+ drawBufferInfo(
242
+ gl,
243
+ this.vertexArrayInfo,
244
+ gl.POINTS,
245
+ count,
246
+ offset
247
+ );
248
+ }
249
+ }, options);
281
250
  }
282
251
  }
@@ -146,6 +146,25 @@ export default class RectMark extends Mark {
146
146
  );
147
147
  }
148
148
 
149
+ finalizeGraphicsInitialization() {
150
+ super.finalizeGraphicsInitialization();
151
+
152
+ this.gl.useProgram(this.programInfo.program);
153
+
154
+ const props = this.properties;
155
+
156
+ setUniforms(this.programInfo, {
157
+ uMinSize: [props.minWidth, props.minHeight], // in pixels
158
+ uMinOpacity: props.minOpacity,
159
+ uCornerRadii: [
160
+ props.cornerRadiusTopRight ?? props.cornerRadius,
161
+ props.cornerRadiusBottomRight ?? props.cornerRadius,
162
+ props.cornerRadiusTopLeft ?? props.cornerRadius,
163
+ props.cornerRadiusBottomLeft ?? props.cornerRadius,
164
+ ],
165
+ });
166
+ }
167
+
149
168
  updateGraphicsData() {
150
169
  const collector = this.unitView.getCollector();
151
170
  const numItems = collector.getItemCount();
@@ -155,13 +174,12 @@ export default class RectMark extends Mark {
155
174
  encoders: this.encoders,
156
175
  attributes: this.getAttributes(),
157
176
  numItems,
158
- buildXIndex: this.properties.buildIndex,
159
177
  });
160
178
 
161
179
  builder.addBatches(collector.facetBatches);
162
180
 
163
181
  const vertexData = builder.toArrays();
164
- this.rangeMap = vertexData.rangeMap;
182
+ this.rangeMap.migrateEntries(vertexData.rangeMap);
165
183
  this.updateBufferInfo(vertexData);
166
184
  }
167
185
 
@@ -169,26 +187,17 @@ export default class RectMark extends Mark {
169
187
  * @param {import("../view/rendering").GlobalRenderingOptions} options
170
188
  */
171
189
  prepareRender(options) {
172
- super.prepareRender(options);
190
+ const ops = super.prepareRender(options);
173
191
 
174
- const props = this.properties;
175
-
176
- setUniforms(this.programInfo, {
177
- uMinSize: [props.minWidth, props.minHeight], // in pixels
178
- uMinOpacity: props.minOpacity,
179
- uCornerRadii: [
180
- props.cornerRadiusTopRight ?? props.cornerRadius,
181
- props.cornerRadiusBottomRight ?? props.cornerRadius,
182
- props.cornerRadiusTopLeft ?? props.cornerRadius,
183
- props.cornerRadiusBottomLeft ?? props.cornerRadius,
184
- ],
185
- });
186
-
187
- setBuffersAndAttributes(
188
- this.gl,
189
- this.programInfo,
190
- this.vertexArrayInfo
192
+ ops.push(() =>
193
+ setBuffersAndAttributes(
194
+ this.gl,
195
+ this.programInfo,
196
+ this.vertexArrayInfo
197
+ )
191
198
  );
199
+
200
+ return ops;
192
201
  }
193
202
 
194
203
  /**
@@ -197,19 +206,15 @@ export default class RectMark extends Mark {
197
206
  render(options) {
198
207
  const gl = this.gl;
199
208
 
200
- return this.createRenderCallback(
201
- (offset, count) => {
202
- drawBufferInfo(
203
- gl,
204
- this.vertexArrayInfo,
205
- gl.TRIANGLE_STRIP,
206
- count,
207
- offset
208
- );
209
- },
210
- options,
211
- () => this.rangeMap
212
- );
209
+ return this.createRenderCallback((offset, count) => {
210
+ drawBufferInfo(
211
+ gl,
212
+ this.vertexArrayInfo,
213
+ gl.TRIANGLE_STRIP,
214
+ count,
215
+ offset
216
+ );
217
+ }, options);
213
218
  }
214
219
 
215
220
  /**