@deck.gl/extensions 9.4.0-beta.1 → 9.4.0-beta.3

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 (37) hide show
  1. package/dist/dist.dev.js +1281 -120
  2. package/dist/fill-style/fill-style-extension.d.ts +38 -6
  3. package/dist/fill-style/fill-style-extension.d.ts.map +1 -1
  4. package/dist/fill-style/fill-style-extension.js +88 -10
  5. package/dist/fill-style/fill-style-extension.js.map +1 -1
  6. package/dist/fill-style/procedural-pattern.d.ts +65 -0
  7. package/dist/fill-style/procedural-pattern.d.ts.map +1 -0
  8. package/dist/fill-style/procedural-pattern.js +135 -0
  9. package/dist/fill-style/procedural-pattern.js.map +1 -0
  10. package/dist/fill-style/shader-module.d.ts +13 -4
  11. package/dist/fill-style/shader-module.d.ts.map +1 -1
  12. package/dist/fill-style/shader-module.js +212 -21
  13. package/dist/fill-style/shader-module.js.map +1 -1
  14. package/dist/index.cjs +946 -172
  15. package/dist/index.cjs.map +3 -3
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/mask/shader-module.d.ts +1 -0
  19. package/dist/mask/shader-module.d.ts.map +1 -1
  20. package/dist/path-style/path-style-extension.d.ts +49 -6
  21. package/dist/path-style/path-style-extension.d.ts.map +1 -1
  22. package/dist/path-style/path-style-extension.js +237 -37
  23. package/dist/path-style/path-style-extension.js.map +1 -1
  24. package/dist/path-style/shaders.glsl.d.ts +30 -1
  25. package/dist/path-style/shaders.glsl.d.ts.map +1 -1
  26. package/dist/path-style/shaders.glsl.js +136 -21
  27. package/dist/path-style/shaders.glsl.js.map +1 -1
  28. package/dist/terrain/shader-module.d.ts +1 -0
  29. package/dist/terrain/shader-module.d.ts.map +1 -1
  30. package/dist.min.js +377 -98
  31. package/package.json +6 -6
  32. package/src/fill-style/fill-style-extension.ts +135 -13
  33. package/src/fill-style/procedural-pattern.ts +203 -0
  34. package/src/fill-style/shader-module.ts +233 -23
  35. package/src/index.ts +10 -1
  36. package/src/path-style/path-style-extension.ts +350 -44
  37. package/src/path-style/shaders.glsl.ts +268 -27
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Plug-and-play functionalities for deck.gl layers",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "version": "9.4.0-beta.1",
6
+ "version": "9.4.0-beta.3",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -42,14 +42,14 @@
42
42
  "prepublishOnly": "npm run build-bundle && npm run build-bundle -- --env=dev"
43
43
  },
44
44
  "dependencies": {
45
- "@luma.gl/shadertools": "^9.4.0-beta.3",
46
- "@luma.gl/webgl": "^9.4.0-beta.3",
45
+ "@luma.gl/shadertools": "^9.4.0-beta.4",
46
+ "@luma.gl/webgl": "^9.4.0-beta.4",
47
47
  "@math.gl/core": "^4.1.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@deck.gl/core": "~9.3.0-beta.2",
51
- "@luma.gl/core": "~9.4.0-beta.3",
52
- "@luma.gl/engine": "~9.4.0-beta.3"
51
+ "@luma.gl/core": "~9.4.0-beta.4",
52
+ "@luma.gl/engine": "~9.4.0-beta.4"
53
53
  },
54
- "gitHead": "5b00a0ced09284c2a642c8ffc56748629f5e796b"
54
+ "gitHead": "7f1e9e63019e6525b3f804cbadcf390ed9d40b65"
55
55
  }
@@ -5,31 +5,47 @@
5
5
  import {LayerExtension} from '@deck.gl/core';
6
6
 
7
7
  import {FillStyleModuleProps, patternShaders} from './shader-module';
8
+ import {
9
+ packProceduralPatterns,
10
+ PROCEDURAL_PATTERN_TEXTURE_FORMAT,
11
+ type ProceduralPatternMapping
12
+ } from './procedural-pattern';
8
13
 
9
14
  import type {
10
15
  Layer,
11
16
  LayerContext,
17
+ Color,
12
18
  DefaultProps,
13
19
  Accessor,
14
20
  AccessorFunction,
15
21
  TextureSource,
22
+ Unit,
16
23
  UpdateParameters
17
24
  } from '@deck.gl/core';
18
25
  import type {Texture} from '@luma.gl/core';
19
26
 
27
+ function leastCommonMultiple(a: number, b: number): number {
28
+ if (!Number.isInteger(b) || b <= 0) return 0;
29
+ if (a === 0) return b;
30
+ let [x, y] = [a, b];
31
+ while (y > 0) [x, y] = [y, x % y];
32
+ return (a / x) * b;
33
+ }
34
+
20
35
  const defaultProps: DefaultProps<FillStyleExtensionProps> = {
21
36
  fillPatternEnabled: true,
22
37
  fillPatternAtlas: {
23
38
  type: 'image',
24
39
  value: null,
25
- async: true,
26
- parameters: {lodMaxClamp: 0}
40
+ async: true
27
41
  },
28
42
  fillPatternMapping: {type: 'object', value: {}, async: true},
29
43
  fillPatternMask: true,
44
+ fillPatternSizeUnits: 'meters',
30
45
  getFillPattern: {type: 'accessor', value: d => d.pattern},
31
46
  getFillPatternScale: {type: 'accessor', value: 1},
32
- getFillPatternOffset: {type: 'accessor', value: [0, 0]}
47
+ getFillPatternOffset: {type: 'accessor', value: [0, 0]},
48
+ getFillPatternBackgroundColor: {type: 'accessor', value: [0, 0, 0, 0]}
33
49
  };
34
50
 
35
51
  export type FillStyleExtensionProps<DataT = any> = {
@@ -37,11 +53,12 @@ export type FillStyleExtensionProps<DataT = any> = {
37
53
  * @default true
38
54
  */
39
55
  fillPatternEnabled?: boolean;
40
- /** Sprite image url or texture that packs all your patterns into one layout. */
56
+ /** Sprite image url or texture that packs all your patterns into one layout. Ignored for procedural patterns. */
41
57
  fillPatternAtlas?: string | TextureSource;
42
- /** Pattern names mapped to pattern definitions, or a url that points to a JSON file. */
58
+ /** Pattern names mapped to atlas frames or procedural definitions, or a URL to a JSON file. */
43
59
  fillPatternMapping?:
44
60
  | string
61
+ | ProceduralPatternMapping
45
62
  | Record<
46
63
  string,
47
64
  {
@@ -60,9 +77,19 @@ export type FillStyleExtensionProps<DataT = any> = {
60
77
  * @default true
61
78
  */
62
79
  fillPatternMask?: boolean;
80
+ /**
81
+ * The units of the pattern size, one of `'meters'`, `'common'` and `'pixels'`. A 24 x 24 pixel
82
+ * pattern at scale `1` covers 24 units of the chosen unit.
83
+ *
84
+ * With `'meters'` (the default) the pattern is anchored to the ground and zooms with the map.
85
+ * With `'pixels'` it keeps a constant size on screen instead, re-anchored at each integer zoom
86
+ * level so that the tiling is stable while zooming within a level.
87
+ * @default 'meters'
88
+ */
89
+ fillPatternSizeUnits?: Unit;
63
90
  /** Accessor for the name of the pattern. */
64
91
  getFillPattern?: AccessorFunction<DataT, string>;
65
- /** Accessor for the scale of the pattern, relative to the original size. If the pattern is 24 x 24 pixels, scale `1` roughly yields 24 meters.
92
+ /** Accessor for the scale of the pattern, relative to its dimensions in `fillPatternSizeUnits`.
66
93
  * @default 1
67
94
  */
68
95
  getFillPatternScale?: Accessor<DataT, number>;
@@ -71,6 +98,13 @@ export type FillStyleExtensionProps<DataT = any> = {
71
98
  * @default [0, 0]
72
99
  */
73
100
  getFillPatternOffset?: Accessor<DataT, Readonly<[number, number]>>;
101
+ /**
102
+ * Accessor for the color filled behind the pattern. The pattern is composited on top of it, so
103
+ * the background is visible wherever the pattern is transparent. Fully transparent by default,
104
+ * which leaves the area behind the pattern unfilled.
105
+ * @default [0, 0, 0, 0]
106
+ */
107
+ getFillPatternBackgroundColor?: Accessor<DataT, Color>;
74
108
  };
75
109
 
76
110
  export type FillStyleExtensionOptions = {
@@ -78,6 +112,10 @@ export type FillStyleExtensionOptions = {
78
112
  * @default false
79
113
  */
80
114
  pattern: boolean;
115
+ /** If `true`, generates patterns in the fragment shader instead of sampling an image atlas.
116
+ * @default false
117
+ */
118
+ proceduralPattern: boolean;
81
119
  };
82
120
 
83
121
  /** Adds selected features to layers that render a "fill", such as the `PolygonLayer` and `ScatterplotLayer`. */
@@ -85,8 +123,11 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
85
123
  static defaultProps = defaultProps;
86
124
  static extensionName = 'FillStyleExtension';
87
125
 
88
- constructor({pattern = false}: Partial<FillStyleExtensionOptions> = {}) {
89
- super({pattern});
126
+ constructor({
127
+ pattern = false,
128
+ proceduralPattern = false
129
+ }: Partial<FillStyleExtensionOptions> = {}) {
130
+ super({pattern: pattern || proceduralPattern, proceduralPattern});
90
131
  }
91
132
 
92
133
  isEnabled(layer: Layer<FillStyleExtensionProps>): boolean {
@@ -116,7 +157,9 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
116
157
  size: 4,
117
158
  stepMode: 'dynamic',
118
159
  accessor: 'getFillPattern',
119
- transform: extension.getPatternFrame.bind(this)
160
+ transform: extension.opts.proceduralPattern
161
+ ? extension.getProceduralPatternIndex.bind(this)
162
+ : extension.getPatternFrame.bind(this)
120
163
  },
121
164
  fillPatternScales: {
122
165
  size: 1,
@@ -128,6 +171,13 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
128
171
  size: 2,
129
172
  stepMode: 'dynamic',
130
173
  accessor: 'getFillPatternOffset'
174
+ },
175
+ fillPatternBackgroundColors: {
176
+ size: 4,
177
+ type: 'unorm8',
178
+ stepMode: 'dynamic',
179
+ accessor: 'getFillPatternBackgroundColor',
180
+ defaultValue: [0, 0, 0, 255]
131
181
  }
132
182
  });
133
183
  }
@@ -136,7 +186,9 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
136
186
  data: new Uint8Array(4),
137
187
  width: 1,
138
188
  height: 1
139
- })
189
+ }),
190
+ ...(extension.opts.proceduralPattern &&
191
+ extension.createProceduralPatternTexture.call(this, {}))
140
192
  });
141
193
  }
142
194
 
@@ -150,8 +202,25 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
150
202
  }
151
203
 
152
204
  if (props.fillPatternMapping && props.fillPatternMapping !== oldProps.fillPatternMapping) {
205
+ if (extension.opts.proceduralPattern && typeof props.fillPatternMapping !== 'string') {
206
+ const oldTexture = this.state.proceduralPatternTexture as Texture | undefined;
207
+ this.setState(
208
+ extension.createProceduralPatternTexture.call(
209
+ this,
210
+ props.fillPatternMapping as ProceduralPatternMapping
211
+ )
212
+ );
213
+ oldTexture?.delete();
214
+ }
153
215
  this.getAttributeManager()!.invalidate('getFillPattern');
154
216
  }
217
+
218
+ if (
219
+ props.fillPatternMapping !== oldProps.fillPatternMapping ||
220
+ props.getFillPatternScale !== oldProps.getFillPatternScale
221
+ ) {
222
+ this.setState({commonFrame: extension.getCommonFrame(props)});
223
+ }
155
224
  }
156
225
 
157
226
  draw(this: Layer<FillStyleExtensionProps>, params: any, extension: this) {
@@ -159,12 +228,18 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
159
228
  return;
160
229
  }
161
230
 
162
- const {fillPatternAtlas, fillPatternEnabled, fillPatternMask} = this.props;
231
+ const {fillPatternAtlas, fillPatternEnabled, fillPatternMask, fillPatternSizeUnits} =
232
+ this.props;
163
233
  const fillProps: FillStyleModuleProps = {
164
234
  project: params.shaderModuleProps.project,
165
235
  fillPatternEnabled,
166
236
  fillPatternMask,
167
- fillPatternTexture: (fillPatternAtlas || this.state.emptyTexture) as Texture
237
+ fillPatternSizeUnits,
238
+ procedural: extension.opts.proceduralPattern,
239
+ fillPatternTexture: (extension.opts.proceduralPattern
240
+ ? this.state.proceduralPatternTexture || this.state.emptyTexture
241
+ : fillPatternAtlas || this.state.emptyTexture) as Texture,
242
+ fillPatternCommonFrame: this.state.commonFrame as [number, number] | null
168
243
  };
169
244
  this.setShaderModuleProps({fill: fillProps});
170
245
  }
@@ -172,11 +247,58 @@ export default class FillStyleExtension extends LayerExtension<FillStyleExtensio
172
247
  finalizeState(this: Layer<FillStyleExtensionProps>) {
173
248
  const emptyTexture = this.state.emptyTexture as Texture;
174
249
  emptyTexture?.delete();
250
+ const proceduralPatternTexture = this.state.proceduralPatternTexture as Texture;
251
+ proceduralPatternTexture?.delete();
175
252
  }
176
253
 
177
254
  getPatternFrame(this: Layer<FillStyleExtensionProps>, name: string) {
178
255
  const {fillPatternMapping} = this.getCurrentLayer()!.props;
179
256
  const def = fillPatternMapping && fillPatternMapping[name];
180
- return def ? [def.x, def.y, def.width, def.height] : [0, 0, 0, 0];
257
+ return def && 'x' in def ? [def.x, def.y, def.width, def.height] : [0, 0, 0, 0];
258
+ }
259
+
260
+ /**
261
+ * Returns common frame into which all patterns fit as integer multiples
262
+ */
263
+ getCommonFrame(props: FillStyleExtensionProps): [number, number] | null {
264
+ const {fillPatternMapping, getFillPatternScale: scale} = props;
265
+ if (typeof scale !== 'number' || !(scale > 0) || typeof fillPatternMapping !== 'object') {
266
+ return null;
267
+ }
268
+ let width = 0;
269
+ let height = 0;
270
+ for (const name in fillPatternMapping) {
271
+ const frame = fillPatternMapping[name];
272
+ // Procedural definitions do not have atlas dimensions and do not need origin reduction here.
273
+ if (!('width' in frame)) return null;
274
+ width = leastCommonMultiple(width, frame.width);
275
+ height = leastCommonMultiple(height, frame.height);
276
+ if (!width || !height) return null;
277
+ }
278
+ return width ? [scale * width, scale * height] : null;
279
+ }
280
+
281
+ getProceduralPatternIndex(this: Layer<FillStyleExtensionProps>, name: string) {
282
+ const patternIndices = this.state.proceduralPatternIndices as
283
+ | ReadonlyMap<string, number>
284
+ | undefined;
285
+ return [patternIndices?.get(name) ?? 0, 0, 0, 0];
286
+ }
287
+
288
+ createProceduralPatternTexture(
289
+ this: Layer<FillStyleExtensionProps>,
290
+ mapping: ProceduralPatternMapping
291
+ ) {
292
+ const packedPatterns = packProceduralPatterns(mapping);
293
+ return {
294
+ proceduralPatternIndices: packedPatterns.patternIndices,
295
+ proceduralPatternTexture: this.context.device.createTexture({
296
+ data: packedPatterns.data,
297
+ width: packedPatterns.width,
298
+ height: packedPatterns.height,
299
+ format: PROCEDURAL_PATTERN_TEXTURE_FORMAT,
300
+ sampler: {minFilter: 'nearest', magFilter: 'nearest'}
301
+ })
302
+ };
181
303
  }
182
304
  }
@@ -0,0 +1,203 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ export type HatchPatternConfig = {
6
+ type: 'hatch';
7
+ /** Direction of the hatch lines in degrees. @default 0 */
8
+ angle?: number;
9
+ /** Width of each hatch line. @default 1 */
10
+ strokeWidth?: number;
11
+ /** Empty edge-to-edge distance between lines. A tuple alternates the two gap values. @default 1 */
12
+ gap?: number | Readonly<[number, number]>;
13
+ };
14
+
15
+ export type CrossHatchPatternConfig = {
16
+ type: 'cross-hatch';
17
+ /** Directions of the two intersecting sets of hatch lines in degrees. @default [45, 135] */
18
+ angles?: Readonly<[number, number]>;
19
+ /** Width of each hatch line. @default 1 */
20
+ strokeWidth?: number;
21
+ /** Empty edge-to-edge distance between adjacent lines. @default 1 */
22
+ gap?: number;
23
+ };
24
+
25
+ export type DotPatternConfig = {
26
+ type: 'dots';
27
+ /** Radius of each dot. @default 1 */
28
+ radius?: number;
29
+ /** Empty edge-to-edge distance between dots along both grid axes. @default 1 */
30
+ gap?: number;
31
+ /** Rotation of the dot grid in degrees. @default 0 */
32
+ angle?: number;
33
+ /** Degrees that the second grid axis tilts toward the first; 0 produces an orthogonal grid. @default 0 */
34
+ skew?: number;
35
+ };
36
+
37
+ /** Procedural pattern shapes supported by FillStyleExtension. */
38
+ export type ProceduralPatternConfig =
39
+ | HatchPatternConfig
40
+ | CrossHatchPatternConfig
41
+ | DotPatternConfig;
42
+
43
+ export type ProceduralPatternMapping = Readonly<Record<string, ProceduralPatternConfig>>;
44
+
45
+ export const PROCEDURAL_PATTERN_TEXTURE_FORMAT = 'rg32float' as const;
46
+ export const PROCEDURAL_PATTERN_COMPONENTS_PER_TEXEL = 2;
47
+ export const PROCEDURAL_PATTERN_TEXELS_PER_PATTERN = 4;
48
+
49
+ export const PROCEDURAL_PATTERN_TYPES = {
50
+ none: 0,
51
+ hatch: 1,
52
+ crossHatch: 2,
53
+ dots: 3
54
+ } as const;
55
+
56
+ /** Component offsets within each fixed-width pattern record. */
57
+ export const PROCEDURAL_PATTERN_OFFSETS = {
58
+ type: 0,
59
+ size: 1,
60
+ gap0: 2,
61
+ gap1: 3,
62
+ angle0: 4,
63
+ angle1: 5,
64
+ /** Number of alternating gaps, hatch directions, or dot-grid axes, depending on type. */
65
+ elementCount: 6
66
+ } as const;
67
+
68
+ export type PackedProceduralPatterns = {
69
+ data: Float32Array;
70
+ width: number;
71
+ height: 1;
72
+ /** Maps the user-facing pattern id to its record in the data texture. Index 0 is reserved for none. */
73
+ patternIndices: ReadonlyMap<string, number>;
74
+ };
75
+
76
+ const COMPONENTS_PER_PATTERN =
77
+ PROCEDURAL_PATTERN_COMPONENTS_PER_TEXEL * PROCEDURAL_PATTERN_TEXELS_PER_PATTERN;
78
+ const DEGREES_TO_RADIANS = Math.PI / 180;
79
+ const DEFAULT_SIZE = 1;
80
+ const DEFAULT_GAP = 1;
81
+ const DEFAULT_HATCH_ANGLE = 0;
82
+ const DEFAULT_CROSS_HATCH_ANGLES = [45, 135] as const;
83
+ const DEFAULT_DOT_ANGLE = 0;
84
+ const DEFAULT_DOT_SKEW = 0;
85
+
86
+ /**
87
+ * Packs procedural pattern definitions into an `rg32float`-compatible data array.
88
+ * The fixed record width keeps vertex-shader lookup independent of the pattern type.
89
+ */
90
+ export function packProceduralPatterns(
91
+ mapping: ProceduralPatternMapping
92
+ ): PackedProceduralPatterns {
93
+ const entries = Object.entries(mapping);
94
+ // The zero-filled first record represents a missing or disabled pattern.
95
+ const data = new Float32Array((entries.length + 1) * COMPONENTS_PER_PATTERN);
96
+ const patternIndices = new Map<string, number>();
97
+
98
+ entries.forEach(([id, config], entryIndex) => {
99
+ const patternIndex = entryIndex + 1;
100
+ const offset = patternIndex * COMPONENTS_PER_PATTERN;
101
+ patternIndices.set(id, patternIndex);
102
+
103
+ switch (config.type) {
104
+ case 'hatch': {
105
+ const gap = config.gap === undefined ? DEFAULT_GAP : config.gap;
106
+ const strokeWidth = config.strokeWidth === undefined ? DEFAULT_SIZE : config.strokeWidth;
107
+ const angle = config.angle === undefined ? DEFAULT_HATCH_ANGLE : config.angle;
108
+ if (Array.isArray(gap) && gap.length !== 2) {
109
+ throw new Error(`${id}.gap must be a number or a two-element array`);
110
+ }
111
+ const gaps = Array.isArray(gap) ? gap : [gap, gap];
112
+ validatePositive(strokeWidth, `${id}.strokeWidth`);
113
+ validateNonNegative(gaps[0], `${id}.gap[0]`);
114
+ validateNonNegative(gaps[1], `${id}.gap[1]`);
115
+ validateAngle(angle, `${id}.angle`);
116
+
117
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.type] = PROCEDURAL_PATTERN_TYPES.hatch;
118
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.size] = strokeWidth;
119
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap0] = gaps[0];
120
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap1] = gaps[1];
121
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.angle0] = angle * DEGREES_TO_RADIANS;
122
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.elementCount] = Array.isArray(gap) ? 2 : 1;
123
+ break;
124
+ }
125
+
126
+ case 'cross-hatch': {
127
+ const angles = config.angles === undefined ? DEFAULT_CROSS_HATCH_ANGLES : config.angles;
128
+ const strokeWidth = config.strokeWidth === undefined ? DEFAULT_SIZE : config.strokeWidth;
129
+ const gap = config.gap === undefined ? DEFAULT_GAP : config.gap;
130
+ if (!Array.isArray(angles) || angles.length !== 2) {
131
+ throw new Error(`${id}.angles must be a two-element array`);
132
+ }
133
+ validatePositive(strokeWidth, `${id}.strokeWidth`);
134
+ validateNonNegative(gap, `${id}.gap`);
135
+ validateAngle(angles[0], `${id}.angles[0]`);
136
+ validateAngle(angles[1], `${id}.angles[1]`);
137
+
138
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.type] = PROCEDURAL_PATTERN_TYPES.crossHatch;
139
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.size] = strokeWidth;
140
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap0] = gap;
141
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap1] = gap;
142
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.angle0] = angles[0] * DEGREES_TO_RADIANS;
143
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.angle1] = angles[1] * DEGREES_TO_RADIANS;
144
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.elementCount] = 2;
145
+ break;
146
+ }
147
+
148
+ case 'dots': {
149
+ const radius = config.radius === undefined ? DEFAULT_SIZE : config.radius;
150
+ const gap = config.gap === undefined ? DEFAULT_GAP : config.gap;
151
+ const angle = config.angle === undefined ? DEFAULT_DOT_ANGLE : config.angle;
152
+ const skew = config.skew === undefined ? DEFAULT_DOT_SKEW : config.skew;
153
+ validatePositive(radius, `${id}.radius`);
154
+ validateNonNegative(gap, `${id}.gap`);
155
+ validateAngle(angle, `${id}.angle`);
156
+ validateSkew(skew, `${id}.skew`);
157
+
158
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.type] = PROCEDURAL_PATTERN_TYPES.dots;
159
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.size] = radius;
160
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap0] = gap;
161
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.gap1] = gap;
162
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.angle0] = angle * DEGREES_TO_RADIANS;
163
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.angle1] = skew * DEGREES_TO_RADIANS;
164
+ data[offset + PROCEDURAL_PATTERN_OFFSETS.elementCount] = 2;
165
+ break;
166
+ }
167
+
168
+ default:
169
+ throw new Error(`${id}.type is not a supported procedural pattern type`);
170
+ }
171
+ });
172
+
173
+ return {
174
+ data,
175
+ width: data.length / PROCEDURAL_PATTERN_COMPONENTS_PER_TEXEL,
176
+ height: 1,
177
+ patternIndices
178
+ };
179
+ }
180
+
181
+ function validatePositive(value: number, name: string): void {
182
+ if (!Number.isFinite(value) || value <= 0) {
183
+ throw new Error(`${name} must be a finite number greater than 0`);
184
+ }
185
+ }
186
+
187
+ function validateNonNegative(value: number, name: string): void {
188
+ if (!Number.isFinite(value) || value < 0) {
189
+ throw new Error(`${name} must be a finite number greater than or equal to 0`);
190
+ }
191
+ }
192
+
193
+ function validateAngle(value: number, name: string): void {
194
+ if (!Number.isFinite(value)) {
195
+ throw new Error(`${name} must be a finite number`);
196
+ }
197
+ }
198
+
199
+ function validateSkew(value: number, name: string): void {
200
+ if (!Number.isFinite(value) || value <= -90 || value >= 90) {
201
+ throw new Error(`${name} must be a finite number greater than -90 and less than 90`);
202
+ }
203
+ }