@uoa-css-lab/duckscatter 1.3.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 (83) hide show
  1. package/.github/dependabot.yml +42 -0
  2. package/.github/workflows/ci.yaml +111 -0
  3. package/.github/workflows/release.yml +55 -0
  4. package/.prettierrc +11 -0
  5. package/LICENSE +22 -0
  6. package/README.md +250 -0
  7. package/dist/data/data-layer.d.ts +169 -0
  8. package/dist/data/data-layer.js +402 -0
  9. package/dist/data/index.d.ts +2 -0
  10. package/dist/data/index.js +2 -0
  11. package/dist/data/repository.d.ts +48 -0
  12. package/dist/data/repository.js +109 -0
  13. package/dist/diagnostics.d.ts +27 -0
  14. package/dist/diagnostics.js +71 -0
  15. package/dist/errors.d.ts +22 -0
  16. package/dist/errors.js +58 -0
  17. package/dist/event-emitter.d.ts +62 -0
  18. package/dist/event-emitter.js +82 -0
  19. package/dist/index.d.ts +12 -0
  20. package/dist/index.js +13 -0
  21. package/dist/renderer/gpu-layer.d.ts +204 -0
  22. package/dist/renderer/gpu-layer.js +611 -0
  23. package/dist/renderer/index.d.ts +3 -0
  24. package/dist/renderer/index.js +3 -0
  25. package/dist/renderer/shaders.d.ts +13 -0
  26. package/dist/renderer/shaders.js +216 -0
  27. package/dist/renderer/webgpu-context.d.ts +20 -0
  28. package/dist/renderer/webgpu-context.js +88 -0
  29. package/dist/scatter-plot.d.ts +210 -0
  30. package/dist/scatter-plot.js +450 -0
  31. package/dist/types.d.ts +171 -0
  32. package/dist/types.js +1 -0
  33. package/dist/ui/index.d.ts +1 -0
  34. package/dist/ui/index.js +1 -0
  35. package/dist/ui/label-layer.d.ts +176 -0
  36. package/dist/ui/label-layer.js +488 -0
  37. package/docs/image.png +0 -0
  38. package/eslint.config.js +72 -0
  39. package/examples/next/README.md +36 -0
  40. package/examples/next/app/components/ColorExpressionInput.tsx +41 -0
  41. package/examples/next/app/components/ControlPanel.tsx +30 -0
  42. package/examples/next/app/components/HoverControlPanel.tsx +69 -0
  43. package/examples/next/app/components/HoverInfoDisplay.tsx +40 -0
  44. package/examples/next/app/components/LabelFilterInput.tsx +46 -0
  45. package/examples/next/app/components/LabelList.tsx +106 -0
  46. package/examples/next/app/components/PointAlphaSlider.tsx +21 -0
  47. package/examples/next/app/components/PointLimitSlider.tsx +23 -0
  48. package/examples/next/app/components/PointList.tsx +105 -0
  49. package/examples/next/app/components/PointSizeScaleSlider.tsx +22 -0
  50. package/examples/next/app/components/ScatterPlotCanvas.tsx +150 -0
  51. package/examples/next/app/components/SearchBox.tsx +46 -0
  52. package/examples/next/app/components/Slider.tsx +76 -0
  53. package/examples/next/app/components/StatsDisplay.tsx +15 -0
  54. package/examples/next/app/components/TimeFilterSlider.tsx +169 -0
  55. package/examples/next/app/context/ScatterPlotContext.tsx +402 -0
  56. package/examples/next/app/favicon.ico +0 -0
  57. package/examples/next/app/globals.css +23 -0
  58. package/examples/next/app/layout.tsx +35 -0
  59. package/examples/next/app/page.tsx +15 -0
  60. package/examples/next/eslint.config.mjs +18 -0
  61. package/examples/next/next.config.ts +7 -0
  62. package/examples/next/package-lock.json +6572 -0
  63. package/examples/next/package.json +27 -0
  64. package/examples/next/postcss.config.mjs +7 -0
  65. package/examples/next/scripts/generate_labels.py +167 -0
  66. package/examples/next/tsconfig.json +34 -0
  67. package/package.json +43 -0
  68. package/src/data/data-layer.ts +515 -0
  69. package/src/data/index.ts +2 -0
  70. package/src/data/repository.ts +146 -0
  71. package/src/diagnostics.ts +108 -0
  72. package/src/errors.ts +69 -0
  73. package/src/event-emitter.ts +88 -0
  74. package/src/index.ts +40 -0
  75. package/src/renderer/gpu-layer.ts +757 -0
  76. package/src/renderer/index.ts +3 -0
  77. package/src/renderer/shaders.ts +219 -0
  78. package/src/renderer/webgpu-context.ts +98 -0
  79. package/src/scatter-plot.ts +533 -0
  80. package/src/types.ts +218 -0
  81. package/src/ui/index.ts +1 -0
  82. package/src/ui/label-layer.ts +648 -0
  83. package/tsconfig.json +19 -0
@@ -0,0 +1,3 @@
1
+ export { GpuLayer, type AllPointsData } from './gpu-layer.js';
2
+ export { WebGPUContext } from './webgpu-context.js';
3
+ export { scatterVertexShader, filterComputeShader } from './shaders.js';
@@ -0,0 +1,219 @@
1
+ /**
2
+ * 散布図レンダリング用WGSLシェーダーコード
3
+ */
4
+
5
+ /**
6
+ * ビューポート境界とLODに基づいて可視ポイントをフィルタリングするコンピュートシェーダー
7
+ * ワークグループローカルのアトミック操作を使用してグローバルアトミックの競合を軽減
8
+ */
9
+ export const filterComputeShader = `
10
+ struct Point {
11
+ x: f32,
12
+ y: f32,
13
+ color: u32,
14
+ size: f32,
15
+ }
16
+
17
+ struct FilterUniforms {
18
+ worldBoundsMin: vec2<f32>,
19
+ worldBoundsMax: vec2<f32>,
20
+ lodThreshold: u32,
21
+ totalPoints: u32,
22
+ activeFilterMask: u32,
23
+ _padding: u32,
24
+ filterRangeMin: vec4<f32>,
25
+ filterRangeMax: vec4<f32>,
26
+ }
27
+
28
+ @group(0) @binding(0) var<storage, read> allPoints: array<Point>;
29
+ @group(0) @binding(1) var<storage, read_write> visibleIndices: array<u32>;
30
+ @group(0) @binding(2) var<storage, read_write> counter: atomic<u32>;
31
+ @group(0) @binding(3) var<uniform> uniforms: FilterUniforms;
32
+ @group(0) @binding(4) var<storage, read> filterColumns: array<vec4<f32>>;
33
+
34
+ var<workgroup> localCount: atomic<u32>;
35
+ var<workgroup> localIndices: array<u32, 256>;
36
+ var<workgroup> globalOffset: u32;
37
+
38
+ fn pcgHash(input: u32) -> u32 {
39
+ let state = input * 747796405u + 2891336453u;
40
+ let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
41
+ return (word >> 22u) ^ word;
42
+ }
43
+
44
+ @compute @workgroup_size(256)
45
+ fn main(
46
+ @builtin(global_invocation_id) globalId: vec3<u32>,
47
+ @builtin(local_invocation_id) localId: vec3<u32>
48
+ ) {
49
+ let idx = globalId.x;
50
+ let lid = localId.x;
51
+
52
+ if (lid == 0u) {
53
+ atomicStore(&localCount, 0u);
54
+ }
55
+ workgroupBarrier();
56
+
57
+ var myLocalSlot: u32 = 0xFFFFFFFFu;
58
+ if (idx < uniforms.totalPoints) {
59
+ let hash = pcgHash(idx);
60
+ var isVisible = hash <= uniforms.lodThreshold;
61
+
62
+ if (isVisible) {
63
+ let point = allPoints[idx];
64
+
65
+ isVisible = point.x >= uniforms.worldBoundsMin.x && point.x <= uniforms.worldBoundsMax.x &&
66
+ point.y >= uniforms.worldBoundsMin.y && point.y <= uniforms.worldBoundsMax.y;
67
+ }
68
+
69
+ if (isVisible && uniforms.activeFilterMask != 0u) {
70
+ let filterData = filterColumns[idx];
71
+
72
+ if ((uniforms.activeFilterMask & 1u) != 0u) {
73
+ isVisible = isVisible &&
74
+ filterData.x >= uniforms.filterRangeMin.x &&
75
+ filterData.x <= uniforms.filterRangeMax.x;
76
+ }
77
+ if ((uniforms.activeFilterMask & 2u) != 0u) {
78
+ isVisible = isVisible &&
79
+ filterData.y >= uniforms.filterRangeMin.y &&
80
+ filterData.y <= uniforms.filterRangeMax.y;
81
+ }
82
+ if ((uniforms.activeFilterMask & 4u) != 0u) {
83
+ isVisible = isVisible &&
84
+ filterData.z >= uniforms.filterRangeMin.z &&
85
+ filterData.z <= uniforms.filterRangeMax.z;
86
+ }
87
+ if ((uniforms.activeFilterMask & 8u) != 0u) {
88
+ isVisible = isVisible &&
89
+ filterData.w >= uniforms.filterRangeMin.w &&
90
+ filterData.w <= uniforms.filterRangeMax.w;
91
+ }
92
+ }
93
+
94
+ if (isVisible) {
95
+ myLocalSlot = atomicAdd(&localCount, 1u);
96
+ localIndices[myLocalSlot] = idx;
97
+ }
98
+ }
99
+ workgroupBarrier();
100
+
101
+ let count = atomicLoad(&localCount);
102
+ if (lid == 0u && count > 0u) {
103
+ globalOffset = atomicAdd(&counter, count);
104
+ }
105
+ workgroupBarrier();
106
+
107
+ if (lid < count) {
108
+ visibleIndices[globalOffset + lid] = localIndices[lid];
109
+ }
110
+ }
111
+ `;
112
+
113
+ /**
114
+ * カウンターから間接描画バッファを更新するコンピュートシェーダー
115
+ */
116
+ export const updateIndirectShader = `
117
+ struct DrawIndexedIndirect {
118
+ indexCount: u32,
119
+ instanceCount: u32,
120
+ firstIndex: u32,
121
+ baseVertex: u32,
122
+ firstInstance: u32,
123
+ }
124
+
125
+ @group(0) @binding(0) var<storage, read> counter: u32;
126
+ @group(0) @binding(1) var<storage, read_write> indirect: DrawIndexedIndirect;
127
+
128
+ @compute @workgroup_size(1)
129
+ fn main() {
130
+ indirect.indexCount = 6u;
131
+ indirect.instanceCount = counter;
132
+ indirect.firstIndex = 0u;
133
+ indirect.baseVertex = 0u;
134
+ indirect.firstInstance = 0u;
135
+ }
136
+ `;
137
+
138
+ export const scatterVertexShader = `
139
+ struct Point {
140
+ x: f32,
141
+ y: f32,
142
+ color: u32,
143
+ size: f32,
144
+ }
145
+
146
+ struct Uniforms {
147
+ viewMatrix: mat4x4<f32>,
148
+ zoomScale: f32,
149
+ viewportWidth: f32,
150
+ viewportHeight: f32,
151
+ pointAlpha: f32,
152
+ pointSizeScale: f32,
153
+ _padding1: f32,
154
+ _padding2: f32,
155
+ _padding3: f32,
156
+ }
157
+
158
+ struct VertexOutput {
159
+ @builtin(position) position: vec4<f32>,
160
+ @location(0) color: vec4<f32>,
161
+ @location(1) pointCoord: vec2<f32>,
162
+ }
163
+
164
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
165
+ @group(0) @binding(1) var<storage, read> allPoints: array<Point>;
166
+ @group(0) @binding(2) var<storage, read> visibleIndices: array<u32>;
167
+
168
+ fn unpackColor(argb: u32) -> vec4<f32> {
169
+ let a = f32((argb >> 24u) & 0xFFu) / 255.0;
170
+ let r = f32((argb >> 16u) & 0xFFu) / 255.0;
171
+ let g = f32((argb >> 8u) & 0xFFu) / 255.0;
172
+ let b = f32(argb & 0xFFu) / 255.0;
173
+ return vec4<f32>(r, g, b, a);
174
+ }
175
+
176
+ @vertex
177
+ fn vertexMain(
178
+ @location(0) quadPosition: vec2<f32>,
179
+ @builtin(instance_index) instanceIdx: u32
180
+ ) -> VertexOutput {
181
+ var output: VertexOutput;
182
+
183
+ let pointIdx = visibleIndices[instanceIdx];
184
+ let point = allPoints[pointIdx];
185
+
186
+ let clipPos = uniforms.viewMatrix * vec4<f32>(point.x, point.y, 0.0, 1.0);
187
+
188
+ let pixelToClipX = 2.0 / uniforms.viewportWidth;
189
+ let pixelToClipY = 2.0 / uniforms.viewportHeight;
190
+ let zoomScale = uniforms.zoomScale;
191
+
192
+ let scaledSize = point.size * uniforms.pointSizeScale;
193
+ let offsetClip = vec2<f32>(
194
+ quadPosition.x * scaledSize * pixelToClipX * zoomScale,
195
+ quadPosition.y * scaledSize * pixelToClipY * zoomScale
196
+ );
197
+
198
+ output.position = clipPos + vec4<f32>(offsetClip, 0.0, 0.0);
199
+ output.color = unpackColor(point.color);
200
+
201
+ output.pointCoord = (quadPosition + 1.0) * 0.5;
202
+
203
+ return output;
204
+ }
205
+
206
+ @fragment
207
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
208
+ let d = input.pointCoord - vec2<f32>(0.5);
209
+ let distSq = dot(d, d);
210
+
211
+ if (distSq > 0.25) {
212
+ discard;
213
+ }
214
+
215
+ let alpha = smoothstep(0.25, 0.23, distSq);
216
+
217
+ return vec4<f32>(input.color.rgb, input.color.a * alpha * uniforms.pointAlpha);
218
+ }
219
+ `;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * WebGPUコンテキストを管理するクラス
3
+ */
4
+ export class WebGPUContext {
5
+ /** GPUデバイスインスタンス */
6
+ public device: GPUDevice | null = null;
7
+ /** キャンバスのWebGPUコンテキスト */
8
+ public context: GPUCanvasContext | null = null;
9
+ /** テクスチャフォーマット */
10
+ public format: GPUTextureFormat = 'bgra8unorm';
11
+
12
+ /**
13
+ * WebGPUを初期化する
14
+ * @param canvas 描画対象のHTMLCanvasElement
15
+ */
16
+ async initialize(canvas: HTMLCanvasElement): Promise<void> {
17
+ if (!navigator.gpu) {
18
+ throw new Error(
19
+ 'WebGPU is not supported in this browser. ' +
20
+ 'Please use Chrome 113+, Edge 113+, or Safari 18+ with WebGPU enabled.'
21
+ );
22
+ }
23
+
24
+ let adapter: GPUAdapter | null = null;
25
+
26
+ try {
27
+ adapter = await navigator.gpu.requestAdapter({
28
+ powerPreference: 'high-performance',
29
+ });
30
+ } catch {
31
+ // empty
32
+ }
33
+
34
+ if (!adapter) {
35
+ try {
36
+ adapter = await navigator.gpu.requestAdapter();
37
+ } catch {
38
+ // empty
39
+ }
40
+ }
41
+
42
+ if (!adapter) {
43
+ try {
44
+ adapter = await navigator.gpu.requestAdapter({
45
+ powerPreference: 'low-power',
46
+ });
47
+ } catch {
48
+ // empty
49
+ }
50
+ }
51
+
52
+ if (!adapter) {
53
+ throw new Error(
54
+ 'Failed to get GPU adapter. Possible reasons:\n' +
55
+ '1. WebGPU is disabled in browser flags\n' +
56
+ '2. Your GPU is blocklisted\n' +
57
+ '3. GPU drivers need updating\n' +
58
+ '4. Running in a virtual machine without GPU access\n\n' +
59
+ 'For Chrome/Edge: Visit chrome://gpu to check WebGPU status\n' +
60
+ 'For Safari: Ensure macOS Sonoma 14.4+ with Safari 18+'
61
+ );
62
+ }
63
+
64
+ try {
65
+ this.device = await adapter.requestDevice();
66
+ } catch (e) {
67
+ throw new Error(`Failed to get GPU device: ${e}`);
68
+ }
69
+
70
+ if (!this.device) {
71
+ throw new Error('Failed to get GPU device: Device is null');
72
+ }
73
+
74
+ this.context = canvas.getContext('webgpu');
75
+ if (!this.context) {
76
+ throw new Error('Failed to get WebGPU context from canvas');
77
+ }
78
+
79
+ this.format = navigator.gpu.getPreferredCanvasFormat();
80
+
81
+ this.context.configure({
82
+ device: this.device,
83
+ format: this.format,
84
+ alphaMode: 'premultiplied',
85
+ });
86
+ }
87
+
88
+ /**
89
+ * WebGPUリソースを破棄する
90
+ */
91
+ destroy(): void {
92
+ if (this.device) {
93
+ this.device.destroy();
94
+ this.device = null;
95
+ }
96
+ this.context = null;
97
+ }
98
+ }