@deck.gl/extensions 9.4.0-beta.1 → 9.4.0-beta.4
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/dist/dist.dev.js +1281 -120
- package/dist/fill-style/fill-style-extension.d.ts +38 -6
- package/dist/fill-style/fill-style-extension.d.ts.map +1 -1
- package/dist/fill-style/fill-style-extension.js +88 -10
- package/dist/fill-style/fill-style-extension.js.map +1 -1
- package/dist/fill-style/procedural-pattern.d.ts +65 -0
- package/dist/fill-style/procedural-pattern.d.ts.map +1 -0
- package/dist/fill-style/procedural-pattern.js +135 -0
- package/dist/fill-style/procedural-pattern.js.map +1 -0
- package/dist/fill-style/shader-module.d.ts +13 -4
- package/dist/fill-style/shader-module.d.ts.map +1 -1
- package/dist/fill-style/shader-module.js +212 -21
- package/dist/fill-style/shader-module.js.map +1 -1
- package/dist/index.cjs +886 -2702
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mask/shader-module.d.ts +1 -0
- package/dist/mask/shader-module.d.ts.map +1 -1
- package/dist/path-style/path-style-extension.d.ts +49 -6
- package/dist/path-style/path-style-extension.d.ts.map +1 -1
- package/dist/path-style/path-style-extension.js +237 -37
- package/dist/path-style/path-style-extension.js.map +1 -1
- package/dist/path-style/shaders.glsl.d.ts +30 -1
- package/dist/path-style/shaders.glsl.d.ts.map +1 -1
- package/dist/path-style/shaders.glsl.js +136 -21
- package/dist/path-style/shaders.glsl.js.map +1 -1
- package/dist/terrain/shader-module.d.ts +1 -0
- package/dist/terrain/shader-module.d.ts.map +1 -1
- package/dist.min.js +377 -98
- package/package.json +7 -7
- package/src/fill-style/fill-style-extension.ts +135 -13
- package/src/fill-style/procedural-pattern.ts +203 -0
- package/src/fill-style/shader-module.ts +233 -23
- package/src/index.ts +10 -1
- package/src/path-style/path-style-extension.ts +350 -44
- 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.
|
|
6
|
+
"version": "9.4.0-beta.4",
|
|
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.
|
|
46
|
-
"@luma.gl/webgl": "^9.4.0-beta.
|
|
45
|
+
"@luma.gl/shadertools": "^9.4.0-beta.7",
|
|
46
|
+
"@luma.gl/webgl": "^9.4.0-beta.7",
|
|
47
47
|
"@math.gl/core": "^4.1.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@deck.gl/core": "~9.
|
|
51
|
-
"@luma.gl/core": "~9.4.0-beta.
|
|
52
|
-
"@luma.gl/engine": "~9.4.0-beta.
|
|
50
|
+
"@deck.gl/core": "~9.4.0-beta.3",
|
|
51
|
+
"@luma.gl/core": "~9.4.0-beta.7",
|
|
52
|
+
"@luma.gl/engine": "~9.4.0-beta.7"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "510649703132adbd49cbfa7d25a49363aec03265"
|
|
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
|
|
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
|
|
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({
|
|
89
|
-
|
|
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.
|
|
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} =
|
|
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
|
-
|
|
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
|
+
}
|