@ngis/plugin-sdk 0.4.0 → 0.4.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.
@@ -32,7 +32,9 @@
32
32
  */
33
33
  import type { AnalysisToolSchema } from "../../api/analysis";
34
34
  import type { SceneType } from "../../api/scenes";
35
+ import type { NgisCrsRef } from "../../domain/models/crs";
35
36
  import type { NgisExecutionValidation, NgisToolUiSchema } from "../../types/analysis";
37
+ import type { NgisResolvedToolDependency } from "../../types/extensions";
36
38
  import type { NgisBounds, NgisUnsubscribe } from "../map-api";
37
39
  export interface NgisSceneInfo {
38
40
  id: string;
@@ -59,6 +61,18 @@ export interface NgisSceneFacet {
59
61
  * `"table"` would need `NgisFile.fileType`, i.e. a second
60
62
  * `GET /api/files/{fileId}` per resource — an N+1 fan-out for a distinction no
61
63
  * v1 consumer needs.
64
+ *
65
+ * **SURVIVES Stage 29 UNCHANGED (F29.1 row 3).** It is the SDK's v1
66
+ * plugin-facing classification, frozen by XD7/F13.6 and enforced by two live
67
+ * gates, and Stage 29 registers **no table dataset model** — so nothing about it
68
+ * moves (F29.2). It is a *projection* of `NgisDatasetKind` onto the coarser
69
+ * vocabulary v1 plugins compile against, never a second spelling of it: its
70
+ * membership is closed because a plugin's `when` expression is, and
71
+ * `NgisDatasetKind` is open because a kind is registered data. Widening this one
72
+ * to match would break every shipped plugin fixture.
73
+ *
74
+ * *(`GENERIC` no longer reaches the classifier — F29.3 removed it from the wire —
75
+ * but the "everything else" arm is unchanged and still catches it.)*
62
76
  */
63
77
  export type NgisResourceType = "vector" | "raster" | "table" | "file";
64
78
  /**
@@ -89,6 +103,13 @@ export type NgisResourceType = "vector" | "raster" | "table" | "file";
89
103
  * against a builtin node's `"Raster"` and `400`s at workflow save. Y6 applies
90
104
  * that column at, and **only** at, the `ngis-workflow` boundary. Nowhere else
91
105
  * in either repo is Pascal correct.
106
+ *
107
+ * **SURVIVES Stage 29 UNCHANGED (F29.1 row 4).** It is a **tool port** data type
108
+ * (F14.1), not a dataset kind. The two are related by the compatibility matrix
109
+ * and **never by equality**: `table` and `pointcloud` are legal port types that
110
+ * no registered kind emits, and a registered kind the client has never seen binds
111
+ * to a port through the matrix rather than by name. Renaming or merging them
112
+ * would assert an identity that does not hold.
92
113
  */
93
114
  export type NgisDataType = "raster" | "vector" | "table" | "pointcloud" | "raster3d" | "file";
94
115
  export declare const NGIS_DATA_TYPES: readonly ["raster", "vector", "table", "pointcloud", "raster3d", "file"];
@@ -130,6 +151,25 @@ export interface NgisResourceSummary {
130
151
  geometryType: string | null;
131
152
  layerCount: number;
132
153
  rendered: boolean;
154
+ /**
155
+ * **Stage 25 (F25.1 / F25.9, `S25-V216`).** The dataset's one CRS, or `null`
156
+ * when the server has none for it (`NEEDS_CRS`).
157
+ *
158
+ * It is on the **summary**, not only the detail, and that placement is the
159
+ * point of the row. `ResourceSelect` lists datasets from `resources.list()`
160
+ * and must draw each row's projection state at pick time (F25.5); resolving a
161
+ * ref through `resources.get()` per row is the N+1 XD7 already refused for
162
+ * `resources.types`. Without this field the picker could draw only the
163
+ * *absent* state — which is what S25-22 shipped, with the limit recorded
164
+ * rather than papered over — because a dataset that *has* a projection had
165
+ * nothing to draw a chip from and inventing one is what G-CRS forbids.
166
+ *
167
+ * `null` is a value and not a gap: it is the server saying `NEEDS_CRS`. There
168
+ * is no `undefined` arm — the facet always answers, and a dataset it could not
169
+ * read at all reports `datasetStatus: "N/A"` with `crs: null`, which is the
170
+ * same honest "we cannot say what this is in".
171
+ */
172
+ crs: NgisCrsRef | null;
133
173
  /**
134
174
  * Scene layers that render this resource, when the host can supply them.
135
175
  *
@@ -160,7 +200,6 @@ export interface NgisResourceField {
160
200
  type: string;
161
201
  }
162
202
  export interface NgisResourceDetail extends NgisResourceSummary {
163
- srid: number | null;
164
203
  /** Vector only; `null` otherwise. */
165
204
  featureCount: number | null;
166
205
  /** Vector only; `[]` otherwise. */
@@ -244,7 +283,21 @@ export interface NgisModelValidateRequest {
244
283
  expectedSceneGeneration?: number;
245
284
  outputMode?: "ADD_LAYER" | "UPDATE_RESOURCE";
246
285
  targetResourceId?: string;
247
- targetSrid?: number;
286
+ /**
287
+ * **Stage 25 (F25.4, and the `S25-V124` ruling that AMENDS F25.12).**
288
+ *
289
+ * Replaces `targetSrid?: number`. It is an {@link NgisCrsRef} **key** — the
290
+ * string `"EPSG:32650"` or `"custom:<ulid>"` — and never an integer, because
291
+ * an integer cannot name a custom CRS at all and because a bare number is the
292
+ * silent-default spelling G-CRS refuses.
293
+ *
294
+ * `targetSrid` was typed, forwarded to `validate-execution` and set by no
295
+ * component: dead plumbing. This member is the live one — `run-data.ts` fills
296
+ * it from the form's `ngis:outputCrs` parameter and the facade adapter puts it
297
+ * on the wire as `outputCrs` (the REST leg is camelCase here; the proto
298
+ * spelling is `output_crs`).
299
+ */
300
+ outputCrs?: string;
248
301
  }
249
302
  export interface NgisModelSearchRequest {
250
303
  query?: string;
@@ -300,6 +353,12 @@ export interface NgisModelsFacet {
300
353
  /** The one normalized UI schema (F14.2). `null` when the tool or its schema
301
354
  * is absent. Scope `models:read`. */
302
355
  uiSchema(toolId: string): Promise<NgisToolUiSchema | null>;
356
+ /**
357
+ * Resolves a declared `manifest.toolDependencies[].alias` for this plugin and
358
+ * viewer. The unavailable arm is a normal, renderable result; it never leaks
359
+ * implementation UUIDs into plugin manifests. Scope `models:read`.
360
+ */
361
+ dependency(alias: string): Promise<NgisResolvedToolDependency>;
303
362
  /** REST projection of `ValidateToolExecution`. Returns the response DTO
304
363
  * **verbatim**. Scope `models:read`, **not** `models:run` — it runs nothing,
305
364
  * and returns only registry facts plus dataset metadata the caller can
@@ -324,6 +383,18 @@ export interface NgisTaskRunRequest {
324
383
  expectedSceneGeneration?: number;
325
384
  outputMode?: "ADD_LAYER" | "UPDATE_RESOURCE";
326
385
  targetResourceId?: string;
386
+ /**
387
+ * **Stage 25 (F25.4).** The output CRS this run computes in and stores its
388
+ * result as — an {@link NgisCrsRef} key, never an integer. Absent means
389
+ * "compute in the primary input's own CRS", which is a real choice and not a
390
+ * default: the server never substitutes one (G-CRS).
391
+ *
392
+ * This is the member that makes exit criterion 6 true. `submitRun` fills it,
393
+ * the tasks facet spreads the request through to the DIRECT source, and
394
+ * `createGisTasksSource` hands it to `startTrackedAnalysisTask`, which puts
395
+ * `outputCrs` in the `POST /api/analysis/tasks` body.
396
+ */
397
+ outputCrs?: string;
327
398
  }
328
399
  export interface NgisTaskHandle {
329
400
  /** DIRECT: the analysis task id. WORKFLOW: the workflow instance id. */
@@ -73,7 +73,7 @@ import type { WorkspacePanelRegistry } from "./panels";
73
73
  * semver compare, **not** by the unknown-capability check: `contributes` and
74
74
  * `permissions` are top-level manifest siblings, and a 0.3 runtime inspects
75
75
  * only `manifest.capabilities`. */
76
- export declare const NGIS_MAP_API_VERSION = "0.4.0";
76
+ export declare const NGIS_MAP_API_VERSION = "0.4.1";
77
77
  /** A scene layer as the SDK exposes it — a stable projection of the store's
78
78
  * `NgisSceneLayer`, so plugins never depend on the store's internal shape. */
79
79
  export interface NgisLayerSummary {
@@ -262,6 +262,9 @@ export interface NgisMapAPIBindings {
262
262
  /** Raw-map getter for `getMap()`. Usually omitted at construction and wired
263
263
  * later via `NgisMapAPIHandle.bindRawMap` (the post-C5 hookup). */
264
264
  getRawMap?(): MapLibreMap | null;
265
+ /** Host hook run before direct raw-map camera commands. Keeps host-owned
266
+ * temporary camera controllers from racing this SDK escape hatch. */
267
+ beforeCameraCommand?(map: MapLibreMap): void;
265
268
  }
266
269
  /**
267
270
  * The factory's return value: the public `api` plus the host-side "driver"
@@ -26,6 +26,7 @@ import type { NgisContributes, NgisPermissionScope } from "./contributions/types
26
26
  import type { NgisSymbolRendererModule } from "./contributions/symbol-renderers";
27
27
  import type { PanelRegistration } from "./panels";
28
28
  import type { GisDynamicToolDef } from "./rail-tools";
29
+ import type { NgisToolDependencyDeclaration } from "../types/extensions";
29
30
  /**
30
31
  * **F13.1 / ED19 (Stage 13, X4b) — the `NgisPluginHost` widening.**
31
32
  *
@@ -106,6 +107,11 @@ export interface NgisPluginManifest {
106
107
  * here and is never prompted.
107
108
  */
108
109
  permissions?: readonly NgisPermissionScope[];
110
+ /**
111
+ * Semantic tool requirements. Bundle code obtains the selected runtime tool
112
+ * through `host.models.dependency(alias)`; it must not contain a tool UUID.
113
+ */
114
+ toolDependencies?: readonly NgisToolDependencyDeclaration[];
109
115
  /** **Reserved, never read this stage** (D3): the future third-party loader's
110
116
  * ESM-bundle path. First-party plugins are statically imported instead. */
111
117
  entry?: string;
@@ -62,6 +62,7 @@ export declare const NGIS_SCOPE_MEMBERS: {
62
62
  readonly "models.get": "models:read";
63
63
  readonly "models.schema": "models:read";
64
64
  readonly "models.uiSchema": "models:read";
65
+ readonly "models.dependency": "models:read";
65
66
  readonly "models.validate": "models:read";
66
67
  readonly "models.search": "models:read";
67
68
  readonly "tasks.run": "models:run";
@@ -1,15 +1,14 @@
1
1
  import type { CustomLayerInterface, CustomRenderMethodInput, Map as MapLibreMap } from "maplibre-gl";
2
2
  import type { SceneLayerStyle } from "../../domain/map/types";
3
+ import type { FlowFieldStyle } from "../../domain/models/flow-field-style";
3
4
  import type { AuthenticatedFetch } from "./authenticated-deck-load";
4
- export type FlowFieldLayerOptions = {
5
+ type FlowFieldStyleOptions = Pick<FlowFieldStyle, "opacity" | "colorRamp" | "speedRange" | "speedFactor" | "particleCount" | "lineWidth" | "trailLength" | "frameDurationMs" | "paused" | "maxParticleAge" | "dropRate" | "dropRateBump">;
6
+ export type FlowFieldLayerOptions = Partial<FlowFieldStyleOptions> & {
5
7
  id: string;
6
8
  flowFieldId: string;
7
- opacity: number;
8
- speedFactor: number;
9
- particleCount: number;
10
- paused: boolean;
11
9
  fetchWithAuth: AuthenticatedFetch;
12
10
  triggerRepaint: () => void;
11
+ onReady?: () => void;
13
12
  onError?: (error: Error) => void;
14
13
  };
15
14
  /**
@@ -38,6 +37,7 @@ export declare class FlowFieldLayer implements CustomLayerInterface {
38
37
  private validTextures;
39
38
  private domainMaskTexture;
40
39
  private projectionTexture;
40
+ private colorRampTexture;
41
41
  private simulationBuffers;
42
42
  private simulationVaos;
43
43
  private transformFeedbacks;
@@ -46,13 +46,15 @@ export declare class FlowFieldLayer implements CustomLayerInterface {
46
46
  private stateIndex;
47
47
  private trailHead;
48
48
  private trailCount;
49
- private frameCounter;
49
+ private frameElapsedMs;
50
+ private lastRenderTimeMs;
50
51
  private particleCount;
51
52
  constructor(options: FlowFieldLayerOptions);
52
53
  onAdd(map: MapLibreMap, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
53
54
  private load;
54
55
  private createGpuResources;
55
56
  private createParticleResources;
57
+ private getFrameState;
56
58
  private runSimulation;
57
59
  private bindTexture;
58
60
  private bindRenderBuffers;
@@ -66,3 +68,4 @@ export declare class FlowFieldLayer implements CustomLayerInterface {
66
68
  private destroyParticleResources;
67
69
  private releaseGpuResources;
68
70
  }
71
+ export {};
@@ -71,6 +71,9 @@ export interface DecodedFlowFieldAssets {
71
71
  domainMask: Uint8Array;
72
72
  speedRange: [number, number];
73
73
  }
74
+ type FlowFieldSpeedRangeListener = (range: [number, number]) => void;
75
+ export declare function getResolvedFlowFieldSpeedRange(fileId: string): [number, number] | null;
76
+ export declare function subscribeToResolvedFlowFieldSpeedRange(fileId: string, listener: FlowFieldSpeedRangeListener): () => void;
74
77
  interface RgbaImage {
75
78
  width: number;
76
79
  height: number;
@@ -1,4 +1,4 @@
1
- export declare const FLOW_UPDATE_VERTEX_SHADER = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec4 a_state;\n\nuniform sampler2D u_velocity0;\nuniform sampler2D u_velocity1;\nuniform sampler2D u_seed;\nuniform sampler2D u_domainMask;\nuniform sampler2D u_valid0;\nuniform sampler2D u_valid1;\nuniform vec2 u_gridSize;\nuniform float u_frameMix;\nuniform float u_speedFactor;\nuniform float u_dropRate;\nuniform float u_dropRateBump;\nuniform float u_randomSeed;\nuniform float u_maxAge;\nuniform float u_nodata;\nuniform vec2 u_speedRange;\n\nout vec4 v_state;\n\nfloat randomValue(vec2 value) {\n return fract(sin(dot(value, vec2(12.9898, 78.233))) * 43758.5453123);\n}\n\nbool isDefinedVelocity(vec2 velocity) {\n return !any(isnan(velocity)) && !any(isinf(velocity))\n && abs(velocity.x - u_nodata) > 0.5\n && abs(velocity.y - u_nodata) > 0.5;\n}\n\nvec2 readVelocity(vec2 uv) {\n return mix(texture(u_velocity0, uv).rg, texture(u_velocity1, uv).rg, u_frameMix);\n}\n\nivec2 decodeSeedCell(vec4 packed) {\n ivec4 bytes = ivec4(floor(packed * 255.0 + 0.5));\n return ivec2(\n (bytes.r << 8) + bytes.g,\n (bytes.b << 8) + bytes.a\n );\n}\n\nbool readMask(sampler2D maskTexture, vec2 uv) {\n ivec2 size = textureSize(maskTexture, 0);\n ivec2 cell = clamp(ivec2(floor(uv * vec2(size))), ivec2(0), size - ivec2(1));\n return texelFetch(maskTexture, cell, 0).r > 0.5;\n}\n\nbool isFlowCell(vec2 uv) {\n return readMask(u_domainMask, uv)\n && readMask(u_valid0, uv)\n && readMask(u_valid1, uv);\n}\n\nfloat normalizedSpeed(vec2 velocity) {\n return clamp(\n (length(velocity) - u_speedRange.x) / max(u_speedRange.y - u_speedRange.x, 1.0e-6),\n 0.0,\n 1.0\n );\n}\n\nvec2 seedPosition(vec2 randomUv) {\n vec2 cell = vec2(decodeSeedCell(texture(u_seed, randomUv)));\n vec2 jitter = vec2(\n randomValue(randomUv + vec2(1.31, u_randomSeed)),\n randomValue(randomUv + vec2(u_randomSeed, 2.17))\n );\n return clamp((cell + jitter) / u_gridSize, vec2(0.0), vec2(1.0));\n}\n\nvec4 respawn(float salt) {\n vec2 randomUv = vec2(\n randomValue(vec2(float(gl_VertexID) + salt, u_randomSeed + 0.37)),\n randomValue(vec2(u_randomSeed + 0.73, float(gl_VertexID) + salt))\n );\n vec2 uv = seedPosition(randomUv);\n vec2 velocity = readVelocity(uv);\n return vec4(uv, 0.0, isDefinedVelocity(velocity) ? normalizedSpeed(velocity) : 0.0);\n}\n\nvoid main() {\n vec2 velocity = readVelocity(a_state.xy);\n float speed = isDefinedVelocity(velocity) ? normalizedSpeed(velocity) : 0.0;\n vec2 nextPosition = a_state.xy + velocity * u_speedFactor / u_gridSize;\n bool outside = any(lessThan(nextPosition, vec2(0.0))) || any(greaterThan(nextPosition, vec2(1.0)));\n float randomDrop = randomValue(a_state.xy + vec2(u_randomSeed, float(gl_VertexID) * 0.000001));\n bool dropped = randomDrop < (u_dropRate + speed * u_dropRateBump);\n bool expired = a_state.z >= u_maxAge;\n\n if (!isDefinedVelocity(velocity) || !isFlowCell(a_state.xy) || outside || dropped || expired) {\n v_state = respawn(a_state.z + 1.0);\n return;\n }\n\n vec2 nextVelocity = readVelocity(nextPosition);\n if (!isDefinedVelocity(nextVelocity) || !isFlowCell(nextPosition)) {\n v_state = respawn(a_state.z + 2.0);\n return;\n }\n\n v_state = vec4(nextPosition, a_state.z + 1.0, normalizedSpeed(nextVelocity));\n}\n";
1
+ export declare const FLOW_UPDATE_VERTEX_SHADER = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec4 a_state;\n\nuniform sampler2D u_velocity0;\nuniform sampler2D u_velocity1;\nuniform sampler2D u_seed;\nuniform sampler2D u_domainMask;\nuniform sampler2D u_valid0;\nuniform sampler2D u_valid1;\nuniform vec2 u_gridSize;\nuniform float u_frameMix;\nuniform float u_speedFactor;\nuniform float u_dropRate;\nuniform float u_dropRateBump;\nuniform float u_randomSeed;\nuniform float u_maxAge;\nuniform float u_nodata;\nuniform vec2 u_speedRange;\nuniform float u_deltaSeconds;\n\nout vec4 v_state;\n\nfloat randomValue(vec2 value) {\n return fract(sin(dot(value, vec2(12.9898, 78.233))) * 43758.5453123);\n}\n\nbool isDefinedVelocity(vec2 velocity) {\n return !any(isnan(velocity)) && !any(isinf(velocity))\n && abs(velocity.x - u_nodata) > 0.5\n && abs(velocity.y - u_nodata) > 0.5;\n}\n\nvec2 readVelocity(vec2 uv) {\n return mix(texture(u_velocity0, uv).rg, texture(u_velocity1, uv).rg, u_frameMix);\n}\n\nivec2 decodeSeedCell(vec4 packed) {\n ivec4 bytes = ivec4(floor(packed * 255.0 + 0.5));\n return ivec2(\n (bytes.r << 8) + bytes.g,\n (bytes.b << 8) + bytes.a\n );\n}\n\nbool readMask(sampler2D maskTexture, vec2 uv) {\n ivec2 size = textureSize(maskTexture, 0);\n ivec2 cell = clamp(ivec2(floor(uv * vec2(size))), ivec2(0), size - ivec2(1));\n return texelFetch(maskTexture, cell, 0).r > 0.5;\n}\n\nbool isFlowCell(vec2 uv) {\n return readMask(u_domainMask, uv)\n && readMask(u_valid0, uv)\n && readMask(u_valid1, uv);\n}\n\nfloat normalizedSpeed(vec2 velocity) {\n return clamp(\n (length(velocity) - u_speedRange.x) / max(u_speedRange.y - u_speedRange.x, 1.0e-6),\n 0.0,\n 1.0\n );\n}\n\nvec2 seedPosition(vec2 randomUv) {\n vec2 cell = vec2(decodeSeedCell(texture(u_seed, randomUv)));\n vec2 jitter = vec2(\n randomValue(randomUv + vec2(1.31, u_randomSeed)),\n randomValue(randomUv + vec2(u_randomSeed, 2.17))\n );\n return clamp((cell + jitter) / u_gridSize, vec2(0.0), vec2(1.0));\n}\n\nvec4 respawn(float salt) {\n vec2 randomUv = vec2(\n randomValue(vec2(float(gl_VertexID) + salt, u_randomSeed + 0.37)),\n randomValue(vec2(u_randomSeed + 0.73, float(gl_VertexID) + salt))\n );\n vec2 uv = seedPosition(randomUv);\n vec2 velocity = readVelocity(uv);\n return vec4(uv, 0.0, isDefinedVelocity(velocity) ? length(velocity) : 0.0);\n}\n\nvoid main() {\n vec2 velocity = readVelocity(a_state.xy);\n float speed = isDefinedVelocity(velocity) ? normalizedSpeed(velocity) : 0.0;\n // 60 Hz preserves the fourth-round speed factor semantics while real elapsed\n // time makes 60 Hz and 144 Hz displays advance identically.\n vec2 nextPosition = a_state.xy + velocity * u_speedFactor * (u_deltaSeconds * 60.0) / u_gridSize;\n bool outside = any(lessThan(nextPosition, vec2(0.0))) || any(greaterThan(nextPosition, vec2(1.0)));\n float randomDrop = randomValue(a_state.xy + vec2(u_randomSeed, float(gl_VertexID) * 0.000001));\n bool dropped = randomDrop < (u_dropRate + speed * u_dropRateBump);\n bool expired = a_state.z >= u_maxAge;\n\n if (!isDefinedVelocity(velocity) || !isFlowCell(a_state.xy) || outside || dropped || expired) {\n v_state = respawn(a_state.z + 1.0);\n return;\n }\n\n vec2 nextVelocity = readVelocity(nextPosition);\n if (!isDefinedVelocity(nextVelocity) || !isFlowCell(nextPosition)) {\n v_state = respawn(a_state.z + 2.0);\n return;\n }\n\n v_state = vec4(nextPosition, a_state.z + 1.0, length(nextVelocity));\n}\n";
2
2
  export declare const FLOW_UPDATE_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\nvoid main() {}\n";
3
- export declare const FLOW_RENDER_VERTEX_SHADER = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec4 a_fromState;\nlayout(location = 1) in vec4 a_toState;\n\nuniform sampler2D u_projection;\nuniform sampler2D u_domainMask;\nuniform sampler2D u_valid0;\nuniform sampler2D u_valid1;\nuniform mat4 u_matrix;\nuniform float u_alpha;\nuniform int u_points;\nuniform vec2 u_viewport;\n\nout float v_speed;\nout float v_alpha;\nout float v_edge;\n\nbool validProjection(vec2 projected) {\n return !any(isnan(projected)) && !any(isinf(projected));\n}\n\nbool readMask(sampler2D maskTexture, vec2 uv) {\n ivec2 size = textureSize(maskTexture, 0);\n ivec2 cell = clamp(ivec2(floor(uv * vec2(size))), ivec2(0), size - ivec2(1));\n return texelFetch(maskTexture, cell, 0).r > 0.5;\n}\n\nbool validCell(vec2 uv) {\n return readMask(u_domainMask, uv)\n && readMask(u_valid0, uv)\n && readMask(u_valid1, uv);\n}\n\nbool validCellAt(ivec2 cell) {\n ivec2 size = textureSize(u_domainMask, 0);\n if (any(lessThan(cell, ivec2(0))) || any(greaterThanEqual(cell, size))) return false;\n return texelFetch(u_domainMask, cell, 0).r > 0.5\n && texelFetch(u_valid0, cell, 0).r > 0.5\n && texelFetch(u_valid1, cell, 0).r > 0.5;\n}\n\nbool validSegment(vec2 start, vec2 end) {\n if (any(lessThan(start, vec2(0.0))) || any(greaterThan(start, vec2(1.0)))\n || any(lessThan(end, vec2(0.0))) || any(greaterThan(end, vec2(1.0)))) return false;\n vec2 size = vec2(textureSize(u_domainMask, 0));\n vec2 from = min(start * size, size - vec2(0.000001));\n vec2 to = min(end * size, size - vec2(0.000001));\n ivec2 cell = ivec2(floor(from));\n ivec2 target = ivec2(floor(to));\n vec2 delta = to - from;\n ivec2 cellStep = ivec2(sign(delta));\n vec2 tDelta = vec2(\n delta.x == 0.0 ? 1.0e30 : abs(1.0 / delta.x),\n delta.y == 0.0 ? 1.0e30 : abs(1.0 / delta.y)\n );\n vec2 nextBoundary = vec2(cell) + vec2(\n cellStep.x > 0 ? 1.0 : 0.0,\n cellStep.y > 0 ? 1.0 : 0.0\n );\n vec2 tMax = vec2(\n delta.x == 0.0 ? 1.0e30 : (nextBoundary.x - from.x) / delta.x,\n delta.y == 0.0 ? 1.0e30 : (nextBoundary.y - from.y) / delta.y\n );\n for (int visited = 0; visited < 256; visited += 1) {\n if (!validCellAt(cell)) return false;\n if (all(equal(cell, target))) return true;\n bool advanceX = tMax.x <= tMax.y;\n bool advanceY = tMax.y <= tMax.x;\n if (advanceX) {\n cell.x += cellStep.x;\n tMax.x += tDelta.x;\n }\n if (advanceY) {\n cell.y += cellStep.y;\n tMax.y += tDelta.y;\n }\n }\n return false;\n}\n\nvoid main() {\n bool continuous = a_toState.z > a_fromState.z\n && distance(a_fromState.xy, a_toState.xy) < 0.1;\n vec2 fromProjected = texture(u_projection, clamp(a_fromState.xy, vec2(0.0), vec2(1.0))).rg;\n vec2 toProjected = texture(u_projection, clamp(a_toState.xy, vec2(0.0), vec2(1.0))).rg;\n\n if ((!continuous && u_points == 0)\n || !validSegment(a_fromState.xy, a_toState.xy)\n || !validProjection(fromProjected)\n || !validProjection(toProjected)) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n v_speed = 0.0;\n v_alpha = 0.0;\n v_edge = 1.0;\n return;\n }\n\n vec4 fromClip = u_matrix * vec4(fromProjected, 0.0, 1.0);\n vec4 toClip = u_matrix * vec4(toProjected, 0.0, 1.0);\n if (u_points == 1) {\n gl_Position = toClip;\n gl_PointSize = 1.5;\n v_edge = 0.0;\n } else {\n vec2 fromScreen = fromClip.xy / fromClip.w;\n vec2 toScreen = toClip.xy / toClip.w;\n vec2 direction = normalize((toScreen - fromScreen) * u_viewport);\n vec2 normal = vec2(-direction.y, direction.x);\n float side = (gl_VertexID == 0 || gl_VertexID == 2) ? -1.0 : 1.0;\n bool useTo = gl_VertexID >= 2;\n vec4 base = useTo ? toClip : fromClip;\n vec2 screen = base.xy / base.w;\n vec2 offset = normal * side * 2.0 / u_viewport;\n gl_Position = vec4((screen + offset) * base.w, base.z, base.w);\n v_edge = side;\n }\n v_speed = a_toState.w;\n v_alpha = u_alpha;\n}\n";
4
- export declare const FLOW_RENDER_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\n\nin float v_speed;\nin float v_alpha;\nin float v_edge;\nout vec4 fragColor;\n\nconst vec3 COLORS[8] = vec3[](\n vec3(0.196, 0.533, 0.741),\n vec3(0.400, 0.761, 0.647),\n vec3(0.671, 0.867, 0.643),\n vec3(0.902, 0.961, 0.596),\n vec3(0.996, 0.878, 0.545),\n vec3(0.992, 0.682, 0.380),\n vec3(0.957, 0.427, 0.263),\n vec3(0.835, 0.243, 0.310)\n);\n\nvec3 velocityColor(float speed) {\n float scaled = clamp(speed, 0.0, 1.0) * 7.0;\n int low = int(floor(scaled));\n int high = min(low + 1, 7);\n return mix(COLORS[low], COLORS[high], fract(scaled));\n}\n\nvoid main() {\n float antialias = 1.0 - smoothstep(0.45, 1.0, abs(v_edge));\n fragColor = vec4(velocityColor(v_speed), v_alpha * antialias);\n}\n";
3
+ export declare const FLOW_RENDER_VERTEX_SHADER = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec4 a_fromState;\nlayout(location = 1) in vec4 a_toState;\n\nuniform sampler2D u_projection;\nuniform sampler2D u_domainMask;\nuniform sampler2D u_valid0;\nuniform sampler2D u_valid1;\nuniform mat4 u_matrix;\nuniform float u_alpha;\nuniform int u_points;\nuniform vec2 u_viewport;\nuniform float u_lineWidth;\n\nout float v_speed;\nout float v_alpha;\nout float v_edge;\n\nbool validProjection(vec2 projected) {\n return !any(isnan(projected)) && !any(isinf(projected));\n}\n\nbool readMask(sampler2D maskTexture, vec2 uv) {\n ivec2 size = textureSize(maskTexture, 0);\n ivec2 cell = clamp(ivec2(floor(uv * vec2(size))), ivec2(0), size - ivec2(1));\n return texelFetch(maskTexture, cell, 0).r > 0.5;\n}\n\nbool validCell(vec2 uv) {\n return readMask(u_domainMask, uv)\n && readMask(u_valid0, uv)\n && readMask(u_valid1, uv);\n}\n\nbool validCellAt(ivec2 cell) {\n ivec2 size = textureSize(u_domainMask, 0);\n if (any(lessThan(cell, ivec2(0))) || any(greaterThanEqual(cell, size))) return false;\n return texelFetch(u_domainMask, cell, 0).r > 0.5\n && texelFetch(u_valid0, cell, 0).r > 0.5\n && texelFetch(u_valid1, cell, 0).r > 0.5;\n}\n\nbool validSegment(vec2 start, vec2 end) {\n if (any(lessThan(start, vec2(0.0))) || any(greaterThan(start, vec2(1.0)))\n || any(lessThan(end, vec2(0.0))) || any(greaterThan(end, vec2(1.0)))) return false;\n vec2 size = vec2(textureSize(u_domainMask, 0));\n vec2 from = min(start * size, size - vec2(0.000001));\n vec2 to = min(end * size, size - vec2(0.000001));\n ivec2 cell = ivec2(floor(from));\n ivec2 target = ivec2(floor(to));\n vec2 delta = to - from;\n ivec2 cellStep = ivec2(sign(delta));\n vec2 tDelta = vec2(\n delta.x == 0.0 ? 1.0e30 : abs(1.0 / delta.x),\n delta.y == 0.0 ? 1.0e30 : abs(1.0 / delta.y)\n );\n vec2 nextBoundary = vec2(cell) + vec2(\n cellStep.x > 0 ? 1.0 : 0.0,\n cellStep.y > 0 ? 1.0 : 0.0\n );\n vec2 tMax = vec2(\n delta.x == 0.0 ? 1.0e30 : (nextBoundary.x - from.x) / delta.x,\n delta.y == 0.0 ? 1.0e30 : (nextBoundary.y - from.y) / delta.y\n );\n for (int visited = 0; visited < 256; visited += 1) {\n if (!validCellAt(cell)) return false;\n if (all(equal(cell, target))) return true;\n bool advanceX = tMax.x <= tMax.y;\n bool advanceY = tMax.y <= tMax.x;\n if (advanceX) {\n cell.x += cellStep.x;\n tMax.x += tDelta.x;\n }\n if (advanceY) {\n cell.y += cellStep.y;\n tMax.y += tDelta.y;\n }\n }\n return false;\n}\n\nvoid main() {\n bool continuous = a_toState.z > a_fromState.z\n && distance(a_fromState.xy, a_toState.xy) < 0.1;\n vec2 fromProjected = texture(u_projection, clamp(a_fromState.xy, vec2(0.0), vec2(1.0))).rg;\n vec2 toProjected = texture(u_projection, clamp(a_toState.xy, vec2(0.0), vec2(1.0))).rg;\n\n if ((!continuous && u_points == 0)\n || !validSegment(a_fromState.xy, a_toState.xy)\n || !validProjection(fromProjected)\n || !validProjection(toProjected)) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n v_speed = 0.0;\n v_alpha = 0.0;\n v_edge = 1.0;\n return;\n }\n\n vec4 fromClip = u_matrix * vec4(fromProjected, 0.0, 1.0);\n vec4 toClip = u_matrix * vec4(toProjected, 0.0, 1.0);\n if (u_points == 1) {\n gl_Position = toClip;\n gl_PointSize = max(1.0, u_lineWidth);\n v_edge = 0.0;\n } else {\n vec2 fromScreen = fromClip.xy / fromClip.w;\n vec2 toScreen = toClip.xy / toClip.w;\n vec2 direction = normalize((toScreen - fromScreen) * u_viewport);\n vec2 normal = vec2(-direction.y, direction.x);\n float side = (gl_VertexID == 0 || gl_VertexID == 2) ? -1.0 : 1.0;\n bool useTo = gl_VertexID >= 2;\n vec4 base = useTo ? toClip : fromClip;\n vec2 screen = base.xy / base.w;\n vec2 offset = normal * side * u_lineWidth / u_viewport;\n gl_Position = vec4((screen + offset) * base.w, base.z, base.w);\n v_edge = side;\n }\n v_speed = a_toState.w;\n v_alpha = u_alpha;\n}\n";
4
+ export declare const FLOW_RENDER_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\n\nin float v_speed;\nin float v_alpha;\nin float v_edge;\nout vec4 fragColor;\nuniform sampler2D u_colorRamp;\nuniform vec2 u_speedRange;\n\nvoid main() {\n float antialias = 1.0 - smoothstep(0.45, 1.0, abs(v_edge));\n float normalizedSpeed = clamp(\n (v_speed - u_speedRange.x) / max(u_speedRange.y - u_speedRange.x, 1.0e-6),\n 0.0,\n 1.0\n );\n vec4 rampColor = texture(u_colorRamp, vec2(normalizedSpeed, 0.5));\n float alpha = rampColor.a * v_alpha * antialias;\n // MapLibre's custom-layer canvas is premultiplied. Keeping this output and\n // the render pass in the same convention prevents repeated trail fragments\n // from being composited as straight-alpha colours.\n fragColor = vec4(rampColor.rgb * alpha, alpha);\n}\n";
@@ -26,6 +26,7 @@
26
26
  * `src/sdk/facets/types.ts`; F14.2 fixes the schema family's home here.
27
27
  */
28
28
  import type { NgisDataType } from "../sdk/facets/types";
29
+ import type { NgisValidationIssue } from "../domain/models/crs";
29
30
  export type WorkSurface = "workflow" | "insight" | "code";
30
31
  export type RightDockMode = "none" | "ai";
31
32
  export type MapFocusMode = "split" | "map" | "work";
@@ -35,15 +36,31 @@ export type NgisParamRole = "input" | "option" | "output";
35
36
  /**
36
37
  * Total, frozen. **There is NO fallback widget:** an unknown key is a
37
38
  * render-time error surfaced in place, never a silently-degraded text input.
38
- * A Y3 widget registry missing any of the twelve is a defect.
39
+ * A Y3 widget registry missing any of the thirteen is a defect.
39
40
  *
40
41
  * `field-select`, `extent` and `textarea` are **never emitted by YB1 in v1** —
41
42
  * no builtin schema expresses a field, a bbox or a long-text parameter. They
42
43
  * are in the union because the registry must be total over it, and because an
43
44
  * author's `ParamForm` `widgets` override and a recipe's promoted inputs can
44
45
  * select them.
46
+ *
47
+ * ## `"crs"` is the thirteenth, and it **AMENDS F14.2** (Stage 25, F25.3)
48
+ *
49
+ * F14.2 froze twelve keys and called the union closed. `"crs"` is added by an
50
+ * explicit amendment, not by growth: it is the widget for `ngis:outputCrs`, the
51
+ * one **platform** parameter every tool's UI schema carries (F25.4), and there
52
+ * was no key in the twelve that could carry a coordinate reference system
53
+ * without lying about it — `select` would have needed a closed choice list over
54
+ * the EPSG registry, and `text` would have accepted anything.
55
+ *
56
+ * Because {@link NgisWidgetRegistry} is a *total* mapped type, an external
57
+ * implementer that does not add the key **fails to compile. That is the intended
58
+ * signal**, recorded here as an amendment rather than discovered as an accident.
59
+ * S25-02 enumerated every implementer in the repo — there are none outside
60
+ * `src/` — and the union has **nine** in-repo gate sites, not one; F25.3 lists
61
+ * them.
45
62
  */
46
- export type NgisWidgetKey = "resource-select" | "resource-multi" | "select" | "multi-select" | "switch" | "number" | "integer" | "text" | "textarea" | "output-name" | "field-select" | "extent";
63
+ export type NgisWidgetKey = "resource-select" | "resource-multi" | "select" | "multi-select" | "switch" | "number" | "integer" | "text" | "textarea" | "output-name" | "field-select" | "extent" | "crs";
47
64
  export interface NgisUiSchemaChoice {
48
65
  value: string;
49
66
  label: string;
@@ -109,7 +126,26 @@ export interface NgisToolUiSchema {
109
126
  * rule `recipeVersion` carries in F14.6).
110
127
  */
111
128
  export declare const NGIS_UI_SCHEMA_VERSION: 1;
112
- /** `PASS | ADAPTABLE | UNKNOWN | FAIL`, passed through verbatim. */
129
+ /**
130
+ * `PASS | ADAPTABLE | UNKNOWN | FAIL`, passed through verbatim.
131
+ *
132
+ * **Stage 29 (F29.8) — `verdict` and issue code are INDEPENDENT axes.**
133
+ * `"ADAPTABLE"` is the same token for Stage 26's **CRS** adaptation and Stage
134
+ * 29's **kind** adaptation; `verdict` carries *what*, `issues[]` carries *why*,
135
+ * and **only the issue code distinguishes them**. A test that asserts nothing but
136
+ * `verdict === "ADAPTABLE"` passes on the CRS case (`binding-adaptable.json`
137
+ * `B02`) and proves Stage 26's code path rather than this stage's — which is why
138
+ * the predicate is `isKindAdaptableIssue` in
139
+ * `src/domain/models/dataset-kind.ts`, over the code and never over the verdict.
140
+ *
141
+ * The kind code is `KIND_ADAPTABLE`, severity `advisory`, `port` set, and **`fix`
142
+ * absent** — `NgisValidationIssueFix` means *"set this parameter to this value"*
143
+ * and there is no parameter that turns a kind adaptation on or off, so a client
144
+ * rendering a one-click fix here has invented an affordance the server cannot
145
+ * honour. (CRS differs: `ngis:outputCrs` is a real parameter.) `NgisCrsIssueCode`
146
+ * is already open, so the code needs no union edit. Rendering it is `S29-33`'s
147
+ * (W3), and the client never re-classifies a server verdict or severity (`G-U`).
148
+ */
113
149
  export type NgisBindingVerdict = "PASS" | "ADAPTABLE" | "UNKNOWN" | "FAIL";
114
150
  /** `READY | NEEDS_CONFIGURATION | NOT_EXECUTABLE`, passed through verbatim. */
115
151
  export type NgisExecutionVerdict = "READY" | "NEEDS_CONFIGURATION" | "NOT_EXECUTABLE";
@@ -130,9 +166,30 @@ export interface NgisBindingValidationEntry {
130
166
  verdict: NgisBindingVerdict;
131
167
  reason: string;
132
168
  geometry_type: string;
133
- srid: number;
134
- native_srid: number;
135
- working_srid: number;
169
+ /**
170
+ * **Stage 25 (F25.1):** the numeric triple `srid` / `native_srid` /
171
+ * `working_srid` is gone **from this interface**. It was one of the three SRID
172
+ * vocabularies that never met, and the only one that could tell a caller
173
+ * "unknown" — it spelled it `0`, which every consumer read as a CRS. A
174
+ * binding's CRS is the dataset's `crs: NgisCrsRef | null`.
175
+ *
176
+ * **CORRECTION OF RECORD (`S25-V255`, S25-32, W3).** This comment used to say
177
+ * the triple was gone "from this entry **and from the wire**". The second half
178
+ * was false when it was written and is still false: `validation_rest.py:72-74`
179
+ * and `binding_validation.py:183-185` send all three fields on both the REST
180
+ * and the gRPC leg to this day. The W3 kickoff marked them
181
+ * `[deprecated = true]` in `geoanalysis.proto`; removing them needs
182
+ * `binding_validation.py`'s `x-required-srid` path to lose its only CRS
183
+ * source, which is not a W3 change.
184
+ *
185
+ * The distinction matters because this file is **quoted verbatim** into the
186
+ * published contract reference at `/market/devkit/docs`: a wrong sentence here
187
+ * is a wrong sentence in the SDK documentation a plugin author reads. What is
188
+ * true is that **the client drops them on the floor** — nothing in this repo
189
+ * declares or reads them, so no frontend code path can be misled by a `0`.
190
+ * That is a client guarantee, not a wire fact, and the two are only the same
191
+ * sentence when the server has actually stopped sending.
192
+ */
136
193
  access_mode: string;
137
194
  executable: boolean;
138
195
  }
@@ -145,6 +202,19 @@ export interface NgisBindingValidation {
145
202
  unresolved_ports: readonly string[];
146
203
  errors: readonly string[];
147
204
  warnings: readonly string[];
205
+ /**
206
+ * **Stage 25 (F25.6), additive.** Typed per-port issues. The F14.9 string
207
+ * arrays above are unchanged and keep their meaning; this channel carries the
208
+ * `param`/`port` routing and the one-click `fix` a string cannot.
209
+ *
210
+ * This half and {@link NgisExecutionValidation.issues} are **one channel read
211
+ * at two levels**, exactly as the F14.9 string arrays are: a blocking entry
212
+ * here blocks the submit too. Reading only the other level was `S29-V234`.
213
+ *
214
+ * Optional because a server that has not shipped F25.6 omits it, and an
215
+ * absent `issues` is "the server said nothing", never "the server said fine".
216
+ */
217
+ issues?: readonly NgisValidationIssue[];
148
218
  }
149
219
  /** `ValidateToolExecutionResponse`, field-for-field, with `normalized_params_json`
150
220
  * replaced by the decoded `normalized_params` object (the one divergence). */
@@ -161,6 +231,15 @@ export interface NgisExecutionValidation {
161
231
  execution_mode: NgisExecutionMode;
162
232
  /** Decoded. The proto's `normalized_params_json` string is never sent to a browser. */
163
233
  normalized_params: Readonly<Record<string, string>>;
234
+ /**
235
+ * **Stage 25 (F25.6), additive.** See {@link NgisBindingValidation.issues}.
236
+ * `isSubmitBlocked` counts a `severity: "blocking"` entry here **or on that
237
+ * half**, in addition to F14.9's existing rule; the client never suppresses,
238
+ * downgrades or re-classifies a server issue (G-U) — and declining to gate on
239
+ * a severity the server declared is a suppression, which is why reading only
240
+ * this level was a defect (`S29-V234`) rather than a narrower reading.
241
+ */
242
+ issues?: readonly NgisValidationIssue[];
164
243
  }
165
244
  /** `POST /api/analysis/tools/{tool_id}/validate-execution` body. */
166
245
  export interface NgisValidateExecutionBody {
@@ -172,7 +251,32 @@ export interface NgisValidateExecutionBody {
172
251
  expected_scene_generation?: number;
173
252
  output_mode?: NgisOutputMode;
174
253
  target_resource_id?: string | null;
175
- target_srid?: number;
254
+ /**
255
+ * **Stage 25 (F25.12):** `target_srid` is removed. It was typed, forwarded to
256
+ * validation, and **set by no component** — dead plumbing, and its presence
257
+ * made a numeric SRID look like a supported way to ask for a projection. The
258
+ * replacement is F25.4's `outputCrs?: string` (a `NgisCrsRef` key, not an
259
+ * integer), added on the submit and validate bodies by the W2 session that
260
+ * owns `RunButton` / `run-data.ts`.
261
+ *
262
+ * **Added here by S25-23 (W2), which is that session.**
263
+ */
264
+ /**
265
+ * **Stage 25 (F25.4).** The `ngis:outputCrs` platform parameter, as an
266
+ * `NgisCrsRef` **key** (`"EPSG:32650"`, `"custom:<ulid>"`).
267
+ *
268
+ * **This one field is camelCase in an otherwise snake_case body, and that is
269
+ * the server's spelling, not a slip.** The W2 dispatch is explicit: the proto
270
+ * carries `output_crs` on `SubmitTaskRequest` / the validate messages, and the
271
+ * REST legs take `outputCrs`. Re-spelling it here to match the neighbours
272
+ * would put a field on the wire that `ngis-geoanalysis` does not read — which
273
+ * is exactly the failure `target_srid` had, arriving forever and meaning
274
+ * nothing.
275
+ *
276
+ * Absent means "compute in the primary input's own CRS" (R4). The client never
277
+ * substitutes a value for an absent one (G-CRS).
278
+ */
279
+ outputCrs?: string;
176
280
  }
177
281
  /** `POST /api/analysis/tools/{tool_id}/validate-bindings` body. */
178
282
  export interface NgisValidateBindingsBody {
@@ -184,7 +184,44 @@ export interface PackageManifestDto {
184
184
  homepage?: string;
185
185
  publisher?: string;
186
186
  changelog?: string;
187
+ /**
188
+ * Semantic tool requirements declared by a UI plugin. The alias is local to
189
+ * this manifest; only the host receives the resolved, opaque implementation
190
+ * id at runtime. A bundle must therefore bind a tool through its alias, never
191
+ * through a UUID copied from the tool catalogue.
192
+ */
193
+ toolDependencies?: NgisToolDependencyDeclaration[];
194
+ }
195
+ /** Stable, publisher-owned identity for a tool dependency. */
196
+ export interface NgisToolDependencyReference {
197
+ namespace: string;
198
+ toolKey: string;
199
+ contractRange: string;
200
+ }
201
+ /** Optional structural contract that narrows a semantic dependency further. */
202
+ export interface NgisToolDependencyRequiredContract {
203
+ inputs?: Record<string, string>;
204
+ outputs?: Record<string, string>;
205
+ parameters?: string[];
187
206
  }
207
+ export interface NgisToolDependencyDeclaration {
208
+ /** Locally unique manifest key, used by plugin code at runtime. */
209
+ alias: string;
210
+ ref: NgisToolDependencyReference;
211
+ requiredContract?: NgisToolDependencyRequiredContract;
212
+ }
213
+ /** Resolver outcomes are intentionally specific enough to render an honest UI. */
214
+ export type NgisToolDependencyUnavailableReason = "tool_missing" | "tool_disabled" | "tool_forbidden" | "tool_version_unavailable" | "tool_contract_incompatible" | "dependency_unavailable";
215
+ export type NgisResolvedToolDependency = {
216
+ available: true;
217
+ toolId: string;
218
+ toolRevision: number;
219
+ contractVersion: string;
220
+ schemaFingerprint: string;
221
+ } | {
222
+ available: false;
223
+ reason: NgisToolDependencyUnavailableReason;
224
+ };
188
225
  export interface ExtensionInstallDto {
189
226
  extId: string;
190
227
  enabled: boolean;
@@ -267,6 +304,12 @@ export interface BootExtensionEntryDto {
267
304
  manifest: PackageManifestDto;
268
305
  bundleUrl: string;
269
306
  integrity: string;
307
+ /**
308
+ * Per-viewer resolution of `manifest.toolDependencies`. This is host data,
309
+ * not bundle configuration: `toolId` is deliberately never written back into
310
+ * the manifest or exposed as an authoring-time identifier.
311
+ */
312
+ dependencies?: Record<string, NgisResolvedToolDependency>;
270
313
  /**
271
314
  * The resolved version's declared scopes — a projection of
272
315
  * `manifest.permissions` (Stage 13 XB1), where an absent key and `[]` mean
@@ -83,7 +83,7 @@ export type GisToolId = "draw-point" | "draw-line" | "draw-polygon" | "draw-rect
83
83
  * command catalog; host-only composite controls can ride on the rail without
84
84
  * becoming invocable map tools.
85
85
  */
86
- export type GisRailItemId = GisToolId | "multi-view" | "flow-field";
86
+ export type GisRailItemId = GisToolId | "multi-view" | "smart-plotting";
87
87
  /** How a tool behaves when invoked from the rail. */
88
88
  export type GisToolKind = "mode" | "measure" | "action" | "dropdown";
89
89
  /** One-shot actions the rail fires on the map via the imperative handle
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@ngis/plugin-sdk",
3
- "version": "0.4.0",
4
- "description": "Types-first SDK for authoring NeoGIS map plugins.",
5
- "license": "Apache-2.0",
3
+ "version": "0.4.1",
4
+ "description": "Types-first SDK for authoring NGIS map plugins (DevKit, Stage 11 E3). Distributed as an immutable-per-version tarball from ngis-extension (GET /api/extensions/sdk/{version}.tgz) \u2014 see the authoring guide.",
5
+ "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "type": "module",
8
- "sideEffects": false,
9
8
  "main": "./dist/index.cjs",
10
9
  "module": "./dist/index.mjs",
11
10
  "types": "./dist/index.d.ts",
@@ -19,27 +18,6 @@
19
18
  "files": [
20
19
  "dist"
21
20
  ],
22
- "engines": {
23
- "node": ">=20"
24
- },
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/GeoScenarioScripter/NeoGIS-Frontend.git",
28
- "directory": "packages/plugin-sdk"
29
- },
30
- "homepage": "https://github.com/GeoScenarioScripter/NeoGIS-Frontend#readme",
31
- "bugs": {
32
- "url": "https://github.com/GeoScenarioScripter/NeoGIS-Frontend/issues"
33
- },
34
- "keywords": [
35
- "neogis",
36
- "gis",
37
- "plugin",
38
- "sdk"
39
- ],
40
- "publishConfig": {
41
- "access": "public"
42
- },
43
21
  "scripts": {
44
22
  "build": "node scripts/build.mjs",
45
23
  "prepack": "npm run build"
package/LICENSE DELETED
@@ -1,13 +0,0 @@
1
- Copyright 2026 NeoGIS contributors
2
-
3
- Licensed under the Apache License, Version 2.0 (the "License");
4
- you may not use this file except in compliance with the License.
5
- You may obtain a copy of the License at
6
-
7
- https://www.apache.org/licenses/LICENSE-2.0
8
-
9
- Unless required by applicable law or agreed to in writing, software
10
- distributed under the License is distributed on an "AS IS" BASIS,
11
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- See the License for the specific language governing permissions and
13
- limitations under the License.
package/README.md DELETED
@@ -1,31 +0,0 @@
1
- # @ngis/plugin-sdk
2
-
3
- The public, types-first authoring contract for NeoGIS map plugins.
4
-
5
- ```bash
6
- npm install --save-dev @ngis/plugin-sdk
7
- ```
8
-
9
- ```ts
10
- import { defineNgisPlugin, type NgisPlugin } from "@ngis/plugin-sdk";
11
-
12
- const plugin: NgisPlugin = defineNgisPlugin({
13
- manifest: {
14
- id: "com.example.my-plugin",
15
- name: "My plugin",
16
- version: "0.1.0",
17
- minNgisVersion: "0.4.0",
18
- capabilities: {},
19
- },
20
- });
21
-
22
- export default plugin;
23
- ```
24
-
25
- Plugin builds must leave `@ngis/plugin-sdk` external. The running NeoGIS host
26
- provides the runtime through its plugin import map. The npm package supplies the
27
- real public declarations plus small host-delegating shims; bundling it into a
28
- plugin would create a runtime copy that is disconnected from the host.
29
-
30
- The package version matches `NGIS_MAP_API_VERSION`. Use
31
- `create-ngis-plugin` for a complete buildable starter.