@sakuzu/maplibre-gl-draw 2.0.0 → 2.1.1
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/CHANGELOG.md +119 -1
- package/README.ja.md +48 -35
- package/README.md +52 -36
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/datasets.d.ts +5 -0
- package/dist/api/draw.d.ts +6 -4
- package/dist/api/draw.js +6 -4
- package/dist/api/events.d.ts +10 -0
- package/dist/api/impl/create-draw.js +1 -1
- package/dist/api/impl/datasets.js +1 -0
- package/dist/api/impl/events.js +2 -1
- package/dist/api/impl/extension-host.js +4 -1
- package/dist/api/impl/options.js +58 -29
- package/dist/dataset/dataset.js +3 -0
- package/dist/dataset/retained.js +19 -18
- package/dist/dataset/selection.d.ts +3 -3
- package/dist/dataset/selection.js +3 -7
- package/dist/dataset/table-source.js +13 -11
- package/dist/dataset/types.d.ts +2 -0
- package/dist/dispatcher/input-router.js +2 -1
- package/dist/dispatcher/normalizer.js +19 -2
- package/dist/geometry/geojson.d.ts +4 -0
- package/dist/geometry/geojson.js +20 -11
- package/dist/index.d.ts +14 -0
- package/dist/view/layer/attach.d.ts +11 -5
- package/dist/view/layer/attach.js +85 -9
- package/dist/view/layer/custom-layer.js +2 -1
- package/dist/view/layer/drape-planner.d.ts +3 -0
- package/dist/view/layer/drape-planner.js +9 -7
- package/dist/view/layer/frame-render.js +2 -3
- package/dist/view/layer/frame-state.d.ts +3 -0
- package/dist/view/layer/frame-state.js +2 -1
- package/dist/view/layer/gl-state.d.ts +2 -3
- package/dist/view/layer/gl-state.js +2 -3
- package/dist/view/renderers/line/dash.d.ts +27 -0
- package/dist/view/renderers/line/dash.js +23 -12
- package/dist/view/terrain/drape/bin-store.d.ts +11 -2
- package/dist/view/terrain/drape/bin-store.js +26 -2
- package/dist/view/terrain/drape/binning.d.ts +32 -0
- package/dist/view/terrain/drape/binning.js +15 -7
- package/dist/view/terrain/drape/geometry.d.ts +27 -0
- package/dist/view/terrain/drape/geometry.js +57 -6
- package/dist/view/terrain/drape/pass.d.ts +8 -3
- package/dist/view/terrain/drape/pass.js +39 -28
- package/dist/view/terrain/drape/renderer.d.ts +13 -0
- package/dist/view/terrain/drape/renderer.js +157 -30
- package/package.json +22 -10
|
@@ -25,8 +25,8 @@ export declare const DRAPE_MAX_VERTICES = 4000000;
|
|
|
25
25
|
export interface DrapeCollectResult {
|
|
26
26
|
/** The elements in draw order */
|
|
27
27
|
readonly elements: DrapeElement[];
|
|
28
|
-
/** The IDs of the Store features that cannot be put on the drape (points, images
|
|
29
|
-
*
|
|
28
|
+
/** The IDs of the Store features that cannot be put on the drape (points, images and so
|
|
29
|
+
* on) */
|
|
30
30
|
readonly excluded: Set<string>;
|
|
31
31
|
/** The breaks in the stacking order (the positions of the images; dataset order =
|
|
32
32
|
* ascending afterElements) */
|
|
@@ -98,8 +98,13 @@ export interface DrapeDatasetSource {
|
|
|
98
98
|
* It is not narrowed down by the view. Narrowing it down would change the element order every
|
|
99
99
|
* time the view moves and require rebuilding the tile indices (the narrowing down within a
|
|
100
100
|
* tile is done by `binTile` using the bounding boxes).
|
|
101
|
+
*
|
|
102
|
+
* @param pixelRatio The rendering pixel ratio, for widths fixed in screen pixels (the rendering
|
|
103
|
+
* scale included; `resolvePixelRatio`)
|
|
104
|
+
* @param contentPixelRatio The rendering pixel ratio for widths that follow the zoom (the
|
|
105
|
+
* rendering scale not applied; `resolveContentPixelRatio`)
|
|
101
106
|
*/
|
|
102
|
-
export declare function collectDrapeElements(store: Store, styles: RetainedStyleResolver, pixelRatio: number, datasets: readonly DrapeDatasetSource[]): DrapeCollectResult;
|
|
107
|
+
export declare function collectDrapeElements(store: Store, styles: RetainedStyleResolver, pixelRatio: number, contentPixelRatio: number, datasets: readonly DrapeDatasetSource[]): DrapeCollectResult;
|
|
103
108
|
/**
|
|
104
109
|
* Whether the analytic drape may be used
|
|
105
110
|
*
|
|
@@ -20,6 +20,7 @@ import { coordinatesOf } from '../../../shared/utils/coordinates.js';
|
|
|
20
20
|
import { getCreatedZoom } from '../../../shared/utils/property.js';
|
|
21
21
|
import { getDisplayFeatures, isLocallyHidden } from '../../../store/local-visibility.js';
|
|
22
22
|
import { layerDrawFactors } from '../../renderers/draw-factors.js';
|
|
23
|
+
import { dashCoefficientsOf } from '../../renderers/line/dash.js';
|
|
23
24
|
import { drapeSelectionKey, } from './binning.js';
|
|
24
25
|
import { drapeBoundsOf, drapeGeometryOf } from './geometry.js';
|
|
25
26
|
import { DRAPE_MAX_SOURCES } from './renderer.js';
|
|
@@ -74,8 +75,13 @@ export function drapeLayerSource(layerIndex, datasetCount) {
|
|
|
74
75
|
* It is not narrowed down by the view. Narrowing it down would change the element order every
|
|
75
76
|
* time the view moves and require rebuilding the tile indices (the narrowing down within a
|
|
76
77
|
* tile is done by `binTile` using the bounding boxes).
|
|
78
|
+
*
|
|
79
|
+
* @param pixelRatio The rendering pixel ratio, for widths fixed in screen pixels (the rendering
|
|
80
|
+
* scale included; `resolvePixelRatio`)
|
|
81
|
+
* @param contentPixelRatio The rendering pixel ratio for widths that follow the zoom (the
|
|
82
|
+
* rendering scale not applied; `resolveContentPixelRatio`)
|
|
77
83
|
*/
|
|
78
|
-
export function collectDrapeElements(store, styles, pixelRatio, datasets) {
|
|
84
|
+
export function collectDrapeElements(store, styles, pixelRatio, contentPixelRatio, datasets) {
|
|
79
85
|
const visible = getDisplayFeatures(store);
|
|
80
86
|
const byLayer = new Map();
|
|
81
87
|
for (const feature of visible) {
|
|
@@ -97,6 +103,7 @@ export function collectDrapeElements(store, styles, pixelRatio, datasets) {
|
|
|
97
103
|
drapedDatasets: new Set(),
|
|
98
104
|
styles,
|
|
99
105
|
pixelRatio,
|
|
106
|
+
contentPixelRatio,
|
|
100
107
|
layerSources: new Map(),
|
|
101
108
|
};
|
|
102
109
|
const byId = new Map();
|
|
@@ -168,21 +175,13 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
|
|
|
168
175
|
}
|
|
169
176
|
if (feature.type === 'Polygon' || feature.type === 'MultiPolygon') {
|
|
170
177
|
const { fillColor, strokeStyle } = context.styles.getPolygonStyles(feature, layer);
|
|
171
|
-
// Dashed lines are not put through the analytic evaluation (solving the accumulated
|
|
172
|
-
// distance along the outline from the pixel is not implemented). Drawing them as solid
|
|
173
|
-
// lines would change the appearance, so features with dashed lines are routed to the
|
|
174
|
-
// previous vertex displacement path.
|
|
175
|
-
if (strokeStyle.lineStyle !== 'solid') {
|
|
176
|
-
context.excluded.add(feature.id);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
178
|
const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
|
|
180
179
|
const rings = polygonRings(feature);
|
|
181
180
|
if (rings.length === 0)
|
|
182
181
|
return;
|
|
183
182
|
if (fillColor[3] <= 0 && !hasStroke)
|
|
184
183
|
return;
|
|
185
|
-
const polygonWidth = widthOf(feature, strokeStyle.width, context
|
|
184
|
+
const polygonWidth = widthOf(feature, strokeStyle.width, context);
|
|
186
185
|
pushElement(context, feature, 0, rings, {
|
|
187
186
|
fill: [fillColor[0], fillColor[1], fillColor[2], fillColor[3] * bakedOpacity],
|
|
188
187
|
stroke: [
|
|
@@ -192,6 +191,7 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
|
|
|
192
191
|
strokeStyle.color[3] * (hasStroke ? strokeStyle.opacity : 0) * bakedOpacity,
|
|
193
192
|
],
|
|
194
193
|
strokeWidthPx: hasStroke ? polygonWidth.px : 0,
|
|
194
|
+
dash: dashOf(strokeStyle.lineStyle, polygonWidth),
|
|
195
195
|
source,
|
|
196
196
|
widthZoom: polygonWidth.zoom,
|
|
197
197
|
// A Store selection is expressed by the selection UI (the box and the handles). The
|
|
@@ -201,16 +201,12 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
|
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
203
|
const strokeStyle = context.styles.getLineStringStrokeStyle(feature, layer);
|
|
204
|
-
if (strokeStyle.lineStyle !== 'solid') {
|
|
205
|
-
context.excluded.add(feature.id);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
204
|
if (strokeStyle.opacity <= 0 || strokeStyle.width <= 0)
|
|
209
205
|
return;
|
|
210
206
|
const paths = linePaths(feature);
|
|
211
207
|
if (paths.length === 0)
|
|
212
208
|
return;
|
|
213
|
-
const lineWidth = widthOf(feature, strokeStyle.width, context
|
|
209
|
+
const lineWidth = widthOf(feature, strokeStyle.width, context);
|
|
214
210
|
pushElement(context, feature, 1, paths, {
|
|
215
211
|
fill: [0, 0, 0, 0],
|
|
216
212
|
stroke: [
|
|
@@ -220,6 +216,7 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
|
|
|
220
216
|
strokeStyle.color[3] * strokeStyle.opacity * bakedOpacity,
|
|
221
217
|
],
|
|
222
218
|
strokeWidthPx: lineWidth.px,
|
|
219
|
+
dash: dashOf(strokeStyle.lineStyle, lineWidth),
|
|
223
220
|
source,
|
|
224
221
|
widthZoom: lineWidth.zoom,
|
|
225
222
|
selectionKey: '',
|
|
@@ -243,16 +240,12 @@ function collectDataset(context, dataset, source) {
|
|
|
243
240
|
if (feature.type === 'Polygon' || feature.type === 'MultiPolygon') {
|
|
244
241
|
const { fillColor, strokeStyle } = context.styles.getPolygonStyles(feature);
|
|
245
242
|
const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
|
|
246
|
-
// A dashed outline is drawn by the immediate path on the retained batch side (the
|
|
247
|
-
// paired test)
|
|
248
|
-
if (hasStroke && strokeStyle.lineStyle !== 'solid')
|
|
249
|
-
continue;
|
|
250
243
|
if (!hasStroke && fillColor[3] <= 0)
|
|
251
244
|
continue;
|
|
252
245
|
const rings = polygonRings(feature);
|
|
253
246
|
if (rings.length === 0)
|
|
254
247
|
continue;
|
|
255
|
-
const width = widthOf(feature, strokeStyle.width, context
|
|
248
|
+
const width = widthOf(feature, strokeStyle.width, context);
|
|
256
249
|
pushElement(context, coordinatesOf(feature), 0, rings, {
|
|
257
250
|
fill: fillColor,
|
|
258
251
|
stroke: [
|
|
@@ -262,6 +255,7 @@ function collectDataset(context, dataset, source) {
|
|
|
262
255
|
hasStroke ? strokeStyle.color[3] : 0,
|
|
263
256
|
],
|
|
264
257
|
strokeWidthPx: hasStroke ? width.px : 0,
|
|
258
|
+
dash: dashOf(strokeStyle.lineStyle, width),
|
|
265
259
|
source,
|
|
266
260
|
widthZoom: width.zoom,
|
|
267
261
|
selectionKey: drapeSelectionKey(dataset.id, feature.id),
|
|
@@ -274,14 +268,12 @@ function collectDataset(context, dataset, source) {
|
|
|
274
268
|
continue;
|
|
275
269
|
}
|
|
276
270
|
const strokeStyle = context.styles.getLineStringStrokeStyle(feature);
|
|
277
|
-
if (strokeStyle.lineStyle !== 'solid')
|
|
278
|
-
continue;
|
|
279
271
|
if (strokeStyle.opacity <= 0 || strokeStyle.width <= 0)
|
|
280
272
|
continue;
|
|
281
273
|
const paths = linePaths(feature);
|
|
282
274
|
if (paths.length === 0)
|
|
283
275
|
continue;
|
|
284
|
-
const width = widthOf(feature, strokeStyle.width, context
|
|
276
|
+
const width = widthOf(feature, strokeStyle.width, context);
|
|
285
277
|
pushElement(context, coordinatesOf(feature), 1, paths, {
|
|
286
278
|
fill: [0, 0, 0, 0],
|
|
287
279
|
stroke: [
|
|
@@ -291,6 +283,7 @@ function collectDataset(context, dataset, source) {
|
|
|
291
283
|
strokeStyle.color[3] * strokeStyle.opacity,
|
|
292
284
|
],
|
|
293
285
|
strokeWidthPx: width.px,
|
|
286
|
+
dash: dashOf(strokeStyle.lineStyle, width),
|
|
294
287
|
source,
|
|
295
288
|
widthZoom: width.zoom,
|
|
296
289
|
selectionKey: drapeSelectionKey(dataset.id, feature.id),
|
|
@@ -301,15 +294,31 @@ function collectDataset(context, dataset, source) {
|
|
|
301
294
|
* Resolves the line width with the same convention as the retained batch path
|
|
302
295
|
*
|
|
303
296
|
* A feature without a `createdZoom` is fixed in screen pixels (the rendering scale applies),
|
|
304
|
-
* and a feature with one scales by `2^(zoom - createdZoom)
|
|
305
|
-
*
|
|
297
|
+
* and a feature with one scales by `2^(zoom - createdZoom)` (the device pixel ratio applies,
|
|
298
|
+
* the rendering scale does not). So that the thickness does not change with the presence of
|
|
299
|
+
* terrain, the drape follows the same convention.
|
|
306
300
|
*/
|
|
307
|
-
function widthOf(feature, width,
|
|
301
|
+
function widthOf(feature, width, ratios) {
|
|
308
302
|
const createdZoom = getCreatedZoom(feature);
|
|
309
303
|
if (createdZoom !== undefined && Number.isFinite(createdZoom)) {
|
|
310
|
-
|
|
304
|
+
const ratio = ratios.contentPixelRatio;
|
|
305
|
+
return { px: width * ratio, zoom: createdZoom, ratio };
|
|
311
306
|
}
|
|
312
|
-
return { px: width * pixelRatio, zoom: -1 };
|
|
307
|
+
return { px: width * ratios.pixelRatio, zoom: -1, ratio: ratios.pixelRatio };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The dash pattern of an outline, or null for a solid one
|
|
311
|
+
*
|
|
312
|
+
* The coefficients come from the table the subdividing path uses (`dash.ts`), and the
|
|
313
|
+
* shader turns them into the dash and the gap of the width the line has in the frame, in
|
|
314
|
+
* CSS pixels as the subdividing path measures them: the pixel ratio the width was resolved
|
|
315
|
+
* with goes along to take back out.
|
|
316
|
+
*/
|
|
317
|
+
function dashOf(lineStyle, width) {
|
|
318
|
+
const coefficients = dashCoefficientsOf(lineStyle);
|
|
319
|
+
if (!coefficients || width.ratio <= 0)
|
|
320
|
+
return null;
|
|
321
|
+
return { ...coefficients, pixelRatio: width.ratio };
|
|
313
322
|
}
|
|
314
323
|
/** The rings of a polygon (with the closing point dropped) */
|
|
315
324
|
function polygonRings(feature) {
|
|
@@ -343,6 +352,8 @@ function pushElement(context, cacheKey, kind, paths, style) {
|
|
|
343
352
|
// is not converted)
|
|
344
353
|
geometry: () => drapeGeometryOf(cacheKey, paths, kind === 0),
|
|
345
354
|
...style,
|
|
355
|
+
// An outline that is not drawn has no pattern to lay out
|
|
356
|
+
dash: style.strokeWidthPx > 0 ? style.dash : null,
|
|
346
357
|
});
|
|
347
358
|
context.edgeCount += edges;
|
|
348
359
|
}
|
|
@@ -26,6 +26,13 @@ import { type DrapePackBuilder } from './bin-store.js';
|
|
|
26
26
|
import { EDGE_UNITS } from './edge-constrain.js';
|
|
27
27
|
import type { ResolvedTileEdges } from './stitch.js';
|
|
28
28
|
export { EDGE_UNITS };
|
|
29
|
+
/**
|
|
30
|
+
* The texture unit of the starts of the edges along their paths
|
|
31
|
+
*
|
|
32
|
+
* 5 and 7 to 9 hold the DEMs of the edges (`EDGE_UNITS`) and 6 the DEM atlas, so it comes after
|
|
33
|
+
* them (WebGL2 guarantees 16 units to a fragment shader).
|
|
34
|
+
*/
|
|
35
|
+
export declare const DRAPE_EDGE_START_UNIT = 10;
|
|
29
36
|
/**
|
|
30
37
|
* Upper limit on the sources of the factors
|
|
31
38
|
*
|
|
@@ -92,6 +99,8 @@ export interface DrapeTileDraw {
|
|
|
92
99
|
readonly grid: number;
|
|
93
100
|
/** The ground size of this tile (meters). Used to compute the shading gradient */
|
|
94
101
|
readonly groundMeters: number;
|
|
102
|
+
/** The zoom of this tile (it scales the lengths of the edges into dash pattern pixels) */
|
|
103
|
+
readonly tileZ: number;
|
|
95
104
|
/**
|
|
96
105
|
* The constraint for each edge (resolving T-junctions)
|
|
97
106
|
*
|
|
@@ -123,10 +132,12 @@ export declare class DrapeRenderer {
|
|
|
123
132
|
private variantName;
|
|
124
133
|
private mesh;
|
|
125
134
|
private edgeTex;
|
|
135
|
+
private edgeStartTex;
|
|
126
136
|
private runTex;
|
|
127
137
|
private cellTex;
|
|
128
138
|
private styleTex;
|
|
129
139
|
private edgeTexSize;
|
|
140
|
+
private edgeStartTexSize;
|
|
130
141
|
private runTexSize;
|
|
131
142
|
private cellTexSize;
|
|
132
143
|
private styleTexSize;
|
|
@@ -179,6 +190,8 @@ export declare class DrapeRenderer {
|
|
|
179
190
|
*
|
|
180
191
|
* texStorage2D is immutable, so the texture is recreated whenever the required
|
|
181
192
|
* size changes (once recreated, everything is sent again).
|
|
193
|
+
*
|
|
194
|
+
* @param channels The numbers per texel: 4 (RGBA32F) or 1 (R32F)
|
|
182
195
|
*/
|
|
183
196
|
private uploadTexture;
|
|
184
197
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
import { SELECTION_HIGHLIGHT_FILL_OPACITY, SELECTION_HIGHLIGHT_RGB, SELECTION_HIGHLIGHT_STROKE_EXTRA, } from '../../../shared/config/selection-highlight.js';
|
|
4
4
|
import { createProgram } from '../../shaders/helpers.js';
|
|
5
|
-
import { DRAPE_SELECTION_TEXEL, DRAPE_STYLE_TEXELS } from './bin-store.js';
|
|
5
|
+
import { DRAPE_DASH_TEXEL, DRAPE_SELECTION_TEXEL, DRAPE_STYLE_TEXELS, } from './bin-store.js';
|
|
6
6
|
import { applyEdgeConstrainUniforms, EDGE_CONSTRAIN_GLSL, EDGE_CONSTRAIN_UNIFORMS, EDGE_UNITS, } from './edge-constrain.js';
|
|
7
7
|
import { createTerrainMeshBuffers, disposeTerrainMeshBuffers, TILE_EXTENT, } from './mesh.js';
|
|
8
8
|
import { applyDrapeProjectionUniforms, applyDrapeTerrainUniforms, DRAPE_PROJECTION_UNIFORMS, DRAPE_TERRAIN_UNIFORMS, TERRAIN_SAMPLE_GLSL, TERRAIN_SHADE_GLSL, } from './shared.js';
|
|
@@ -21,6 +21,15 @@ const UNIT_EDGE = 1;
|
|
|
21
21
|
const UNIT_RUN = 2;
|
|
22
22
|
const UNIT_CELL = 3;
|
|
23
23
|
const UNIT_STYLE = 4;
|
|
24
|
+
/**
|
|
25
|
+
* The texture unit of the starts of the edges along their paths
|
|
26
|
+
*
|
|
27
|
+
* 5 and 7 to 9 hold the DEMs of the edges (`EDGE_UNITS`) and 6 the DEM atlas, so it comes after
|
|
28
|
+
* them (WebGL2 guarantees 16 units to a fragment shader).
|
|
29
|
+
*/
|
|
30
|
+
export const DRAPE_EDGE_START_UNIT = 10;
|
|
31
|
+
/** The pixels of a 512-pixel tile, the size the dash patterns are measured in */
|
|
32
|
+
const DASH_TILE_PX = 512;
|
|
24
33
|
/**
|
|
25
34
|
* Upper limit on the sources of the factors
|
|
26
35
|
*
|
|
@@ -98,6 +107,8 @@ uniform sampler2D u_edge_tex;
|
|
|
98
107
|
uniform sampler2D u_run_tex;
|
|
99
108
|
uniform sampler2D u_cell_tex;
|
|
100
109
|
uniform sampler2D u_style_tex;
|
|
110
|
+
// Where each edge starts along its path (Mercator units), in step with u_edge_tex
|
|
111
|
+
uniform sampler2D u_edge_start_tex;
|
|
101
112
|
uniform int u_data_width;
|
|
102
113
|
uniform int u_cell_base;
|
|
103
114
|
uniform float u_cells;
|
|
@@ -128,6 +139,11 @@ uniform vec2 u_paint_range;
|
|
|
128
139
|
// Whether to compose the selection highlight in this draw (1 only for the final
|
|
129
140
|
// segment)
|
|
130
141
|
uniform float u_emit_selection;
|
|
142
|
+
// The pixels of a dash pattern per unit of the Mercator world (512 * 2^zoom) and per
|
|
143
|
+
// unit of this tile (512 * 2^(zoom - tile zoom)). A pattern is measured the way the
|
|
144
|
+
// subdividing path measures it: in pixels of the flat map at the zoom of the frame
|
|
145
|
+
uniform float u_dash_world_px;
|
|
146
|
+
uniform float u_dash_tile_px;
|
|
131
147
|
|
|
132
148
|
out vec4 fragColor;
|
|
133
149
|
|
|
@@ -153,12 +169,42 @@ bool crosses(vec2 p, vec2 q, vec2 a, vec2 b) {
|
|
|
153
169
|
return (d1 * d2 < 0.0) && (d3 * d4 < 0.0);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
|
-
// The shortest vector from
|
|
157
|
-
vec2 closestVector(vec2
|
|
172
|
+
// The shortest vector from the origin to a segment
|
|
173
|
+
vec2 closestVector(vec2 a, vec2 b) {
|
|
158
174
|
vec2 ab = b - a;
|
|
159
175
|
float len2 = dot(ab, ab);
|
|
160
|
-
float t = len2 > 0.0 ? clamp(dot(
|
|
161
|
-
return
|
|
176
|
+
float t = len2 > 0.0 ? clamp(-dot(a, ab) / len2, 0.0, 1.0) : 0.0;
|
|
177
|
+
return a + ab * t;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The screen distance from the pixel to the dashes of a pattern on one edge
|
|
181
|
+
//
|
|
182
|
+
// sa and sb are the ends of the edge on the screen, around the pixel. start and len are
|
|
183
|
+
// where the edge starts along its path and how long it is, and dash and period those of
|
|
184
|
+
// the pattern (all in pattern pixels). The place along the edge is taken where the pixel
|
|
185
|
+
// is closest to it on the screen, so the dash picked is the one closest on the screen.
|
|
186
|
+
// The dash there and the next one are measured: between them lies a gap, and the pixel
|
|
187
|
+
// is closer to one of their ends. A dash is cut where the edge ends; the dash goes on
|
|
188
|
+
// on the next edge, which draws its own part, and the two parts meet in a round joint
|
|
189
|
+
// as a solid line does. Each part is a segment with round caps, so a dash gets the
|
|
190
|
+
// round caps of the subdividing path at its ends.
|
|
191
|
+
float dashDistance(vec2 sa, vec2 sb, float start, float len, float dash, float period) {
|
|
192
|
+
if (len <= 0.0) return 1.0e9;
|
|
193
|
+
vec2 ab = sb - sa;
|
|
194
|
+
float len2 = dot(ab, ab);
|
|
195
|
+
float u = len2 > 0.0 ? clamp(-dot(sa, ab) / len2, 0.0, 1.0) : 0.0;
|
|
196
|
+
float k = floor((start + u * len) / period);
|
|
197
|
+
float best = 1.0e9;
|
|
198
|
+
for (int i = 0; i < 2; i++) {
|
|
199
|
+
float from = (k + float(i)) * period;
|
|
200
|
+
float u0 = (from - start) / len;
|
|
201
|
+
float u1 = (from + dash - start) / len;
|
|
202
|
+
if (u1 <= 0.0 || u0 >= 1.0) continue;
|
|
203
|
+
u0 = clamp(u0, 0.0, 1.0);
|
|
204
|
+
u1 = clamp(u1, 0.0, 1.0);
|
|
205
|
+
best = min(best, length(closestVector(sa + ab * u0, sa + ab * u1)));
|
|
206
|
+
}
|
|
207
|
+
return best;
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
// Compose on top (premultiplied alpha)
|
|
@@ -224,20 +270,12 @@ void main() {
|
|
|
224
270
|
> 0.5;
|
|
225
271
|
if (!paints && !selected) continue;
|
|
226
272
|
|
|
227
|
-
float minDist = 1.0e9;
|
|
228
|
-
for (int e = 0; e < edgeCount; e++) {
|
|
229
|
-
vec4 seg = fetchData(u_edge_tex, edgeStart + e);
|
|
230
|
-
vec2 a = seg.xy;
|
|
231
|
-
vec2 b = seg.zw;
|
|
232
|
-
if (crosses(origin, p, a, b)) inside = !inside;
|
|
233
|
-
vec2 offset = closestVector(p, a, b);
|
|
234
|
-
minDist = min(minDist, length(jacobianInverse * offset));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
273
|
int styleAt = elementIndex * ${DRAPE_STYLE_TEXELS};
|
|
238
274
|
vec4 fill = fetchData(u_style_tex, styleAt);
|
|
239
275
|
vec4 stroke = fetchData(u_style_tex, styleAt + 1);
|
|
240
276
|
vec4 params = fetchData(u_style_tex, styleAt + 2);
|
|
277
|
+
// (a, b, c, the physical pixels of the width per CSS pixel; 0 for a solid outline)
|
|
278
|
+
vec4 dashSpec = fetchData(u_style_tex, styleAt + ${DRAPE_DASH_TEXEL});
|
|
241
279
|
int source = int(params.z);
|
|
242
280
|
vec2 factors = source >= 0 && source < ${DRAPE_MAX_SOURCES}
|
|
243
281
|
? u_source_factors[source]
|
|
@@ -248,6 +286,49 @@ void main() {
|
|
|
248
286
|
float halfWidth = params.x * 0.5 * factors.y * widthScale;
|
|
249
287
|
bool isPolygon = params.y < 0.5;
|
|
250
288
|
|
|
289
|
+
// A dashed outline: the dash and the gap follow from the width the line has in
|
|
290
|
+
// this frame (in CSS pixels), with the formulas of the subdividing path
|
|
291
|
+
// (dash.ts): dash = max(a, b * w), gap = max(w + 1, c * w)
|
|
292
|
+
bool dashed = dashSpec.w > 0.0;
|
|
293
|
+
float dashLength = 0.0;
|
|
294
|
+
float dashPeriod = 1.0;
|
|
295
|
+
if (dashed) {
|
|
296
|
+
float w = 2.0 * halfWidth / dashSpec.w;
|
|
297
|
+
dashLength = max(dashSpec.x, dashSpec.y * w);
|
|
298
|
+
dashPeriod = dashLength + max(w + 1.0, dashSpec.z * w);
|
|
299
|
+
}
|
|
300
|
+
// The distance to the whole outline is what the fill and the selection read. A
|
|
301
|
+
// dashed outline measures its dashes apart, and the whole outline only when
|
|
302
|
+
// something reads it
|
|
303
|
+
bool wantsEdges = !dashed || selected || (paints && isPolygon && fill.a > 0.0);
|
|
304
|
+
bool wantsDashes = dashed && paints;
|
|
305
|
+
|
|
306
|
+
float minDist = 1.0e9;
|
|
307
|
+
float dashDist = 1.0e9;
|
|
308
|
+
for (int e = 0; e < edgeCount; e++) {
|
|
309
|
+
vec4 seg = fetchData(u_edge_tex, edgeStart + e);
|
|
310
|
+
vec2 a = seg.xy;
|
|
311
|
+
vec2 b = seg.zw;
|
|
312
|
+
if (crosses(origin, p, a, b)) inside = !inside;
|
|
313
|
+
// The distance is taken on the screen: the segment is mapped to screen pixels
|
|
314
|
+
// around the pixel first. The closest point found on the ground and mapped
|
|
315
|
+
// afterwards is not the closest one on the screen where the ground is seen
|
|
316
|
+
// at a slant, and the line came out thinner between its vertices than at them
|
|
317
|
+
vec2 sa = jacobianInverse * (a - p);
|
|
318
|
+
vec2 sb = jacobianInverse * (b - p);
|
|
319
|
+
if (wantsEdges) minDist = min(minDist, length(closestVector(sa, sb)));
|
|
320
|
+
if (wantsDashes) {
|
|
321
|
+
int at = edgeStart + e;
|
|
322
|
+
float start = texelFetch(
|
|
323
|
+
u_edge_start_tex, ivec2(at % u_data_width, at / u_data_width), 0
|
|
324
|
+
).r * u_dash_world_px;
|
|
325
|
+
float len = length(b - a) * u_dash_tile_px;
|
|
326
|
+
dashDist = min(dashDist, dashDistance(sa, sb, start, len, dashLength, dashPeriod));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// The outline is drawn where its dashes are (the whole of it when it is solid)
|
|
330
|
+
float strokeDist = dashed ? dashDist : minDist;
|
|
331
|
+
|
|
251
332
|
if (paints && isPolygon && fill.a > 0.0) {
|
|
252
333
|
// A signed distance, negative inside and positive outside (screen pixels)
|
|
253
334
|
float signedDist = inside ? -minDist : minDist;
|
|
@@ -261,7 +342,7 @@ void main() {
|
|
|
261
342
|
}
|
|
262
343
|
|
|
263
344
|
if (paints && halfWidth > 0.0 && stroke.a > 0.0) {
|
|
264
|
-
float coverage = clamp(halfWidth + 0.5 -
|
|
345
|
+
float coverage = clamp(halfWidth + 0.5 - strokeDist, 0.0, 1.0);
|
|
265
346
|
if (coverage > 0.0) acc = over(acc, stroke.rgb, stroke.a * coverage * factors.x);
|
|
266
347
|
}
|
|
267
348
|
|
|
@@ -283,7 +364,8 @@ void main() {
|
|
|
283
364
|
// The added thickness is kept constant in screen pixels (so that "an
|
|
284
365
|
// outline one size thicker" always looks the same regardless of the
|
|
285
366
|
// unit of the original thickness). The highlight outline is also drawn
|
|
286
|
-
// for features that have no outline (the same as the immediate mode)
|
|
367
|
+
// for features that have no outline (the same as the immediate mode), and
|
|
368
|
+
// it is solid under a dashed outline too
|
|
287
369
|
float selHalf = halfWidth + u_selection_stroke_extra * 0.5;
|
|
288
370
|
selStroke = max(selStroke, clamp(selHalf + 0.5 - minDist, 0.0, 1.0) * factors.x);
|
|
289
371
|
}
|
|
@@ -339,6 +421,12 @@ export class DrapeRenderer {
|
|
|
339
421
|
writable: true,
|
|
340
422
|
value: null
|
|
341
423
|
});
|
|
424
|
+
Object.defineProperty(this, "edgeStartTex", {
|
|
425
|
+
enumerable: true,
|
|
426
|
+
configurable: true,
|
|
427
|
+
writable: true,
|
|
428
|
+
value: null
|
|
429
|
+
});
|
|
342
430
|
Object.defineProperty(this, "runTex", {
|
|
343
431
|
enumerable: true,
|
|
344
432
|
configurable: true,
|
|
@@ -363,6 +451,12 @@ export class DrapeRenderer {
|
|
|
363
451
|
writable: true,
|
|
364
452
|
value: [0, 0]
|
|
365
453
|
});
|
|
454
|
+
Object.defineProperty(this, "edgeStartTexSize", {
|
|
455
|
+
enumerable: true,
|
|
456
|
+
configurable: true,
|
|
457
|
+
writable: true,
|
|
458
|
+
value: [0, 0]
|
|
459
|
+
});
|
|
366
460
|
Object.defineProperty(this, "runTexSize", {
|
|
367
461
|
enumerable: true,
|
|
368
462
|
configurable: true,
|
|
@@ -385,7 +479,7 @@ export class DrapeRenderer {
|
|
|
385
479
|
enumerable: true,
|
|
386
480
|
configurable: true,
|
|
387
481
|
writable: true,
|
|
388
|
-
value:
|
|
482
|
+
value: emptyUploadMark()
|
|
389
483
|
});
|
|
390
484
|
/** The selection version the transferred style table reflects (-1 = not transferred) */
|
|
391
485
|
Object.defineProperty(this, "uploadedSelection", {
|
|
@@ -450,6 +544,7 @@ export class DrapeRenderer {
|
|
|
450
544
|
'u_run_tex',
|
|
451
545
|
'u_cell_tex',
|
|
452
546
|
'u_style_tex',
|
|
547
|
+
'u_edge_start_tex',
|
|
453
548
|
'u_data_width',
|
|
454
549
|
'u_cell_base',
|
|
455
550
|
'u_cells',
|
|
@@ -464,6 +559,8 @@ export class DrapeRenderer {
|
|
|
464
559
|
'u_selection_stroke_extra',
|
|
465
560
|
'u_paint_range',
|
|
466
561
|
'u_emit_selection',
|
|
562
|
+
'u_dash_world_px',
|
|
563
|
+
'u_dash_tile_px',
|
|
467
564
|
...EDGE_CONSTRAIN_UNIFORMS,
|
|
468
565
|
];
|
|
469
566
|
this.locations = {};
|
|
@@ -485,24 +582,28 @@ export class DrapeRenderer {
|
|
|
485
582
|
*/
|
|
486
583
|
upload(pack) {
|
|
487
584
|
if (this.uploaded.builderId !== pack.id) {
|
|
488
|
-
this.uploaded = { builderId: pack.id
|
|
585
|
+
this.uploaded = { ...emptyUploadMark(), builderId: pack.id };
|
|
489
586
|
this.uploadedSelection = -1;
|
|
490
587
|
}
|
|
491
588
|
// The selection highlight rewrites rows that have already been sent (it is not
|
|
492
589
|
// an append), so when the version moves only the style table is sent again from
|
|
493
|
-
// the beginning. The style table is
|
|
590
|
+
// the beginning. The style table is a few texels per element, orders of magnitude
|
|
494
591
|
// smaller than the edge table
|
|
495
592
|
if (this.uploadedSelection !== pack.selectionVersion) {
|
|
496
593
|
this.uploaded.styles = 0;
|
|
497
594
|
this.uploadedSelection = pack.selectionVersion;
|
|
498
595
|
}
|
|
499
596
|
this.edgeTex = this.uploadTexture(this.edgeTex, 'edgeTexSize', 'edges', DATA_TEX_WIDTH, pack.edges, pack.edgeCount);
|
|
597
|
+
// The starts along the paths go one per texel, in step with the edges
|
|
598
|
+
this.edgeStartTex = this.uploadTexture(this.edgeStartTex, 'edgeStartTexSize', 'edgeStarts', DATA_TEX_WIDTH, pack.edgeStarts, pack.edgeCount, 1);
|
|
500
599
|
this.runTex = this.uploadTexture(this.runTex, 'runTexSize', 'runs', DATA_TEX_WIDTH, pack.runs, pack.runCount);
|
|
501
600
|
this.cellTex = this.uploadTexture(this.cellTex, 'cellTexSize', 'cells', DATA_TEX_WIDTH, pack.cells, pack.cellCount);
|
|
502
|
-
// The style table is laid out like the others (
|
|
503
|
-
//
|
|
601
|
+
// The style table is laid out like the others (the texels of the elements one after
|
|
602
|
+
// another in rows of DATA_TEX_WIDTH; an element may run over into the next row), so its
|
|
603
|
+
// height grows with the elements / DATA_TEX_WIDTH, not with the elements
|
|
504
604
|
this.styleTex = this.uploadTexture(this.styleTex, 'styleTexSize', 'styles', DATA_TEX_WIDTH, pack.styles, DRAPE_STYLE_TEXELS * Math.max(1, pack.elementCount));
|
|
505
605
|
return (this.edgeTex !== null &&
|
|
606
|
+
this.edgeStartTex !== null &&
|
|
506
607
|
this.runTex !== null &&
|
|
507
608
|
this.cellTex !== null &&
|
|
508
609
|
this.styleTex !== null);
|
|
@@ -522,7 +623,12 @@ export class DrapeRenderer {
|
|
|
522
623
|
const gl = this.gl;
|
|
523
624
|
const program = this.program;
|
|
524
625
|
const mesh = this.mesh;
|
|
525
|
-
if (!program ||
|
|
626
|
+
if (!program ||
|
|
627
|
+
!mesh ||
|
|
628
|
+
!this.edgeTex ||
|
|
629
|
+
!this.edgeStartTex ||
|
|
630
|
+
!this.cellTex ||
|
|
631
|
+
!this.styleTex) {
|
|
526
632
|
this.lastError = 'draw: missing resources';
|
|
527
633
|
return;
|
|
528
634
|
}
|
|
@@ -534,6 +640,7 @@ export class DrapeRenderer {
|
|
|
534
640
|
this.bindData(UNIT_RUN, this.runTex, set.u_run_tex);
|
|
535
641
|
this.bindData(UNIT_CELL, this.cellTex, set.u_cell_tex);
|
|
536
642
|
this.bindData(UNIT_STYLE, this.styleTex, set.u_style_tex);
|
|
643
|
+
this.bindData(DRAPE_EDGE_START_UNIT, this.edgeStartTex, set.u_edge_start_tex);
|
|
537
644
|
if (set.u_data_width)
|
|
538
645
|
gl.uniform1i(set.u_data_width, DATA_TEX_WIDTH);
|
|
539
646
|
if (set.u_cell_base)
|
|
@@ -544,6 +651,11 @@ export class DrapeRenderer {
|
|
|
544
651
|
gl.uniform1f(set.u_opacity, opacity);
|
|
545
652
|
if (set.u_zoom)
|
|
546
653
|
gl.uniform1f(set.u_zoom, zoom);
|
|
654
|
+
if (set.u_dash_world_px)
|
|
655
|
+
gl.uniform1f(set.u_dash_world_px, DASH_TILE_PX * 2 ** zoom);
|
|
656
|
+
if (set.u_dash_tile_px) {
|
|
657
|
+
gl.uniform1f(set.u_dash_tile_px, DASH_TILE_PX * 2 ** (zoom - draw.tileZ));
|
|
658
|
+
}
|
|
547
659
|
if (set['u_source_factors[0]']) {
|
|
548
660
|
gl.uniform2fv(set['u_source_factors[0]'], sourceFactors);
|
|
549
661
|
}
|
|
@@ -604,19 +716,27 @@ export class DrapeRenderer {
|
|
|
604
716
|
disposeTerrainMeshBuffers(gl, this.mesh);
|
|
605
717
|
this.mesh = null;
|
|
606
718
|
}
|
|
607
|
-
for (const texture of [
|
|
719
|
+
for (const texture of [
|
|
720
|
+
this.edgeTex,
|
|
721
|
+
this.edgeStartTex,
|
|
722
|
+
this.runTex,
|
|
723
|
+
this.cellTex,
|
|
724
|
+
this.styleTex,
|
|
725
|
+
]) {
|
|
608
726
|
if (texture)
|
|
609
727
|
gl.deleteTexture(texture);
|
|
610
728
|
}
|
|
611
729
|
this.edgeTex = null;
|
|
730
|
+
this.edgeStartTex = null;
|
|
612
731
|
this.runTex = null;
|
|
613
732
|
this.cellTex = null;
|
|
614
733
|
this.styleTex = null;
|
|
615
734
|
this.edgeTexSize = [0, 0];
|
|
735
|
+
this.edgeStartTexSize = [0, 0];
|
|
616
736
|
this.runTexSize = [0, 0];
|
|
617
737
|
this.cellTexSize = [0, 0];
|
|
618
738
|
this.styleTexSize = [0, 0];
|
|
619
|
-
this.uploaded =
|
|
739
|
+
this.uploaded = emptyUploadMark();
|
|
620
740
|
this.uploadedSelection = -1;
|
|
621
741
|
}
|
|
622
742
|
bindData(unit, texture, location) {
|
|
@@ -639,8 +759,10 @@ export class DrapeRenderer {
|
|
|
639
759
|
*
|
|
640
760
|
* texStorage2D is immutable, so the texture is recreated whenever the required
|
|
641
761
|
* size changes (once recreated, everything is sent again).
|
|
762
|
+
*
|
|
763
|
+
* @param channels The numbers per texel: 4 (RGBA32F) or 1 (R32F)
|
|
642
764
|
*/
|
|
643
|
-
uploadTexture(texture, sizeField, markField, width, data, texelCount) {
|
|
765
|
+
uploadTexture(texture, sizeField, markField, width, data, texelCount, channels = 4) {
|
|
644
766
|
const gl = this.gl;
|
|
645
767
|
const size = this[sizeField];
|
|
646
768
|
const neededRows = Math.max(1, Math.ceil(texelCount / width));
|
|
@@ -663,7 +785,7 @@ export class DrapeRenderer {
|
|
|
663
785
|
// Allocate a few extra rows (so it is not recreated on every increase)
|
|
664
786
|
const rows = Math.min(maxRows, Math.max(1, Math.ceil(neededRows * 1.5)));
|
|
665
787
|
gl.bindTexture(gl.TEXTURE_2D, target);
|
|
666
|
-
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, width, rows);
|
|
788
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, channels === 4 ? gl.RGBA32F : gl.R32F, width, rows);
|
|
667
789
|
// Only the allocation is checked (it is rare; getError is a synchronous round trip)
|
|
668
790
|
if (gl.getError() !== gl.NO_ERROR) {
|
|
669
791
|
gl.deleteTexture(target);
|
|
@@ -688,16 +810,21 @@ export class DrapeRenderer {
|
|
|
688
810
|
const fromRow = Math.floor(uploaded / width);
|
|
689
811
|
const toRow = Math.ceil(texelCount / width);
|
|
690
812
|
const rows = toRow - fromRow;
|
|
691
|
-
const needed = rows * width *
|
|
692
|
-
const offset = fromRow * width *
|
|
813
|
+
const needed = rows * width * channels;
|
|
814
|
+
const offset = fromRow * width * channels;
|
|
693
815
|
const payload = data.length >= offset + needed
|
|
694
816
|
? data.subarray(offset, offset + needed)
|
|
695
817
|
: padTo(data.subarray(offset), needed);
|
|
696
|
-
|
|
818
|
+
const format = channels === 4 ? gl.RGBA : gl.RED;
|
|
819
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, fromRow, width, rows, format, gl.FLOAT, payload);
|
|
697
820
|
this.uploaded[markField] = texelCount;
|
|
698
821
|
return target;
|
|
699
822
|
}
|
|
700
823
|
}
|
|
824
|
+
/** Nothing transferred yet */
|
|
825
|
+
function emptyUploadMark() {
|
|
826
|
+
return { builderId: -1, edges: 0, edgeStarts: 0, runs: 0, cells: 0, styles: 0 };
|
|
827
|
+
}
|
|
701
828
|
/** Pads with 0 up to the length required for the transfer */
|
|
702
829
|
function padTo(data, length) {
|
|
703
830
|
const out = new Float32Array(length);
|