@sakuzu/maplibre-gl-draw 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/README.ja.md +2 -1
- package/README.md +2 -1
- package/dist/api/extension/store.d.ts +22 -1
- package/dist/api/impl/import-export/embedded-file.d.ts +2 -1
- package/dist/api/impl/import-export/embedded-file.js +12 -3
- package/dist/api/impl/import-export/geojson-export.d.ts +17 -3
- package/dist/api/impl/import-export/geojson-export.js +27 -6
- package/dist/api/impl/options.js +4 -1
- package/dist/api/model.d.ts +34 -0
- package/dist/api/model.js +30 -0
- package/dist/dataset/chunk-set.js +2 -3
- package/dist/dataset/dataset.js +6 -4
- package/dist/dataset/retained.js +3 -3
- package/dist/dataset/selection.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/shared/utils/image.d.ts +21 -7
- package/dist/shared/utils/image.js +73 -6
- package/dist/shared/utils/name-generator.d.ts +12 -0
- package/dist/shared/utils/name-generator.js +21 -8
- package/dist/store/draw-store.js +9 -0
- package/dist/store/store.d.ts +8 -1
- package/dist/view/renderers/batch-manager.js +25 -17
- package/dist/view/renderers/draw-factors.d.ts +11 -1
- package/dist/view/renderers/draw-factors.js +13 -0
- package/dist/view/renderers/line/line-shader.js +6 -1
- package/dist/view/renderers/point/point-instance.d.ts +19 -0
- package/dist/view/renderers/point/point-instance.js +47 -1
- package/dist/view/renderers/polygon/sdf-polygon.js +5 -1
- package/dist/view/terrain/drape/renderer.js +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,64 @@ the project follows semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.2.0] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
Features can be written as GeoJSON without a drawing, a store that holds
|
|
12
|
+
only part of the document can keep the numbers of the automatic names,
|
|
13
|
+
and an image keeps a small format on a browser that cannot encode WebP.
|
|
14
|
+
|
|
15
|
+
### Added in 2.2.0
|
|
16
|
+
|
|
17
|
+
- `featuresToGeoJSON(features, options)` and `featureToGeoJSON(feature,
|
|
18
|
+
options)` write features that need not be in a drawing with the rules of
|
|
19
|
+
`document.toGeoJSON`: rounding, ring orientation, the antimeridian, the
|
|
20
|
+
prefixed keys and the bbox. `ToGeoJSONOptions.getFile` reads the file of
|
|
21
|
+
an Image by its ID, and an Image whose file it does not find is written
|
|
22
|
+
without its pixels.
|
|
23
|
+
- `Store` gets two optional members, `getMaxNameNumber(type)` and
|
|
24
|
+
`recordNameNumber(type, number)`. A store that holds only part of the
|
|
25
|
+
document implements them so that a number used in the part it does not
|
|
26
|
+
hold is not used again for an automatic name. A store without them is
|
|
27
|
+
numbered as before.
|
|
28
|
+
|
|
29
|
+
### Changed in 2.2.0
|
|
30
|
+
|
|
31
|
+
- `FileData.mimeType` of an image that was converted is the type the
|
|
32
|
+
browser returned, not the one requested
|
|
33
|
+
([#31](https://github.com/sakuzu/maplibre-gl-draw/issues/31)).
|
|
34
|
+
- When the browser cannot encode WebP, an image with no transparent pixel
|
|
35
|
+
is encoded as JPEG (quality 0.92) instead of being kept as PNG, so a
|
|
36
|
+
photo stays small. An image with a transparent pixel keeps the format
|
|
37
|
+
the browser returns
|
|
38
|
+
([#34](https://github.com/sakuzu/maplibre-gl-draw/issues/34)).
|
|
39
|
+
|
|
40
|
+
## [2.1.4] - 2026-10-07
|
|
41
|
+
|
|
42
|
+
Two fixes to how sizes follow the zoom: the paths of a dataset that drew
|
|
43
|
+
at a fixed size, and sizes below one pixel that never disappeared.
|
|
44
|
+
|
|
45
|
+
### Fixed in 2.1.4
|
|
46
|
+
|
|
47
|
+
- A dataset applies the draw factors of its `zoomScale` (size and
|
|
48
|
+
opacity) on every path it draws through. The selection highlight and
|
|
49
|
+
the selected feature drawn on top of it, dashed and dotted lines and
|
|
50
|
+
polygon outlines, and shapes that go through immediate mode used to be
|
|
51
|
+
drawn at their stored size, so a selected point or a dashed line did
|
|
52
|
+
not change with the zoom. The dash pattern is now made from the scaled
|
|
53
|
+
width. `BatchManager.beginFrame` and `DisplayBatchTarget.beginFrame`
|
|
54
|
+
take an optional `factors` that is combined with the layer factors
|
|
55
|
+
([#28](https://github.com/sakuzu/maplibre-gl-draw/issues/28)).
|
|
56
|
+
- Sizes below one pixel are drawn so that the ink on screen follows the
|
|
57
|
+
real size and fades out smoothly. A point whose diameter is below
|
|
58
|
+
1 px is drawn at 1 px with its alpha multiplied by the ratio of the
|
|
59
|
+
areas, and a line or a polygon outline thinner than 1 px is drawn at
|
|
60
|
+
1 px with its alpha multiplied by the width, on the flat map and on
|
|
61
|
+
the terrain drape alike. Before, a shrinking point kept a faint blob
|
|
62
|
+
of about the antialiasing width until its size was exactly 0. This
|
|
63
|
+
applies to datasets and hand-drawn features. Nothing changes at 1 px
|
|
64
|
+
and above, and no minimum or maximum size is added
|
|
65
|
+
([#28](https://github.com/sakuzu/maplibre-gl-draw/issues/28)).
|
|
66
|
+
|
|
9
67
|
## [2.1.3] - 2026-10-04
|
|
10
68
|
|
|
11
69
|
Two fixes: the snap indicator that stayed after its input, and the
|
|
@@ -1218,7 +1276,9 @@ available from Kasika, Inc.
|
|
|
1218
1276
|
- A read-only mode and an interaction lock.
|
|
1219
1277
|
- Plugins, custom modes and custom feature types.
|
|
1220
1278
|
|
|
1221
|
-
[Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.
|
|
1279
|
+
[Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.2.0...HEAD
|
|
1280
|
+
[2.2.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.4...v2.2.0
|
|
1281
|
+
[2.1.4]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.3...v2.1.4
|
|
1222
1282
|
[2.1.3]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.2...v2.1.3
|
|
1223
1283
|
[2.1.2]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.1...v2.1.2
|
|
1224
1284
|
[2.1.1]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.0...v2.1.1
|
package/README.ja.md
CHANGED
|
@@ -99,7 +99,8 @@ GeoParquet や Arrow の表は、点、線、多角形が行ごとに混ざっ
|
|
|
99
99
|
含めて保存でき、読み込めば同じ状態に戻ります。GeoJSON で書き出せば、
|
|
100
100
|
ほかのツールと地物をやり取りできます。地物は GeoJSON の図形と
|
|
101
101
|
GeoJSON の properties を持つので、ファイルはコードで読む地物と同じ形に
|
|
102
|
-
|
|
102
|
+
なります。描画の外に持っている地物も、`featuresToGeoJSON` で同じ規則で
|
|
103
|
+
書き出せます。
|
|
103
104
|
変更は取引ごとに 1 つのイベントで、出どころを添えて届きます。文書を
|
|
104
105
|
保存するストアは差し替えられます
|
|
105
106
|
([保存と読み込み](docs/guides/save-load.ja.md))。
|
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ Write the document in the native format to keep layers, groups, styles
|
|
|
103
103
|
and images, and load it to get the same state back. Write it as GeoJSON
|
|
104
104
|
to exchange features with other tools: a feature holds a GeoJSON
|
|
105
105
|
geometry and GeoJSON properties, so the file has the shape of the
|
|
106
|
-
features you read in code.
|
|
106
|
+
features you read in code. Features kept outside a drawing are written
|
|
107
|
+
with the same rules by `featuresToGeoJSON`.
|
|
107
108
|
Every change arrives as one event per transaction, with where it came
|
|
108
109
|
from, and the store that holds the document can be replaced
|
|
109
110
|
([save and load](docs/guides/save-load.md)).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* option of `createDraw`, for example to keep the document somewhere shared.
|
|
6
6
|
*/
|
|
7
7
|
import type { DocumentChange } from '../events.js';
|
|
8
|
-
import type { Feature, FileData, Group, Layer, Metadata } from '../model.js';
|
|
8
|
+
import type { Feature, FeatureType, FileData, Group, Layer, Metadata } from '../model.js';
|
|
9
9
|
import type { Mode, Selection, SelectionType, VertexSelection } from '../state.js';
|
|
10
10
|
/**
|
|
11
11
|
* Where a write came from, as it arrives in {@link DocumentChange.source} and in the `source` of
|
|
@@ -187,4 +187,25 @@ export interface Store extends StoreView {
|
|
|
187
187
|
setInteractionLock(value: boolean): void;
|
|
188
188
|
/** Hides or shows an item in this client. */
|
|
189
189
|
setLocallyHidden(id: string, hidden: boolean): void;
|
|
190
|
+
/**
|
|
191
|
+
* The largest number of a generated name ("Point 7") the Store knows to be used for this
|
|
192
|
+
* type: a feature type, `'Layer'` or `'Group'`. Optional.
|
|
193
|
+
*
|
|
194
|
+
* The instance numbers the names of new features, layers and groups from the names it can
|
|
195
|
+
* read through `listFeatures`, `listLayers` and `listGroups`. A Store that holds only part
|
|
196
|
+
* of the document can implement this method to give the numbers used in the part it does
|
|
197
|
+
* not hold, so that they are not used again: the next number is the one after the larger
|
|
198
|
+
* of the two. Return `undefined` when the Store knows nothing more than what it lists.
|
|
199
|
+
*/
|
|
200
|
+
getMaxNameNumber?(type: FeatureType | 'Layer' | 'Group'): number | undefined;
|
|
201
|
+
/**
|
|
202
|
+
* Tells the Store the number of a name the instance has just generated for this type: a
|
|
203
|
+
* feature type, `'Layer'` or `'Group'`. Optional.
|
|
204
|
+
*
|
|
205
|
+
* It is called once per generated name, as the name is generated; the name may still not be
|
|
206
|
+
* written (a refused write leaves its number unused, as a deletion does). A Store that
|
|
207
|
+
* implements {@link Store.getMaxNameNumber} can use it to keep what that method returns up
|
|
208
|
+
* to date.
|
|
209
|
+
*/
|
|
210
|
+
recordNameNumber?(type: FeatureType | 'Layer' | 'Group', number: number): void;
|
|
190
211
|
}
|
|
@@ -8,6 +8,7 @@ export interface EmbeddedFileContent {
|
|
|
8
8
|
*
|
|
9
9
|
* @param dataURL - the image data URL found in the data
|
|
10
10
|
* @param mimeType - the MIME type declared next to it
|
|
11
|
-
* @returns the content to store, or null when it is not an accepted embedded image
|
|
11
|
+
* @returns the content to store, or null when it is not an accepted embedded image, or when
|
|
12
|
+
* it has to be scaled down and the browser returns the result in a type that is not accepted
|
|
12
13
|
*/
|
|
13
14
|
export declare function normalizeEmbeddedFile(dataURL: unknown, mimeType: unknown): Promise<EmbeddedFileContent | null>;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* texture limit is scaled down here, the same as when an image file is loaded.
|
|
10
10
|
*/
|
|
11
11
|
import { readEmbeddedImage } from '../../../shared/utils/embedded-image.js';
|
|
12
|
-
import { exceedsMaxImageSize, processImageDataUrl } from '../../../shared/utils/image.js';
|
|
12
|
+
import { exceedsMaxImageSize, processImageDataUrl, UnsupportedImageTypeError, } from '../../../shared/utils/image.js';
|
|
13
13
|
/**
|
|
14
14
|
* Validates and normalizes an embedded image
|
|
15
15
|
*
|
|
16
16
|
* @param dataURL - the image data URL found in the data
|
|
17
17
|
* @param mimeType - the MIME type declared next to it
|
|
18
|
-
* @returns the content to store, or null when it is not an accepted embedded image
|
|
18
|
+
* @returns the content to store, or null when it is not an accepted embedded image, or when
|
|
19
|
+
* it has to be scaled down and the browser returns the result in a type that is not accepted
|
|
19
20
|
*/
|
|
20
21
|
export async function normalizeEmbeddedFile(dataURL, mimeType) {
|
|
21
22
|
const info = readEmbeddedImage(dataURL);
|
|
@@ -24,7 +25,15 @@ export async function normalizeEmbeddedFile(dataURL, mimeType) {
|
|
|
24
25
|
if (mimeType !== info.mimeType)
|
|
25
26
|
return null;
|
|
26
27
|
if (exceedsMaxImageSize(info.width, info.height)) {
|
|
27
|
-
|
|
28
|
+
let processed;
|
|
29
|
+
try {
|
|
30
|
+
processed = await processImageDataUrl(dataURL);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof UnsupportedImageTypeError)
|
|
34
|
+
return null;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
28
37
|
return { mimeType: processed.mimeType, dataURL: processed.dataUrl };
|
|
29
38
|
}
|
|
30
39
|
return { mimeType: info.mimeType, dataURL };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Store } from '../../../store/store.js';
|
|
2
|
-
import type { ExportOptions, Feature } from '../../../store/types.js';
|
|
2
|
+
import type { ExportOptions, Feature, FileData } from '../../../store/types.js';
|
|
3
|
+
/** Reads an embedded file by its ID (undefined when there is no such file) */
|
|
4
|
+
export type ReadFile = (id: string) => FileData | undefined;
|
|
3
5
|
/**
|
|
4
6
|
* Converts an internal Feature into a GeoJSON Feature
|
|
5
7
|
*
|
|
@@ -10,10 +12,22 @@ import type { ExportOptions, Feature } from '../../../store/types.js';
|
|
|
10
12
|
* properties with the maplibre-gl-draw: prefix
|
|
11
13
|
* - the positions are rounded and the rings oriented (see the top of this file)
|
|
12
14
|
* - a type that is not the type of its geometry is identified by featureType
|
|
13
|
-
* - for an Image feature, the image data is embedded as Base64
|
|
15
|
+
* - for an Image feature, the image data is embedded as Base64, when `readFile` is given
|
|
16
|
+
* and finds its file
|
|
17
|
+
*
|
|
18
|
+
* @param readFile - Reads the file of an Image by its ID
|
|
14
19
|
*/
|
|
15
|
-
export declare function convertFeatureToGeoJSON(feature: Feature,
|
|
20
|
+
export declare function convertFeatureToGeoJSON(feature: Feature, readFile?: ReadFile): GeoJSON.Feature<GeoJSON.Geometry> | null;
|
|
16
21
|
/**
|
|
17
22
|
* Exports the current data as a GeoJSON FeatureCollection
|
|
18
23
|
*/
|
|
19
24
|
export declare function exportGeoJSON(store: Store, options?: ExportOptions): GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
|
25
|
+
/**
|
|
26
|
+
* Converts internal Features into a GeoJSON FeatureCollection, in the order given
|
|
27
|
+
*
|
|
28
|
+
* A feature whose geometry cannot be exported is left out. The collection carries the bbox of
|
|
29
|
+
* every exported position, unless nothing was exported.
|
|
30
|
+
*
|
|
31
|
+
* @param readFile - Reads the file of an Image by its ID
|
|
32
|
+
*/
|
|
33
|
+
export declare function convertFeaturesToGeoJSON(features: readonly Feature[], readFile?: ReadFile): GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Conversion from the internal Feature to the GeoJSON format + export of a
|
|
5
5
|
* GeoJSON FeatureCollection
|
|
6
6
|
*
|
|
7
|
+
* The conversion is pure: it reads only the features it is given and, for the image of an
|
|
8
|
+
* Image, the function that reads an embedded file. The export of the document puts the
|
|
9
|
+
* features of the Store in their order and converts them with the same functions.
|
|
10
|
+
*
|
|
7
11
|
* The output follows RFC 7946: every position is rounded to GEOJSON_COORDINATE_DECIMALS
|
|
8
12
|
* decimal places, the rings of a polygon follow the right-hand rule (the outer ring
|
|
9
13
|
* counter-clockwise, the holes clockwise) whatever orientation they were drawn in, every
|
|
@@ -107,9 +111,12 @@ function exportGeometry(geometry) {
|
|
|
107
111
|
* properties with the maplibre-gl-draw: prefix
|
|
108
112
|
* - the positions are rounded and the rings oriented (see the top of this file)
|
|
109
113
|
* - a type that is not the type of its geometry is identified by featureType
|
|
110
|
-
* - for an Image feature, the image data is embedded as Base64
|
|
114
|
+
* - for an Image feature, the image data is embedded as Base64, when `readFile` is given
|
|
115
|
+
* and finds its file
|
|
116
|
+
*
|
|
117
|
+
* @param readFile - Reads the file of an Image by its ID
|
|
111
118
|
*/
|
|
112
|
-
export function convertFeatureToGeoJSON(feature,
|
|
119
|
+
export function convertFeatureToGeoJSON(feature, readFile) {
|
|
113
120
|
const properties = {};
|
|
114
121
|
// Add the properties as they are (the values of this library already carry the prefix).
|
|
115
122
|
// They are defined as own properties so that a `__proto__` key is written out instead of
|
|
@@ -120,7 +127,9 @@ export function convertFeatureToGeoJSON(feature, store) {
|
|
|
120
127
|
// Add the metadata
|
|
121
128
|
properties[`${DRAW_PROPERTY_PREFIX}id`] = feature.id;
|
|
122
129
|
properties[`${DRAW_PROPERTY_PREFIX}layerId`] = feature.layerId;
|
|
123
|
-
|
|
130
|
+
// A feature given from outside a drawing may carry `null` for no group, which is read as
|
|
131
|
+
// undefined at the boundary
|
|
132
|
+
if (feature.groupId != null) {
|
|
124
133
|
properties[`${DRAW_PROPERTY_PREFIX}groupId`] = feature.groupId;
|
|
125
134
|
}
|
|
126
135
|
if (!feature.visible) {
|
|
@@ -130,7 +139,8 @@ export function convertFeatureToGeoJSON(feature, store) {
|
|
|
130
139
|
properties[`${DRAW_PROPERTY_PREFIX}locked`] = true;
|
|
131
140
|
}
|
|
132
141
|
if (Object.keys(feature.style).length > 0) {
|
|
133
|
-
|
|
142
|
+
// A copy, so that changing the output does not change the feature given
|
|
143
|
+
properties[`${DRAW_PROPERTY_PREFIX}style`] = structuredClone(feature.style);
|
|
134
144
|
}
|
|
135
145
|
const geometry = exportGeometry(feature.geometry);
|
|
136
146
|
if (!geometry) {
|
|
@@ -146,7 +156,7 @@ export function convertFeatureToGeoJSON(feature, store) {
|
|
|
146
156
|
// Embed the image data of an Image as Base64
|
|
147
157
|
if (feature.type === 'Image') {
|
|
148
158
|
const imageFileId = getDrawProperty(feature, 'imageFileId');
|
|
149
|
-
const fileData = imageFileId ?
|
|
159
|
+
const fileData = imageFileId && readFile ? readFile(imageFileId) : undefined;
|
|
150
160
|
if (fileData) {
|
|
151
161
|
properties[`${DRAW_PROPERTY_PREFIX}imageData`] = fileData.dataURL;
|
|
152
162
|
properties[`${DRAW_PROPERTY_PREFIX}imageMimeType`] = fileData.mimeType;
|
|
@@ -179,9 +189,20 @@ export function exportGeoJSON(store, options) {
|
|
|
179
189
|
const layerIdSet = new Set(options.layerIds);
|
|
180
190
|
features = features.filter((f) => layerIdSet.has(f.layerId));
|
|
181
191
|
}
|
|
192
|
+
return convertFeaturesToGeoJSON(features, (id) => store.getFile(id));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Converts internal Features into a GeoJSON FeatureCollection, in the order given
|
|
196
|
+
*
|
|
197
|
+
* A feature whose geometry cannot be exported is left out. The collection carries the bbox of
|
|
198
|
+
* every exported position, unless nothing was exported.
|
|
199
|
+
*
|
|
200
|
+
* @param readFile - Reads the file of an Image by its ID
|
|
201
|
+
*/
|
|
202
|
+
export function convertFeaturesToGeoJSON(features, readFile) {
|
|
182
203
|
const geoJSONFeatures = [];
|
|
183
204
|
for (const feature of features) {
|
|
184
|
-
const geoFeature = convertFeatureToGeoJSON(feature,
|
|
205
|
+
const geoFeature = convertFeatureToGeoJSON(feature, readFile);
|
|
185
206
|
if (geoFeature) {
|
|
186
207
|
geoJSONFeatures.push(geoFeature);
|
|
187
208
|
}
|
package/dist/api/impl/options.js
CHANGED
|
@@ -197,7 +197,10 @@ const RUNTIME_CHECKS = {
|
|
|
197
197
|
}),
|
|
198
198
|
isExternalEntry: func,
|
|
199
199
|
};
|
|
200
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* The members of the public Store the instance calls; `abortIntermediateUpdates`,
|
|
202
|
+
* `getMaxNameNumber` and `recordNameNumber` are optional
|
|
203
|
+
*/
|
|
201
204
|
const STORE_METHODS = [
|
|
202
205
|
'getFeature',
|
|
203
206
|
'listFeatures',
|
package/dist/api/model.d.ts
CHANGED
|
@@ -485,3 +485,37 @@ export declare function deriveLegend(rule: StyleRule, messages?: Partial<Message
|
|
|
485
485
|
* @returns `point` for points, `stroke` for lines and `fill` for areas
|
|
486
486
|
*/
|
|
487
487
|
export declare function getStyleRuleChannel(type: FeatureType): 'point' | 'stroke' | 'fill';
|
|
488
|
+
/** Options of {@link featuresToGeoJSON} and {@link featureToGeoJSON}. */
|
|
489
|
+
export interface ToGeoJSONOptions {
|
|
490
|
+
/**
|
|
491
|
+
* Reads an embedded file by its ID, for the pixels of an Image: `(id) => store.getFile(id)`,
|
|
492
|
+
* for example. An Image whose file it does not find, or every Image when it is left out, is
|
|
493
|
+
* written without `maplibre-gl-draw:imageData` and `maplibre-gl-draw:imageMimeType`.
|
|
494
|
+
*/
|
|
495
|
+
getFile?: (id: string) => FileData | undefined;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Writes features as a GeoJSON FeatureCollection, as `document.toGeoJSON` writes the
|
|
499
|
+
* document, without a drawing: the features need not be in one.
|
|
500
|
+
*
|
|
501
|
+
* The features come out in the order given (`document.toGeoJSON` gives the draw order, with
|
|
502
|
+
* the hidden features at the end). The rules of the output are those of `document.toGeoJSON`:
|
|
503
|
+
* positions rounded to 7 decimal places, rings by the right-hand rule, longitudes brought into
|
|
504
|
+
* -180 to 180, the values of the feature added to `properties` under the prefixed keys, and a
|
|
505
|
+
* `bbox` unless nothing was written. A feature whose geometry is a GeometryCollection is left
|
|
506
|
+
* out. The features given are not changed.
|
|
507
|
+
*
|
|
508
|
+
* @param features - The features, in the order to write them
|
|
509
|
+
* @param options - Where the files of the images are read from
|
|
510
|
+
* @returns The FeatureCollection
|
|
511
|
+
*/
|
|
512
|
+
export declare function featuresToGeoJSON(features: readonly Feature[], options?: ToGeoJSONOptions): GeoJSONFeatureCollection;
|
|
513
|
+
/**
|
|
514
|
+
* Writes one feature as a GeoJSON Feature, as {@link featuresToGeoJSON} writes each of its
|
|
515
|
+
* features.
|
|
516
|
+
*
|
|
517
|
+
* @param feature - The feature
|
|
518
|
+
* @param options - Where the file of an image is read from
|
|
519
|
+
* @returns The Feature, or `null` when its geometry is a GeometryCollection
|
|
520
|
+
*/
|
|
521
|
+
export declare function featureToGeoJSON(feature: Feature, options?: ToGeoJSONOptions): GeoJSONFeature | null;
|
package/dist/api/model.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
import { isDrawProperty as isLibraryKey } from '../shared/properties.js';
|
|
4
4
|
import { deriveLegend as deriveRuleLegend, evaluateStyleRule as evaluateRule, getStyleRuleChannel as getRuleChannel, } from '../view/style-rule.js';
|
|
5
|
+
import { convertFeaturesToGeoJSON, convertFeatureToGeoJSON, } from './impl/import-export/geojson-export.js';
|
|
5
6
|
export { DRAW_PROPERTY_PREFIX } from '../shared/properties.js';
|
|
6
7
|
/**
|
|
7
8
|
* Whether a key of `properties` holds a value of the library, so that an attribute panel can
|
|
@@ -45,3 +46,32 @@ export function deriveLegend(rule, messages) {
|
|
|
45
46
|
export function getStyleRuleChannel(type) {
|
|
46
47
|
return getRuleChannel(type);
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Writes features as a GeoJSON FeatureCollection, as `document.toGeoJSON` writes the
|
|
51
|
+
* document, without a drawing: the features need not be in one.
|
|
52
|
+
*
|
|
53
|
+
* The features come out in the order given (`document.toGeoJSON` gives the draw order, with
|
|
54
|
+
* the hidden features at the end). The rules of the output are those of `document.toGeoJSON`:
|
|
55
|
+
* positions rounded to 7 decimal places, rings by the right-hand rule, longitudes brought into
|
|
56
|
+
* -180 to 180, the values of the feature added to `properties` under the prefixed keys, and a
|
|
57
|
+
* `bbox` unless nothing was written. A feature whose geometry is a GeometryCollection is left
|
|
58
|
+
* out. The features given are not changed.
|
|
59
|
+
*
|
|
60
|
+
* @param features - The features, in the order to write them
|
|
61
|
+
* @param options - Where the files of the images are read from
|
|
62
|
+
* @returns The FeatureCollection
|
|
63
|
+
*/
|
|
64
|
+
export function featuresToGeoJSON(features, options) {
|
|
65
|
+
return convertFeaturesToGeoJSON(features, options?.getFile);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Writes one feature as a GeoJSON Feature, as {@link featuresToGeoJSON} writes each of its
|
|
69
|
+
* features.
|
|
70
|
+
*
|
|
71
|
+
* @param feature - The feature
|
|
72
|
+
* @param options - Where the file of an image is read from
|
|
73
|
+
* @returns The Feature, or `null` when its geometry is a GeometryCollection
|
|
74
|
+
*/
|
|
75
|
+
export function featureToGeoJSON(feature, options) {
|
|
76
|
+
return convertFeatureToGeoJSON(feature, options?.getFile);
|
|
77
|
+
}
|
|
@@ -313,11 +313,10 @@ export class DisplayChunkSet {
|
|
|
313
313
|
}
|
|
314
314
|
drawChunkBatches(state.batches, renderers, zoom, projectionData, factors);
|
|
315
315
|
// The features that cannot be retained are drawn immediately, right after the retained
|
|
316
|
-
// batches of the chunk, the points last (the
|
|
317
|
-
// factors)
|
|
316
|
+
// batches of the chunk, the points last (with the same factors as the retained batches)
|
|
318
317
|
const { fallback, pointFallback } = state.batches;
|
|
319
318
|
if (fallback.length > 0 || pointFallback.length > 0) {
|
|
320
|
-
target.beginFrame(projectionData, zoom);
|
|
319
|
+
target.beginFrame(projectionData, zoom, undefined, factors);
|
|
321
320
|
for (const feature of fallback)
|
|
322
321
|
target.processFeature(feature, false);
|
|
323
322
|
for (const feature of pointFallback)
|
package/dist/dataset/dataset.js
CHANGED
|
@@ -608,12 +608,13 @@ export class DatasetImpl {
|
|
|
608
608
|
this.chunks.draw({ target, renderers, terrain, projectionData, zoom, bounds, factors });
|
|
609
609
|
}
|
|
610
610
|
else {
|
|
611
|
-
this.drawImmediate(target, projectionData, zoom, bounds);
|
|
611
|
+
this.drawImmediate(target, projectionData, zoom, bounds, factors);
|
|
612
612
|
}
|
|
613
613
|
drawSelectionHighlight({
|
|
614
614
|
target,
|
|
615
615
|
projectionData,
|
|
616
616
|
zoom,
|
|
617
|
+
factors,
|
|
617
618
|
bounds,
|
|
618
619
|
selected: this.selected,
|
|
619
620
|
styles: this.chunks.retainedRenderers?.styles,
|
|
@@ -710,9 +711,10 @@ export class DatasetImpl {
|
|
|
710
711
|
* Draws in immediate mode (the path for an environment without the retained-mode renderers)
|
|
711
712
|
*
|
|
712
713
|
* The frame of the batch is started without passing a layer. The rules have already been
|
|
713
|
-
* evaluated here, so the rule evaluation of the rendering side (FeatureDrawer) is not used.
|
|
714
|
+
* evaluated here, so the rule evaluation of the rendering side (FeatureDrawer) is not used. The
|
|
715
|
+
* factors of the frame are passed, the same as to the retained batches.
|
|
714
716
|
*/
|
|
715
|
-
drawImmediate(target, projectionData, zoom, bounds) {
|
|
717
|
+
drawImmediate(target, projectionData, zoom, bounds, factors) {
|
|
716
718
|
const rows = this.source.index.search(bounds).filter((row) => this.thinning.isDrawnRow(row));
|
|
717
719
|
let visible = this.styler.prepareAll(this.featuresOf(rows));
|
|
718
720
|
// A point drawn by an external renderer is not drawn by core (without a predicate it passes
|
|
@@ -722,7 +724,7 @@ export class DatasetImpl {
|
|
|
722
724
|
}
|
|
723
725
|
if (visible.length === 0)
|
|
724
726
|
return;
|
|
725
|
-
target.beginFrame(projectionData, zoom);
|
|
727
|
+
target.beginFrame(projectionData, zoom, undefined, factors);
|
|
726
728
|
for (const feature of visible) {
|
|
727
729
|
target.processFeature(feature, false);
|
|
728
730
|
}
|
package/dist/dataset/retained.js
CHANGED
|
@@ -812,7 +812,7 @@ function collectPoint(feature, coord, draft, styles, isExternallyRenderedPoint)
|
|
|
812
812
|
const style = styles.getPointStyle(feature);
|
|
813
813
|
const shape = toInstancedPointShape(style.shape);
|
|
814
814
|
if (!shape) {
|
|
815
|
-
// The
|
|
815
|
+
// The immediate-mode path gets the same factors of zoomScale (DisplayBatchTarget.beginFrame)
|
|
816
816
|
pushFallback(draft, feature);
|
|
817
817
|
return;
|
|
818
818
|
}
|
|
@@ -836,7 +836,7 @@ function collectLine(feature, coords, draft, styles, skipDraped = false) {
|
|
|
836
836
|
if (skipDraped && strokeStyle.opacity > 0 && strokeStyle.width > 0)
|
|
837
837
|
return;
|
|
838
838
|
if (strokeStyle.lineStyle !== 'solid') {
|
|
839
|
-
// The
|
|
839
|
+
// The immediate-mode path gets the same factors of zoomScale (DisplayBatchTarget.beginFrame)
|
|
840
840
|
pushFallback(draft, feature);
|
|
841
841
|
return;
|
|
842
842
|
}
|
|
@@ -870,7 +870,7 @@ function collectPolygon(feature, rings, draft, styles, partIndex = 0, skipDraped
|
|
|
870
870
|
if (skipDraped)
|
|
871
871
|
return;
|
|
872
872
|
if (hasStroke && strokeStyle.lineStyle !== 'solid') {
|
|
873
|
-
// The
|
|
873
|
+
// The immediate-mode path gets the same factors of zoomScale (DisplayBatchTarget.beginFrame)
|
|
874
874
|
pushFallback(draft, feature);
|
|
875
875
|
return;
|
|
876
876
|
}
|
|
@@ -39,7 +39,7 @@ export function drawSelectionHighlight(input) {
|
|
|
39
39
|
const visible = input.selected.filter((feature) => feature.visible && boundsIntersect(getBoundingBox(feature), bounds));
|
|
40
40
|
if (visible.length === 0)
|
|
41
41
|
return;
|
|
42
|
-
target.beginFrame(projectionData, zoom);
|
|
42
|
+
target.beginFrame(projectionData, zoom, undefined, input.factors);
|
|
43
43
|
for (const feature of visible) {
|
|
44
44
|
const prepared = input.prepare(feature);
|
|
45
45
|
// The hand-over decision (drapedFills) switches a few frames late, so looking only at it
|
package/dist/index.d.ts
CHANGED
|
@@ -70,3 +70,5 @@ export type { CompanionProvider, HandleProvider, Hit, SnapCandidate, SnapProvide
|
|
|
70
70
|
export type { FeatureRenderer, FillRenderer, LineRenderer, OffsetUniforms, OverlayRenderer, PointRenderer, RenderContext, ShaderData, } from './api/extension/render.js';
|
|
71
71
|
export type { ExtensionsCollections, FeatureTypesCollection, ModesCollection, OverlaysCollection, PluginsCollection, ProvidersCollection, } from './api/extensions.js';
|
|
72
72
|
export { deriveLegend, evaluateStyleRule, getStyleRuleChannel } from './api/model.js';
|
|
73
|
+
export type { ToGeoJSONOptions } from './api/model.js';
|
|
74
|
+
export { featuresToGeoJSON, featureToGeoJSON } from './api/model.js';
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image processing utilities
|
|
3
|
-
*
|
|
4
|
-
* Converts an image file into the WebP format, resizing it when necessary.
|
|
5
|
-
*/
|
|
6
1
|
export interface ProcessedImage {
|
|
7
2
|
dataUrl: string;
|
|
8
3
|
width: number;
|
|
9
4
|
height: number;
|
|
10
5
|
mimeType: string;
|
|
11
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The error thrown when the canvas returns an image of a type the library does not accept
|
|
9
|
+
*
|
|
10
|
+
* A browser that cannot encode the requested type returns another one (usually PNG); one that
|
|
11
|
+
* is not PNG / JPEG / WebP / GIF cannot be stored as an embedded image.
|
|
12
|
+
*/
|
|
13
|
+
export declare class UnsupportedImageTypeError extends Error {
|
|
14
|
+
constructor(mimeType: string);
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Maximum size of an image
|
|
14
18
|
* A value that accounts for the WebGL texture limit (modern GPUs support 4096 or more)
|
|
15
19
|
*/
|
|
16
20
|
export declare const MAX_IMAGE_SIZE = 4096;
|
|
17
21
|
/**
|
|
18
|
-
* Converts a file into a DataURL in the WebP format
|
|
22
|
+
* Converts a file into a DataURL in the WebP format. When the browser cannot encode WebP, an image
|
|
23
|
+
* with no transparent pixel is encoded as JPEG, and one with a transparent pixel keeps the format
|
|
24
|
+
* the browser returns (such as PNG)
|
|
19
25
|
* A large image is resized automatically
|
|
26
|
+
*
|
|
27
|
+
* @throws UnsupportedImageTypeError when the browser returns a type other than PNG / JPEG /
|
|
28
|
+
* WebP / GIF
|
|
20
29
|
*/
|
|
21
30
|
export declare function processImageFile(file: File): Promise<ProcessedImage>;
|
|
22
31
|
/**
|
|
@@ -24,7 +33,12 @@ export declare function processImageFile(file: File): Promise<ProcessedImage>;
|
|
|
24
33
|
*/
|
|
25
34
|
export declare function exceedsMaxImageSize(width: number, height: number): boolean;
|
|
26
35
|
/**
|
|
27
|
-
* Converts an image DataURL into a DataURL in the WebP format
|
|
36
|
+
* Converts an image DataURL into a DataURL in the WebP format. When the browser cannot encode
|
|
37
|
+
* WebP, an image with no transparent pixel is encoded as JPEG, and one with a transparent pixel
|
|
38
|
+
* keeps the format the browser returns (such as PNG)
|
|
28
39
|
* A large image is resized automatically
|
|
40
|
+
*
|
|
41
|
+
* @throws UnsupportedImageTypeError when the browser returns a type other than PNG / JPEG /
|
|
42
|
+
* WebP / GIF
|
|
29
43
|
*/
|
|
30
44
|
export declare function processImageDataUrl(originalDataUrl: string): Promise<ProcessedImage>;
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
/**
|
|
4
|
+
* Image processing utilities
|
|
5
|
+
*
|
|
6
|
+
* Converts an image file into the WebP format, resizing it when necessary. A browser that cannot
|
|
7
|
+
* encode WebP from a canvas returns another format (such as PNG). The image is then encoded as JPEG
|
|
8
|
+
* when none of its pixels is transparent, and otherwise keeps the format the browser returned.
|
|
9
|
+
*/
|
|
10
|
+
import { EMBEDDED_IMAGE_MIME_TYPES } from './embedded-image.js';
|
|
3
11
|
const WEBP_QUALITY = 0.95;
|
|
12
|
+
/** The quality of the JPEG an opaque image is encoded as when the browser cannot encode WebP */
|
|
13
|
+
const JPEG_QUALITY = 0.92;
|
|
14
|
+
/**
|
|
15
|
+
* The error thrown when the canvas returns an image of a type the library does not accept
|
|
16
|
+
*
|
|
17
|
+
* A browser that cannot encode the requested type returns another one (usually PNG); one that
|
|
18
|
+
* is not PNG / JPEG / WebP / GIF cannot be stored as an embedded image.
|
|
19
|
+
*/
|
|
20
|
+
export class UnsupportedImageTypeError extends Error {
|
|
21
|
+
constructor(mimeType) {
|
|
22
|
+
super(`The browser returned an image of an unsupported type: ${mimeType || '(none)'}`);
|
|
23
|
+
this.name = 'UnsupportedImageTypeError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** The type in the `data:<type>;` or `data:<type>,` prefix of a data URL */
|
|
27
|
+
const DATA_URL_TYPE = /^data:([^;,]*)[;,]/;
|
|
28
|
+
/**
|
|
29
|
+
* The type of the image a data URL holds, read from its prefix
|
|
30
|
+
*
|
|
31
|
+
* @throws UnsupportedImageTypeError when the type is not PNG / JPEG / WebP / GIF
|
|
32
|
+
*/
|
|
33
|
+
function acceptedImageType(dataUrl) {
|
|
34
|
+
const mimeType = DATA_URL_TYPE.exec(dataUrl)?.[1] ?? '';
|
|
35
|
+
if (!EMBEDDED_IMAGE_MIME_TYPES.includes(mimeType)) {
|
|
36
|
+
throw new UnsupportedImageTypeError(mimeType);
|
|
37
|
+
}
|
|
38
|
+
return mimeType;
|
|
39
|
+
}
|
|
4
40
|
/**
|
|
5
41
|
* Maximum size of an image
|
|
6
42
|
* A value that accounts for the WebGL texture limit (modern GPUs support 4096 or more)
|
|
@@ -27,8 +63,13 @@ function calculateResizedDimensions(width, height, maxSize) {
|
|
|
27
63
|
return { width: newWidth, height: newHeight, needsResize: true };
|
|
28
64
|
}
|
|
29
65
|
/**
|
|
30
|
-
* Converts a file into a DataURL in the WebP format
|
|
66
|
+
* Converts a file into a DataURL in the WebP format. When the browser cannot encode WebP, an image
|
|
67
|
+
* with no transparent pixel is encoded as JPEG, and one with a transparent pixel keeps the format
|
|
68
|
+
* the browser returns (such as PNG)
|
|
31
69
|
* A large image is resized automatically
|
|
70
|
+
*
|
|
71
|
+
* @throws UnsupportedImageTypeError when the browser returns a type other than PNG / JPEG /
|
|
72
|
+
* WebP / GIF
|
|
32
73
|
*/
|
|
33
74
|
export async function processImageFile(file) {
|
|
34
75
|
return processImageDataUrl(await fileToDataUrl(file));
|
|
@@ -40,8 +81,13 @@ export function exceedsMaxImageSize(width, height) {
|
|
|
40
81
|
return width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE;
|
|
41
82
|
}
|
|
42
83
|
/**
|
|
43
|
-
* Converts an image DataURL into a DataURL in the WebP format
|
|
84
|
+
* Converts an image DataURL into a DataURL in the WebP format. When the browser cannot encode
|
|
85
|
+
* WebP, an image with no transparent pixel is encoded as JPEG, and one with a transparent pixel
|
|
86
|
+
* keeps the format the browser returns (such as PNG)
|
|
44
87
|
* A large image is resized automatically
|
|
88
|
+
*
|
|
89
|
+
* @throws UnsupportedImageTypeError when the browser returns a type other than PNG / JPEG /
|
|
90
|
+
* WebP / GIF
|
|
45
91
|
*/
|
|
46
92
|
export async function processImageDataUrl(originalDataUrl) {
|
|
47
93
|
// 1. Load it as an Image (to get the dimensions)
|
|
@@ -60,15 +106,36 @@ export async function processImageDataUrl(originalDataUrl) {
|
|
|
60
106
|
ctx.imageSmoothingEnabled = true;
|
|
61
107
|
ctx.imageSmoothingQuality = 'high';
|
|
62
108
|
ctx.drawImage(img, 0, 0, width, height);
|
|
63
|
-
// 4. Convert into WebP
|
|
64
|
-
|
|
109
|
+
// 4. Convert into WebP. A browser that cannot encode WebP returns another type, so the type
|
|
110
|
+
// is read from the data URL it returned rather than taken from the request
|
|
111
|
+
let dataUrl = canvas.toDataURL('image/webp', WEBP_QUALITY);
|
|
112
|
+
let mimeType = acceptedImageType(dataUrl);
|
|
113
|
+
// 5. When the browser cannot encode WebP, an opaque image is encoded as JPEG, which is far
|
|
114
|
+
// smaller than PNG for a photo. An image with a transparent pixel keeps the returned type,
|
|
115
|
+
// because JPEG would lose the transparency
|
|
116
|
+
if (mimeType !== 'image/webp' && !hasTransparentPixel(ctx, width, height)) {
|
|
117
|
+
dataUrl = canvas.toDataURL('image/jpeg', JPEG_QUALITY);
|
|
118
|
+
mimeType = acceptedImageType(dataUrl);
|
|
119
|
+
}
|
|
65
120
|
return {
|
|
66
|
-
dataUrl
|
|
121
|
+
dataUrl,
|
|
67
122
|
width,
|
|
68
123
|
height,
|
|
69
|
-
mimeType
|
|
124
|
+
mimeType,
|
|
70
125
|
};
|
|
71
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether any pixel drawn on the canvas is not fully opaque (alpha below 255)
|
|
129
|
+
* Stops at the first such pixel
|
|
130
|
+
*/
|
|
131
|
+
function hasTransparentPixel(ctx, width, height) {
|
|
132
|
+
const { data } = ctx.getImageData(0, 0, width, height);
|
|
133
|
+
for (let i = 3; i < data.length; i += 4) {
|
|
134
|
+
if (data[i] < 255)
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
72
139
|
/**
|
|
73
140
|
* Converts a File into a DataURL
|
|
74
141
|
*/
|
|
@@ -75,6 +75,11 @@ export declare function normalizeAutoNameConfig(config: AutoNameConfig | boolean
|
|
|
75
75
|
* any generation happened is also recorded (with the old approach it could be reused after the
|
|
76
76
|
* deletion). This is an extension of the "a number used once is not reused" policy, and it can
|
|
77
77
|
* only work in the direction of increasing the gaps.
|
|
78
|
+
*
|
|
79
|
+
* A Store that holds only part of the document can give, per type, the largest number it
|
|
80
|
+
* knows to be used (`getMaxNameNumber`): each generation then takes the number after the
|
|
81
|
+
* larger of that and the count above, and tells the Store the number it took
|
|
82
|
+
* (`recordNameNumber`). With a Store that has neither, the numbering is the count above alone.
|
|
78
83
|
*/
|
|
79
84
|
export declare class AutoNameGenerator {
|
|
80
85
|
private readonly store;
|
|
@@ -90,6 +95,8 @@ export declare class AutoNameGenerator {
|
|
|
90
95
|
listLayers(): Layer[];
|
|
91
96
|
listGroups(): Group[];
|
|
92
97
|
subscribe(listener: (changes: StoreChange) => void): () => void;
|
|
98
|
+
getMaxNameNumber?(type: AutoNameType): number | undefined;
|
|
99
|
+
recordNameNumber?(type: AutoNameType, number: number): void;
|
|
93
100
|
}, config?: AutoNameConfig | boolean);
|
|
94
101
|
/**
|
|
95
102
|
* Returns whether it is enabled
|
|
@@ -123,6 +130,11 @@ export declare class AutoNameGenerator {
|
|
|
123
130
|
* not reused and a gap appears.
|
|
124
131
|
*/
|
|
125
132
|
private findNextNumber;
|
|
133
|
+
/**
|
|
134
|
+
* Takes the next number of a counter key: the one after the larger of the counter and the
|
|
135
|
+
* largest number the Store knows (when it gives one), and tells the Store the number taken
|
|
136
|
+
*/
|
|
137
|
+
private takeNextNumber;
|
|
126
138
|
/**
|
|
127
139
|
* Takes in the largest existing number only the first time a generation happens for that
|
|
128
140
|
* counter key
|
|
@@ -66,6 +66,11 @@ export function normalizeAutoNameConfig(config) {
|
|
|
66
66
|
* any generation happened is also recorded (with the old approach it could be reused after the
|
|
67
67
|
* deletion). This is an extension of the "a number used once is not reused" policy, and it can
|
|
68
68
|
* only work in the direction of increasing the gaps.
|
|
69
|
+
*
|
|
70
|
+
* A Store that holds only part of the document can give, per type, the largest number it
|
|
71
|
+
* knows to be used (`getMaxNameNumber`): each generation then takes the number after the
|
|
72
|
+
* larger of that and the count above, and tells the Store the number it took
|
|
73
|
+
* (`recordNameNumber`). With a Store that has neither, the numbering is the count above alone.
|
|
69
74
|
*/
|
|
70
75
|
export class AutoNameGenerator {
|
|
71
76
|
constructor(store, config = true) {
|
|
@@ -175,8 +180,20 @@ export class AutoNameGenerator {
|
|
|
175
180
|
*/
|
|
176
181
|
findNextNumber(featureType, typeName) {
|
|
177
182
|
this.scanOnce(featureType, () => this.getMaxNumberFromFeatures(featureType, typeName));
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
return this.takeNextNumber(featureType);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Takes the next number of a counter key: the one after the larger of the counter and the
|
|
187
|
+
* largest number the Store knows (when it gives one), and tells the Store the number taken
|
|
188
|
+
*/
|
|
189
|
+
takeNextNumber(counterKey) {
|
|
190
|
+
const known = this.store.getMaxNameNumber?.(counterKey);
|
|
191
|
+
if (known !== undefined && Number.isSafeInteger(known)) {
|
|
192
|
+
this.recordNumber(counterKey, known);
|
|
193
|
+
}
|
|
194
|
+
const nextNumber = (this.counters.get(counterKey) ?? 0) + 1;
|
|
195
|
+
this.counters.set(counterKey, nextNumber);
|
|
196
|
+
this.store.recordNameNumber?.(counterKey, nextNumber);
|
|
180
197
|
return nextNumber;
|
|
181
198
|
}
|
|
182
199
|
/**
|
|
@@ -312,9 +329,7 @@ export class AutoNameGenerator {
|
|
|
312
329
|
}
|
|
313
330
|
return maxNumber;
|
|
314
331
|
});
|
|
315
|
-
|
|
316
|
-
this.counters.set(LAYER_COUNTER_KEY, nextNumber);
|
|
317
|
-
return nextNumber;
|
|
332
|
+
return this.takeNextNumber(LAYER_COUNTER_KEY);
|
|
318
333
|
}
|
|
319
334
|
/**
|
|
320
335
|
* Generates the name of a new Group
|
|
@@ -347,8 +362,6 @@ export class AutoNameGenerator {
|
|
|
347
362
|
}
|
|
348
363
|
return maxNumber;
|
|
349
364
|
});
|
|
350
|
-
|
|
351
|
-
this.counters.set(GROUP_COUNTER_KEY, nextNumber);
|
|
352
|
-
return nextNumber;
|
|
365
|
+
return this.takeNextNumber(GROUP_COUNTER_KEY);
|
|
353
366
|
}
|
|
354
367
|
}
|
package/dist/store/draw-store.js
CHANGED
|
@@ -162,6 +162,15 @@ export class DrawStore {
|
|
|
162
162
|
return __classPrivateFieldGet(this, _DrawStore_instances, "m", _DrawStore_write).call(this, () => __classPrivateFieldGet(this, _DrawStore_contract, "f").setMetadata(metadata));
|
|
163
163
|
}
|
|
164
164
|
// ============================================================================
|
|
165
|
+
// The numbers of generated names (optional members of the contract)
|
|
166
|
+
// ============================================================================
|
|
167
|
+
getMaxNameNumber(type) {
|
|
168
|
+
return __classPrivateFieldGet(this, _DrawStore_contract, "f").getMaxNameNumber?.(type);
|
|
169
|
+
}
|
|
170
|
+
recordNameNumber(type, number) {
|
|
171
|
+
__classPrivateFieldGet(this, _DrawStore_contract, "f").recordNameNumber?.(type, number);
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
165
174
|
// The state of this client (kept by the contract)
|
|
166
175
|
// ============================================================================
|
|
167
176
|
getSelection() {
|
package/dist/store/store.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* DocumentStore is the document half the in-memory contract store is built on.
|
|
20
20
|
*/
|
|
21
21
|
import type { Store as PublicStore } from '../api/extension/store.js';
|
|
22
|
-
import type { BoxSelection, DragState, Feature, FileData, Group, Layer, Metadata, Mode, Selection, SelectionType, StoreChange, TentativeState, UpdateFeatureOptions, UpdateSource, VertexSelection } from './types.js';
|
|
22
|
+
import type { BoxSelection, DragState, Feature, FeatureType, FileData, Group, Layer, Metadata, Mode, Selection, SelectionType, StoreChange, TentativeState, UpdateFeatureOptions, UpdateSource, VertexSelection } from './types.js';
|
|
23
23
|
/**
|
|
24
24
|
* The Store a host gives with the `store` option: the public `Store`, with the rules core
|
|
25
25
|
* relies on
|
|
@@ -373,4 +373,11 @@ export interface Store extends StoreView, UiState {
|
|
|
373
373
|
deleteFile(id: string): boolean;
|
|
374
374
|
/** Sets the metadata, merging (see {@link DocumentStore.setMetadata}) */
|
|
375
375
|
setMetadata(metadata: Partial<Metadata>): boolean;
|
|
376
|
+
/**
|
|
377
|
+
* The largest number of a generated name the Store knows to be used for this type, beyond
|
|
378
|
+
* what it lists, or undefined (see the public `Store.getMaxNameNumber`)
|
|
379
|
+
*/
|
|
380
|
+
getMaxNameNumber?(type: FeatureType | 'Layer' | 'Group'): number | undefined;
|
|
381
|
+
/** Tells the Store the number of a name just generated (see the public `Store.recordNameNumber`) */
|
|
382
|
+
recordNameNumber?(type: FeatureType | 'Layer' | 'Group', number: number): void;
|
|
376
383
|
}
|
|
@@ -6,7 +6,7 @@ import { getCircleRadius, getCreatedZoom } from '../../shared/utils/property.js'
|
|
|
6
6
|
import { densifyPathForGlobe } from '../globe-subdivision.js';
|
|
7
7
|
import { TerrainContext } from '../terrain/context.js';
|
|
8
8
|
import { anchorGhostOpacity } from '../terrain/occlusion.js';
|
|
9
|
-
import { layerDrawFactors, NEUTRAL_DRAW_FACTORS, } from './draw-factors.js';
|
|
9
|
+
import { combineDrawFactors, layerDrawFactors, NEUTRAL_DRAW_FACTORS, } from './draw-factors.js';
|
|
10
10
|
import { getStrokeDashPattern, splitIntoDashes } from './line/dash.js';
|
|
11
11
|
import { toInstancedPointShape, } from './point/point-instance.js';
|
|
12
12
|
/**
|
|
@@ -310,12 +310,15 @@ export class BatchManager {
|
|
|
310
310
|
*
|
|
311
311
|
* @param layer The layer to draw (used to evaluate the style rules, and its opacity is
|
|
312
312
|
* multiplied into everything the frame draws; no factor when omitted)
|
|
313
|
+
* @param factors The draw factors of the frame itself (a dataset passes those of its
|
|
314
|
+
* `zoomScale`). They are combined with the factors of the layer and multiplied into
|
|
315
|
+
* everything the frame draws, the same as on the retained path; no factor when omitted
|
|
313
316
|
*/
|
|
314
|
-
beginFrame(projectionData, zoom, layer) {
|
|
317
|
+
beginFrame(projectionData, zoom, layer, factors = NEUTRAL_DRAW_FACTORS) {
|
|
315
318
|
this.projectionData = projectionData;
|
|
316
319
|
this.zoom = zoom;
|
|
317
320
|
this.layer = layer;
|
|
318
|
-
this.factors = layerDrawFactors(layer);
|
|
321
|
+
this.factors = combineDrawFactors(layerDrawFactors(layer), factors);
|
|
319
322
|
this.currentBatchType = null;
|
|
320
323
|
}
|
|
321
324
|
/**
|
|
@@ -408,7 +411,7 @@ export class BatchManager {
|
|
|
408
411
|
}
|
|
409
412
|
// Shapes that do not support instancing are drawn individually, one point at a time
|
|
410
413
|
// (MultiPoint comes into this function part by part, so they are drawn per point)
|
|
411
|
-
this.featureDrawer.drawPointShape(coord, this.
|
|
414
|
+
this.featureDrawer.drawPointShape(coord, this.withFrameFactors(style), this.zoom, feature.id);
|
|
412
415
|
}
|
|
413
416
|
else {
|
|
414
417
|
// Color, size and stroke are instance attributes, so the batch key only needs the shape
|
|
@@ -474,8 +477,9 @@ export class BatchManager {
|
|
|
474
477
|
// the look does not change).
|
|
475
478
|
const fixedWidth = getCreatedZoom(feature) === undefined;
|
|
476
479
|
const createdZoom = getCreatedZoom(feature) ?? this.zoom;
|
|
477
|
-
// Compute the effective line width at the current zoom level
|
|
478
|
-
|
|
480
|
+
// Compute the effective line width at the current zoom level (with the size factor of the
|
|
481
|
+
// frame, which the shader multiplies into the width, so the dashes follow the drawn width)
|
|
482
|
+
const effectiveStrokeWidth = strokeStyle.width * 2 ** (this.zoom - createdZoom) * this.factors.scale;
|
|
479
483
|
// For dashed/dotted lines the dashes are computed on the CPU side
|
|
480
484
|
const dashPattern = getStrokeDashPattern(strokeStyle.lineStyle, effectiveStrokeWidth);
|
|
481
485
|
if (dashPattern) {
|
|
@@ -588,7 +592,8 @@ export class BatchManager {
|
|
|
588
592
|
// convention as withFixedWidth; collectPolygons in retained mode does the same).
|
|
589
593
|
const fixedWidth = getCreatedZoom(feature) === undefined;
|
|
590
594
|
const createdZoom = getCreatedZoom(feature) ?? this.zoom;
|
|
591
|
-
|
|
595
|
+
// The drawn width of the outline (with the size factor of the frame; the dashes follow it)
|
|
596
|
+
const effectiveStrokeWidth = strokeStyle.width * 2 ** (this.zoom - createdZoom) * this.factors.scale;
|
|
592
597
|
const isDashed = strokeStyle.lineStyle !== 'solid';
|
|
593
598
|
if (strokeStyle.opacity > 0 && !isDashed) {
|
|
594
599
|
// For a solid line: drawn all at once by SDFPolygonRenderer
|
|
@@ -618,7 +623,7 @@ export class BatchManager {
|
|
|
618
623
|
if (fillColor[3] > 0) {
|
|
619
624
|
this.polygonFillBatch.push({
|
|
620
625
|
coordinates: rings,
|
|
621
|
-
color: this.
|
|
626
|
+
color: this.colorWithFrameOpacity(fillColor),
|
|
622
627
|
featureId: feature.id,
|
|
623
628
|
partIndex,
|
|
624
629
|
});
|
|
@@ -634,7 +639,7 @@ export class BatchManager {
|
|
|
634
639
|
if (fillColor[3] > 0) {
|
|
635
640
|
this.polygonFillBatch.push({
|
|
636
641
|
coordinates: rings,
|
|
637
|
-
color: this.
|
|
642
|
+
color: this.colorWithFrameOpacity(fillColor),
|
|
638
643
|
featureId,
|
|
639
644
|
partIndex,
|
|
640
645
|
});
|
|
@@ -678,23 +683,26 @@ export class BatchManager {
|
|
|
678
683
|
}
|
|
679
684
|
}
|
|
680
685
|
/**
|
|
681
|
-
* A color with the opacity of the
|
|
682
|
-
* batch, whose colors are per vertex and which has no factor uniform)
|
|
686
|
+
* A color with the opacity factor of the frame multiplied into its alpha (for the fill-only
|
|
687
|
+
* polygon batch, whose colors are per vertex and which has no factor uniform)
|
|
683
688
|
*/
|
|
684
|
-
|
|
689
|
+
colorWithFrameOpacity(color) {
|
|
685
690
|
const opacity = this.factors.opacity;
|
|
686
691
|
return opacity === 1 ? color : [color[0], color[1], color[2], color[3] * opacity];
|
|
687
692
|
}
|
|
688
693
|
/**
|
|
689
|
-
* A point style with the
|
|
690
|
-
* has no factor uniform)
|
|
694
|
+
* A point style with the factors of the frame multiplied in (for the per-point renderer, which
|
|
695
|
+
* has no factor uniform): the size and the stroke width by the size factor, the opacities by
|
|
696
|
+
* the opacity factor
|
|
691
697
|
*/
|
|
692
|
-
|
|
693
|
-
const opacity = this.factors
|
|
694
|
-
if (opacity === 1)
|
|
698
|
+
withFrameFactors(style) {
|
|
699
|
+
const { scale, opacity } = this.factors;
|
|
700
|
+
if (scale === 1 && opacity === 1)
|
|
695
701
|
return style;
|
|
696
702
|
return {
|
|
697
703
|
...style,
|
|
704
|
+
size: style.size * scale,
|
|
705
|
+
strokeWidth: style.strokeWidth * scale,
|
|
698
706
|
fillOpacity: style.fillOpacity * opacity,
|
|
699
707
|
strokeOpacity: style.strokeOpacity * opacity,
|
|
700
708
|
};
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* Conventions:
|
|
11
11
|
*
|
|
12
12
|
* - The default is `NEUTRAL_DRAW_FACTORS` (1x scale, opacity 1), so calls that
|
|
13
|
-
* pass no factors (Store rendering,
|
|
13
|
+
* pass no factors (the Store rendering, for example) look exactly as before,
|
|
14
14
|
* down to the pixel
|
|
15
|
+
* - The immediate-mode frame of a dataset takes its factors through
|
|
16
|
+
* `BatchManager.beginFrame`, which combines them with the factors of the layer
|
|
17
|
+
* (`combineDrawFactors`)
|
|
15
18
|
* - A uniform is per-program residual state, so even rendering paths that do
|
|
16
19
|
* not use the factors must write 1 (a value left over from a previous draw
|
|
17
20
|
* makes another draw come out thinner; this is the same family of accident as
|
|
@@ -58,3 +61,10 @@ export declare function sanitizeDrawFactors(factors: {
|
|
|
58
61
|
export declare function layerDrawFactors(layer: {
|
|
59
62
|
readonly opacity?: number;
|
|
60
63
|
} | null | undefined): RetainedDrawFactors;
|
|
64
|
+
/**
|
|
65
|
+
* The product of two sets of factors (the frame's own factors over those of the layer)
|
|
66
|
+
*
|
|
67
|
+
* Both are taken as already rounded (`sanitizeDrawFactors`, `layerDrawFactors`). Neutral
|
|
68
|
+
* factors on either side give the other set itself.
|
|
69
|
+
*/
|
|
70
|
+
export declare function combineDrawFactors(a: RetainedDrawFactors, b: RetainedDrawFactors): RetainedDrawFactors;
|
|
@@ -33,3 +33,16 @@ export function layerDrawFactors(layer) {
|
|
|
33
33
|
return NEUTRAL_DRAW_FACTORS;
|
|
34
34
|
return sanitizeDrawFactors({ scale: 1, opacity });
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The product of two sets of factors (the frame's own factors over those of the layer)
|
|
38
|
+
*
|
|
39
|
+
* Both are taken as already rounded (`sanitizeDrawFactors`, `layerDrawFactors`). Neutral
|
|
40
|
+
* factors on either side give the other set itself.
|
|
41
|
+
*/
|
|
42
|
+
export function combineDrawFactors(a, b) {
|
|
43
|
+
if (b === NEUTRAL_DRAW_FACTORS || (b.scale === 1 && b.opacity === 1))
|
|
44
|
+
return a;
|
|
45
|
+
if (a === NEUTRAL_DRAW_FACTORS || (a.scale === 1 && a.opacity === 1))
|
|
46
|
+
return b;
|
|
47
|
+
return { scale: a.scale * b.scale, opacity: a.opacity * b.opacity };
|
|
48
|
+
}
|
|
@@ -254,6 +254,11 @@ void main() {
|
|
|
254
254
|
float lineWidth = (strokeWidth > 0.0
|
|
255
255
|
? strokeWidth * pow(2.0, u_zoom - createdZoom)
|
|
256
256
|
: (strokeWidth < 0.0 ? -strokeWidth : u_width)) * u_size_scale;
|
|
257
|
+
// Below one pixel the line is drawn one pixel wide and fainter by its width, so the ink per
|
|
258
|
+
// unit of length follows the width and goes to nothing with it (the caps of every dash
|
|
259
|
+
// included, which would otherwise keep a blob of a fixed size)
|
|
260
|
+
float ink = clamp(lineWidth, 0.0, 1.0);
|
|
261
|
+
lineWidth = max(lineWidth, 1.0);
|
|
257
262
|
float halfWidth = lineWidth / 2.0;
|
|
258
263
|
|
|
259
264
|
// HHAA: expand by w+1 so that partially covered pixels are included too
|
|
@@ -298,7 +303,7 @@ void main() {
|
|
|
298
303
|
v_linesofar = segmentStart + t * screenSegmentLength; // cumulative distance (for dashes)
|
|
299
304
|
v_line_pos = linePos; // position along the line
|
|
300
305
|
v_seg_length = screenSegmentLength; // segment length (computed on the GPU)
|
|
301
|
-
v_color = a_color;
|
|
306
|
+
v_color = a_color * ink; // line color (instance attribute), by the ink
|
|
302
307
|
}`;
|
|
303
308
|
}
|
|
304
309
|
/**
|
|
@@ -55,6 +55,15 @@ export interface PointInstanceData {
|
|
|
55
55
|
* Cutting the billboard exactly at the outer radius loses the outer half of the antialiasing.
|
|
56
56
|
*/
|
|
57
57
|
export declare const POINT_AA_PADDING_PX = 1;
|
|
58
|
+
/**
|
|
59
|
+
* The smallest outer radius a point is drawn at (device pixels): a diameter of one pixel
|
|
60
|
+
*
|
|
61
|
+
* A smaller point cannot be drawn smaller, since the antialiasing band alone is a pixel wide: its
|
|
62
|
+
* coverage would tend to a blob of a fixed size as the radius goes to 0, and the point would never
|
|
63
|
+
* fade. Below this radius the shape is drawn at it, fainter by the ratio of the areas
|
|
64
|
+
* (`pointInkFactor`), so the ink it leaves follows its area and goes to nothing with it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const POINT_MIN_OUTER_PX = 0.5;
|
|
58
67
|
/**
|
|
59
68
|
* SDF boundaries of one instance (device pixels)
|
|
60
69
|
*/
|
|
@@ -85,6 +94,16 @@ export interface PointSdfEdges {
|
|
|
85
94
|
* @param sizeScale Size factor at draw time (default 1)
|
|
86
95
|
*/
|
|
87
96
|
export declare function pointSdfEdges(fillSizePx: number, strokeWidthPx: number, sizeScale?: number): PointSdfEdges;
|
|
97
|
+
/**
|
|
98
|
+
* The factor the alpha of a point is multiplied by (the same computation as the shader)
|
|
99
|
+
*
|
|
100
|
+
* 1 at an outer radius of `POINT_MIN_OUTER_PX` and above. Below it the point is drawn at that
|
|
101
|
+
* radius (`pointSdfEdges`), and the factor is the ratio of its true area to the area drawn, so the
|
|
102
|
+
* ink it leaves follows its area and goes to 0 with it. A radius of 0 or below draws nothing (0).
|
|
103
|
+
*
|
|
104
|
+
* @param outerPx The outer radius (radius + stroke width, with the size factor; device pixels)
|
|
105
|
+
*/
|
|
106
|
+
export declare function pointInkFactor(outerPx: number): number;
|
|
88
107
|
/**
|
|
89
108
|
* Point batch of the retained mode (GPU resources)
|
|
90
109
|
*
|
|
@@ -34,6 +34,15 @@ const AA_HALF_WIDTH_PX = 0.5;
|
|
|
34
34
|
* Cutting the billboard exactly at the outer radius loses the outer half of the antialiasing.
|
|
35
35
|
*/
|
|
36
36
|
export const POINT_AA_PADDING_PX = 1;
|
|
37
|
+
/**
|
|
38
|
+
* The smallest outer radius a point is drawn at (device pixels): a diameter of one pixel
|
|
39
|
+
*
|
|
40
|
+
* A smaller point cannot be drawn smaller, since the antialiasing band alone is a pixel wide: its
|
|
41
|
+
* coverage would tend to a blob of a fixed size as the radius goes to 0, and the point would never
|
|
42
|
+
* fade. Below this radius the shape is drawn at it, fainter by the ratio of the areas
|
|
43
|
+
* (`pointInkFactor`), so the ink it leaves follows its area and goes to nothing with it.
|
|
44
|
+
*/
|
|
45
|
+
export const POINT_MIN_OUTER_PX = 0.5;
|
|
37
46
|
// Stride of the instance data (floats per instance)
|
|
38
47
|
// position(2) + fillColor(4) + strokeColor(4) + size(1) + strokeWidth(1)
|
|
39
48
|
// + elevation(1) = 13 floats
|
|
@@ -58,8 +67,34 @@ const SHAPE_UNIFORM = POINT_SHAPE_CODE;
|
|
|
58
67
|
export function pointSdfEdges(fillSizePx, strokeWidthPx, sizeScale = 1) {
|
|
59
68
|
const radius = Math.max(0, fillSizePx) * sizeScale;
|
|
60
69
|
const outer = radius + Math.max(0, strokeWidthPx) * sizeScale;
|
|
70
|
+
// Below a diameter of one pixel the shape is drawn at that diameter (pointInkFactor)
|
|
71
|
+
if (outer > 0 && outer < POINT_MIN_OUTER_PX) {
|
|
72
|
+
const grow = POINT_MIN_OUTER_PX / outer;
|
|
73
|
+
return {
|
|
74
|
+
radius: radius * grow,
|
|
75
|
+
outer: POINT_MIN_OUTER_PX,
|
|
76
|
+
extent: POINT_MIN_OUTER_PX + POINT_AA_PADDING_PX,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
61
79
|
return { radius, outer, extent: outer + POINT_AA_PADDING_PX };
|
|
62
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The factor the alpha of a point is multiplied by (the same computation as the shader)
|
|
83
|
+
*
|
|
84
|
+
* 1 at an outer radius of `POINT_MIN_OUTER_PX` and above. Below it the point is drawn at that
|
|
85
|
+
* radius (`pointSdfEdges`), and the factor is the ratio of its true area to the area drawn, so the
|
|
86
|
+
* ink it leaves follows its area and goes to 0 with it. A radius of 0 or below draws nothing (0).
|
|
87
|
+
*
|
|
88
|
+
* @param outerPx The outer radius (radius + stroke width, with the size factor; device pixels)
|
|
89
|
+
*/
|
|
90
|
+
export function pointInkFactor(outerPx) {
|
|
91
|
+
if (!(outerPx > 0))
|
|
92
|
+
return 0;
|
|
93
|
+
if (outerPx >= POINT_MIN_OUTER_PX)
|
|
94
|
+
return 1;
|
|
95
|
+
const ratio = outerPx / POINT_MIN_OUTER_PX;
|
|
96
|
+
return ratio * ratio;
|
|
97
|
+
}
|
|
63
98
|
/**
|
|
64
99
|
* Rounds the origin of the relative coordinates to Float32
|
|
65
100
|
*/
|
|
@@ -326,6 +361,7 @@ flat out vec4 v_stroke_color;
|
|
|
326
361
|
out vec2 v_offset_px;
|
|
327
362
|
flat out float v_radius_px;
|
|
328
363
|
flat out float v_outer_px;
|
|
364
|
+
flat out float v_ink;
|
|
329
365
|
|
|
330
366
|
void main() {
|
|
331
367
|
// The anchor elevation comes from the CPU (the same source as hit testing).
|
|
@@ -338,6 +374,15 @@ void main() {
|
|
|
338
374
|
|
|
339
375
|
float radius = max(a_instance_size, 0.0) * u_size_scale;
|
|
340
376
|
float outer = radius + max(a_instance_stroke_width, 0.0) * u_size_scale;
|
|
377
|
+
// Below a diameter of one pixel the shape is drawn at that diameter, fainter by the ratio of
|
|
378
|
+
// the areas, so the ink it leaves follows its area (pointSdfEdges, pointInkFactor)
|
|
379
|
+
v_ink = 1.0;
|
|
380
|
+
if (outer > 0.0 && outer < ${POINT_MIN_OUTER_PX.toFixed(1)}) {
|
|
381
|
+
float grow = ${POINT_MIN_OUTER_PX.toFixed(1)} / outer;
|
|
382
|
+
v_ink = 1.0 / (grow * grow);
|
|
383
|
+
radius *= grow;
|
|
384
|
+
outer = ${POINT_MIN_OUTER_PX.toFixed(1)};
|
|
385
|
+
}
|
|
341
386
|
// The billboard is sized as the outer radius plus the antialiasing padding
|
|
342
387
|
// (so that the edge is not clipped)
|
|
343
388
|
vec2 offsetPx = a_quad * (outer + ${POINT_AA_PADDING_PX.toFixed(1)});
|
|
@@ -357,6 +402,7 @@ flat in vec4 v_stroke_color;
|
|
|
357
402
|
in vec2 v_offset_px;
|
|
358
403
|
flat in float v_radius_px;
|
|
359
404
|
flat in float v_outer_px;
|
|
405
|
+
flat in float v_ink; // the factor of a point below one pixel (1 otherwise)
|
|
360
406
|
uniform float u_opacity; // opacity factor at draw time (default 1)
|
|
361
407
|
uniform float u_shape; // shape (0 = circle, 1 = square, 2 = triangle, 3 = star)
|
|
362
408
|
out vec4 fragColor;
|
|
@@ -384,7 +430,7 @@ void main() {
|
|
|
384
430
|
vec4 color = mix(v_stroke_color, v_fill_color, fillMix);
|
|
385
431
|
|
|
386
432
|
// Blending is non-premultiplied, so the factors are applied to the alpha only
|
|
387
|
-
fragColor = vec4(color.rgb, color.a * u_opacity * coverage);
|
|
433
|
+
fragColor = vec4(color.rgb, color.a * u_opacity * coverage * v_ink);
|
|
388
434
|
}`;
|
|
389
435
|
return createProgram(this.gl, vertexSource, fragmentSource);
|
|
390
436
|
}
|
|
@@ -376,6 +376,10 @@ void main() {
|
|
|
376
376
|
float lineWidth = (strokeWidth >= 0.0
|
|
377
377
|
? strokeWidth * pow(2.0, u_zoom - createdZoom)
|
|
378
378
|
: -strokeWidth) * u_size_scale;
|
|
379
|
+
// Below one pixel the outline is drawn one pixel wide and fainter by its width, so the
|
|
380
|
+
// ink per unit of length follows the width and goes to nothing with it
|
|
381
|
+
float ink = clamp(lineWidth, 0.0, 1.0);
|
|
382
|
+
lineWidth = max(lineWidth, 1.0);
|
|
379
383
|
float halfWidth = lineWidth / 2.0;
|
|
380
384
|
|
|
381
385
|
// Expand by 1 pixel for HHAA
|
|
@@ -389,7 +393,7 @@ void main() {
|
|
|
389
393
|
vec2 finalNdc = (finalScreenPos / u_viewport) * 2.0 - 1.0;
|
|
390
394
|
gl_Position = vec4(finalNdc * clipPos.w, clipPos.z, clipPos.w);
|
|
391
395
|
|
|
392
|
-
v_color = a_color;
|
|
396
|
+
v_color = vec4(a_color.rgb, a_color.a * ink);
|
|
393
397
|
v_type = 1.0;
|
|
394
398
|
v_dist = a_side * expandedHalfWidth;
|
|
395
399
|
v_halfWidth = halfWidth;
|
|
@@ -207,6 +207,15 @@ float dashDistance(vec2 sa, vec2 sb, float start, float len, float dash, float p
|
|
|
207
207
|
return best;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// The coverage of an outline of a half width at a screen distance from it. Below one
|
|
211
|
+
// pixel of width the outline is drawn one pixel wide and fainter by its width, so the ink
|
|
212
|
+
// per unit of length follows the width and goes to nothing with it (the rule of the
|
|
213
|
+
// vertex path's line and outline shaders)
|
|
214
|
+
float strokeCoverage(float halfWidth, float dist) {
|
|
215
|
+
float ink = clamp(2.0 * halfWidth, 0.0, 1.0);
|
|
216
|
+
return clamp(max(halfWidth, 0.5) + 0.5 - dist, 0.0, 1.0) * ink;
|
|
217
|
+
}
|
|
218
|
+
|
|
210
219
|
// Compose on top (premultiplied alpha)
|
|
211
220
|
vec4 over(vec4 below, vec3 rgb, float a) {
|
|
212
221
|
return vec4(rgb * a + below.rgb * (1.0 - a), a + below.a * (1.0 - a));
|
|
@@ -342,7 +351,7 @@ void main() {
|
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
if (paints && halfWidth > 0.0 && stroke.a > 0.0) {
|
|
345
|
-
float coverage =
|
|
354
|
+
float coverage = strokeCoverage(halfWidth, strokeDist);
|
|
346
355
|
if (coverage > 0.0) acc = over(acc, stroke.rgb, stroke.a * coverage * factors.x);
|
|
347
356
|
}
|
|
348
357
|
|
|
@@ -367,7 +376,7 @@ void main() {
|
|
|
367
376
|
// for features that have no outline (the same as the immediate mode), and
|
|
368
377
|
// it is solid under a dashed outline too
|
|
369
378
|
float selHalf = halfWidth + u_selection_stroke_extra * 0.5;
|
|
370
|
-
selStroke = max(selStroke,
|
|
379
|
+
selStroke = max(selStroke, strokeCoverage(selHalf, minDist) * factors.x);
|
|
371
380
|
}
|
|
372
381
|
}
|
|
373
382
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sakuzu/maplibre-gl-draw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "WebGL2 drawing and editing engine for MapLibre GL JS: points, lines, and polygons rendered in a custom layer, with selection, hit testing, and geometry utilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"maplibre",
|