Finalize layertype pluginization: attachments, capabilities, config/filter/time surfaces, extends #115 - #1036
Merged
Merged
Conversation
…ng button - Remove .searchBarExpanded blue border/box-shadow on panel open - min-width 280px → 293px (+13px) - .searchTimeWarning align-items: center (vertically centers button) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…der, +7px wider - .searchCompactBar gets permanent 2px transparent bottom border to prevent layout shift when focus/active state adds the accent color - .searchBarFocused (panel open) applies same tint+underline as active - min-width 293px → 300px (+7px) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…range - Add maybeShowTimeWarning helper that queries geodataset without time bounds, compares result extent to current slider, only shows warning if features exist outside the range - Fix case-sensitive regex search missing start_time/end_time columns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…ime warning Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Adds searchGeneration ref that increments on each handleSearch call. tryHighlight and trySelect check the generation and bail if a newer search has started — prevents re-selecting a previous feature. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
This reverts commit 3f9554b.
When a value only exists in geodataset layers, doWithSearch was still running on all vector layers in the group. If a vector layer happened to have a match (e.g. same search text), it would re-select that feature — overriding the resetLayerFills() and showing a stale selection. Now filters vecLayers by sourceLayers (same as geoSearchTargets). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
When toggling geodataset layers on, addGeoJSONData / updateVectorLayer re-selects L_.activeFeature from a previous search — causing stale highlights. Clearing activeFeature at the start of select mode prevents layer reloads from re-selecting the old feature. Also filters vector layers by sourceLayers in group select mode so doWithSearch only runs on layers that actually have the clicked value. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…e, multi-field search, test corrections, variable naming Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
… query Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…-search feat: Global Feature Search — unified panel, field search, operator selection, layer management
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…lowercase fix: lowercase COG colormap_name before passing to TiTiler
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…ution Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Four surfaces the eight authoring experiments asked for, each replacing something core was deciding on their behalf: - layertype `source.fetch(layerObj, ctx) -> GeoJSON`, dispatched from LayerCapturer for every acquisition. Core keeps the whole dynamic-extent policy (extent, debounce/settling, zoom gate, move threshold, request staleness, clear/update, reload subscribers) — extracted out of the two duplicated branches into capture/dynamicExtent.js so it is testable — so `fetch` is a pure "given this, give me GeoJSON" for data that doesn't come out of a url core can fetch (POST bodies, headers, pagination, an SDK). - layertype `legend.derive(layerObj)`, replacing the `['image','tile'].includes(type) && cogTransform || type === 'velocity' || (type === 'data' && shader === 'colorize')` list that both the LayersTool and the LegendTool carried. Tile/Image/Velocity/Data now declare the surface. - attachment `onConfigChange(ctx)`, with `mmgisAPI.setLayerAttachmentConfig()` as its dispatcher: core writes the new settings into the host's live config at the declared configPath, then lets the attachment retune in place rather than paying for the default host rebuild. - interaction `configPath`, so an interaction with per-layer settings is handed its own subtree as `ctx.config` instead of digging through layerData. Also: `raw` (the engine namespace) on both globe contexts, matching `mctx.raw`; `test:plugins:unit` no longer fails when no plugin ships a @Unit spec. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Eight plugins were written against this branch by separate agents; these are the things they had to guess at or work around. - The layertypes README still said data acquisition is not in the vocabulary and belongs in `make`, 75 lines above the `source` surface that replaced that advice. - `gctx.raw` was absent from the gctx table, which still sent globe authors to `gctx.renderer` for engine-specific work. Documented, including that the two engines' namespaces are symmetric in name only. - Globe operations are dispatched by layer *name* (only `make` gets a layer object), and `setVisibility`/`setOpacity` take the new value positionally. None of that was written down; two authors guessed. - Nothing said how a layer opts into dynamic extent, so `source.fetch` saw a null `ctx.view` with no explanation. - `legend.derive` is handed config, not data; say so, and where render state actually lives, instead of leaving authors to stash on the layer. - `onConfigChange` is passed the built attachment, which the ctx table omitted. - The layertype scaffold's own test asserted `modules.globe[key]` over `Object.keys(renderers.globe)`, so it failed for every globe layertype, and it assumed `modules` where a single-module `extends` plugin has `module`. - The scaffold and the attachment README both captured `window.L` at import time, which is undefined in the unit test the same scaffold ships. Read the global per call instead, and drop `F_` from the example: importing an MMGIS singleton pulls jQuery and makes a module un-importable in Node, which browser-globals now states rather than implying it protects against that. - `create` printed `npm run build` for interactions instead of `activate`, said nothing when a name collided with an existing plugin in another family (a `Curtain` layertype silently shadowed the `Curtain` tool), and left the author to discover that a non-core container is untracked here. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
A plugin whose main setting is written in another language — the Overpass QL query one of the authoring experiments built its layertype around — got a single-line text field, because every free-text component Maker renders is one. `textarea` is multiline and monospace, sized by an optional `rows`. The component vocabulary itself was only discoverable by declaring something invalid and reading the validator's error, so plugins/README.md now has the metaconfig section that error was standing in for: what a row is, what each component field does, and what all 23 component types render. Also: the interaction runner assigned `ctx.config` only for interactions that declare a `configPath`, so an interaction without one was handed whichever config the interaction before it in the pipeline had. It now gets null, which is also what a layer that was never configured yields — the previous test asserted the leak, so it changes with it. For the globe, `gctx.raw` on LithoSphere is the globe class and nothing else (the package exports one thing), so the README now says where the work actually happens: `renderer` for the live globe and `window.THREE` for geometry, which MMGIS vendors and puts on the window. An author looked for THREE on `raw`, didn't find it, and dropped LithoSphere support. `create layertype` scaffolds a standalone renderer, which is also now called out as the wrong starting point for a type that draws like one MMGIS already has: extend it instead. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
An interaction could be handed `ctx.config` but had no way to declare what an admin should be able to put there, so its settings existed only as prose in a README. It now declares `config.rows` in the same metaconfig vocabulary an attachment uses, and Maker renders those rows on the interaction's card in a layer's Interactions tab, behind a gear — beside where the interaction was chosen and dragged, rather than in a tab of its own. Validation follows from where the form lives: rows need a `configPath` (the runner has nowhere else to read them back from), every `field` must sit inside it, and `tab`/`tabs` are rejected. A row's `default` remains a form default — nothing is written until an admin touches the field — so plugins still default their own values, which the scaffold and README now spell out. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Closing a layer modal rebuilds the layer from the fields its layer type's tabs declare, which is why attachment config paths were already exempted. An interaction's settings come from its own manifest, so they were written, rendered, and then silently dropped on close. Interaction manifests now load into the store alongside the attachment ones, and their config paths are preserved by the same pass. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
No core interaction has per-layer settings, so the fixture interaction is served in place of the generated registry — a static JSON file, so no plugin has to be installed to stand in for one. Fails without the layer modal preserving interaction-owned config paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Eight fresh authors built a plugin each against this branch's docs. The
contracts held; the tooling around them did not.
- `create FOVWedges` emitted `f_ovwedges` and `fOVWedges` — an acronym is one
word now, and so is a word with a digit in it (`HTML5Parser`). The printed
next-steps filenames derive from the same tokens rather than re-deriving
them wrongly.
- The layerattachment scaffold defaulted to a Configure tab of its own, the
exact thing its README warns against; it now joins an existing tab, `validate`
warns when a non-core attachment's `config.tab` matches no core tab, and the
next steps say so.
- `create`'s activate diff buried what you just made under every plugin there
is on a first run; it summarises past 8 lines, `--verbose` to list them.
Two of core's own unit assertions failed for a *valid* third-party attachment:
an exact tab list, and a field walk that descended into `objectarray.object`,
whose item fields are relative and so are not paths under `configPath`. Both
keep their invariant (core's tabs present and in order, no duplicate tabs,
every real field owned by the attachment that reads it) while allowing an
installed plugin, with the third-party case asserted rather than assumed.
`enableWhenField` is `{ field, value }`; given a bare path Configure greys the
control out forever, so validation rejects it now.
Docs, all from things an author had to read core source to learn: an
interactions family README (the `ctx` table, phase/order, settings, and what
can't be unit tested); a `Time` section (the TimeControl surface, and that a
feature's timestamps live at the properties `time.startProp`/`endProp` name);
what the inherited vector renderer does with the features a `source.fetch`
returns, including `prop-<name>` styling; `objectarray`'s relative item fields,
`default` vs `defaultChecked`, and that a cleared number arrives as `""`; what
an attachment's returned object may carry and that `ctx.config` is never null in
`make`; and importing `L_` by alias in a globe module.
Also drops two overwritten no-op `TimeControl.subscribe`/`unsubscribe` stubs.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
A feature that needs a layer type *and* an attachment *and* an interaction was
buildable but had three ways to fail quietly, all found by building eight of them:
- `pluginDependencies` resolved against tools/backend/components/interactions
only, so an interaction depending on the layer type it was written for was
reported enabled, validated clean, and then left out of the generated registry.
Dependency discovery now covers every family, and the warning says the plugin
never loads instead of leaving that to be inferred.
- `validate --json` was unparseable whenever the manifest validator logged, since
it logs JSON lines to stdout. Captured and returned as warnings.
- The module validator accepted `export default { … }` but not the named form the
scaffolds and docs use, and matched examples inside comments.
Also: `syncData` is handed the attachment's `config` and `layerObj`, as `make`
already was, so a redraw need not stash them; stale-registry validation resolves
the imported module rather than its directory, which is what a type switching to
`extends` deletes; and `applicableLayerTypes`/`defaultInteractions` ids that no
enabled plugin provides are reported.
`create layertype --extends <typeId>` scaffolds what the README recommends and
the best plugins are — a manifest with `extends` and one module whose keys are
surfaces, starting at `source.fetch` — instead of a standalone renderer. The
parent is checked as you type it. Local `install` no longer copies `.git`, and
`--container` pins the container name, which plugin ids embed.
The interaction scaffold's spec tested the handler, which cannot be imported in
Node once it imports a singleton; the decisions now live in `logic.js` and that is
what it tests. Both other scaffolds emit a named default export, so a plugin
following them no longer starts with a lint warning.
Docs: the `_legend` entry shape, one container holding a whole feature and how its
plugins reach each other, attachment time (no op needed — `syncData`), `syncData`'s
context, calling your own backend under a subpath, `kindAlias` being an array,
`NODE_ENV` for eslint, and install/container identity.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
A feature is often a layer type plus an attachment plus an interaction, and the
type is the plugin that knows what the attachment should be. It could already
declare capabilities.defaultInteractions but had no way to say the same about an
attachment, so three separate plugin authors worked around it the same wrong
way: the type's own config rows wrote a string-literal copy of the attachment's
configPath, coupling two plugins through a path either could rename.
capabilities.defaultAttachments maps attachmentId to that attachment's
settings ({} for none), and core hands it to the attachment under the
attachment's own configPath as if an admin had filled the form in. A layer's own
settings sit on top field by field, so changing one thing doesn't lose the rest
and enabled: false opts out. It resolves through configFor/isEnabledOn, which
everything already went through, so no call site changes.
validate warns for a declared attachmentId no enabled plugin provides, and for
one whose applicableLayerTypes excludes the declaring type - a well-formed
default that would silently never apply.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…, empty form fields beat a type's defaults extends merged parent and child one level per *surface*, so a type adding a config.normalize silently lost its parent's config.expand — the opposite of 'declare only what differs'. Merging is now per operation (and per engine under globe), in a pure typeInheritance module. A layer's attachment settings also overrode a type's defaultAttachments with '' — which is what Configure writes into a row nobody filled in — leaving the attachment with no property name at all. Empty (nullish or '') no longer overrides; false and 0 still do. Plus the smaller traps: 'create tool XTool' produced XToolTool; activate reported 'No changes' after a manifest edit that rewrote a registry; ToolController_.getTool returned a silent no-op stub for a tool that isn't loaded; the backend scaffold had no models/ despite its README documenting one; and the docs said nothing about interaction-to-tool calls, stopGuests being mount-wide and status-blind, failure responses arriving as HTTP 200, layer types resolving defaultIcon against MUI rather than MDI, or how L_.layers.attachments is keyed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
capabilities.defaultInteractions took interaction ids only, so a type that
shipped an interaction had no way to tell it the property names it had just
fetched — the gap defaultAttachments closed for the other family, and four of
round five's authors worked around it by duplicating those names into a third
subtree or stashing them on the layer under a private key.
An event may now be an object instead of a list:
"click": { "wind:report": { "speedProp": "windSpeed" } }
Key order is the pipeline order, as the array's is; the settings resolve into
the interaction's own configPath on the way to ctx.config, with the layer's own
settings over them field by field. An interaction is written once and cannot
tell whether a type configured it or an admin did.
The merge rules are now shared with attachments (declaredConfig), validate
cross-checks both forms — including a declared interaction whose
applicableLayerTypes excludes the declaring type — and VectorTile's click, which
had been passing no layerTypeChain, now enforces applicability like every other
click path.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Round six's authors could author, validate, activate and register a new layer type and still not configure a layer with it: both config validators switched on the layer's type and called anything they did not recognise unknown. They ask the generated layer-type registry instead, and hold a plugin type only to the checks it declares for itself (a `source`-backed type may have no url) — while a type extending a built-in still gets its parent's field defaults, since it is drawn by the parent's renderer. Also from that round: a LithoSphere type that registers itself in gctx.layers now has its destroy/setVisibility/setOpacity dispatched rather than declared, validated and never called; a type that declares a map renderer and resolves none says so instead of loading and drawing nothing; and window.location exists in the unit-test browser globals, so a source can resolve a relative url under test. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Two surfaces round six asked for, both of which existed as core internals a
plugin had to reach into.
`ctx.refreshLayer()`, on every interaction and attachment: an interaction that
writes a feature to its own backend had to call
`L_.Map_.refreshLayer(layerObj, cb, skipOrderedBringToFront, stopLoops,
resolvedUrl)` to see its own write. The re-acquisition goes through the same
path every other trigger does, and a `source`-backed type now sees it as
`ctx.trigger === 'refresh'` rather than as another `'make'`.
`time.availability(layerObj, ctx)`: `capabilities.time.histogram` validated,
reported true, and meant nothing — TimeUI found histogram sources by matching
`stac-collection:` and `{t}` against the layer's url, so any other endpoint was
silently excluded. The type answers now, with times and counts; core keeps the
window, the binning and the drawing. Tile's two url schemes move into its time
module, and declaring the capability without a `modules.time` to implement it in
is a validation error.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Turning the Reference Mission's geodataset time-series layer on and then moving the time bar left the Layers tool showing it off, while it was on and drawn. The refresh swap hides the old layer and shows the rebuilt one with `ignoreToggleStateChange`, so `L_.layers.on` deliberately never changes \u2014 and both halves then compute the state to broadcast from that unchanged value and notify subscribers 'off' twice. Subscribers are what track on-ness (the checkbox; a dynamic-extent layer's re-query), so a toggle that preserves state now tells them nothing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…pdown options The four remaining findings from the round-six plugin authoring sessions. extends: a child's operation is handed the inherited implementation as one extra last argument, so overriding vector's `config.normalize` to add a field no longer silently drops the `kind`/`radius` it sets. Phases match by name, a bare function stays a bare function, and `inherited()` is a safe no-op when the parent has no such operation. source.fetch: `ctx.signal` (a layer's next acquisition aborts its last, and an AbortError is not logged as a failure), `ctx.emit(geojson)` for a paged source to draw what it has so far, and `ctx.resolveUrl` so a plugin with an endpoint in its own config doesn't hand-roll mission/ROOT_PATH resolution. Core keeps the extent, staleness, move threshold and rendering; an emit after the request is stale is dropped. Maker: a dropdown may name an `optionsFrom` provider instead of listing options — `layerProperties` (the layer's own feature property names, from the geodataset schema or by sampling the file), `layers`, `layerTypes` — so "pick a property of this layer's data" stops being an unchecked text field. Validated against the provider names Configure actually has. Docs: a components family README (when init runs and what exists by then, the z-index bands, the subscriptions, and why there is no core interaction → component channel), and why composing across *layers* is unsupported — with the supportable shape being one layer whose source.fetch acquires both inputs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…ids fail validate The layer modal's Layer Type control is a row in the *selected* type's own manifest, and every core manifest listed the eleven built-in ids literally — so a plugin type could be authored, validated, activated and registered, and then never be chosen by an admin. The row asks the registry instead (`optionsFrom: "layerTypes"`), which is also the only list that stays correct. Two owners of one `typeId`/`attachmentId` are refused by registry generation but were reported by `validate` as clean, and a failed generation leaves the previous registries in place — a green check followed by a build failure, or an app silently running the last generation. `validate` now reports them, as it already did for `interactionId`. Docs: the inherited-op example counted `inherited` from the wrong position (`config.normalize` is dispatched with one argument, not two), and the per-family `config` shape (a layer type declares `tabs`, everyone else `rows`) was stated for interactions only. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…ds from a type's declarations, and drop only the offender on activate - `ctx.acquire(layerName)` on source/interaction/attachment contexts: a configured layer's data, acquired through its own layer type, headlessly — not turned on, not drawn, no rendered state exposed, and the layer's own live acquisition untouched. - A type's `defaultAttachments`/`defaultInteractions` values may be `"$variables.…"` references read off the host layer, so the property an admin picks on the type's form isn't typed again on the other plugins'. An unanswerable path drops its key, leaving the plugin its own default; `$$` escapes a literal `$`. - `activate` leaves a duplicate-id or bad-`extends` plugin out of the registry and regenerates everything else, rather than aborting and silently leaving the previous generation of every registry in place. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
`npx playwright test` (no path argument) matched plugin-cli/scaffolds/*/tests/*.spec.js, which are templates a plugin is created from: their `__name__` placeholders and relative helper paths only resolve once copied into a plugin directory, so collection failed with six 'Cannot find module' errors before any test ran. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…rtype-plugins Finalize layertype pluginization: attachments, capabilities, config/filter/time surfaces, extends
tariqksoliman
requested review from
ac-61,
jdrodjpl and
jtroberts
and removed request for
jdrodjpl
August 5, 2026 23:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With Devin: JPL-Devin#115
Purpose
#112 moved map-render dispatch behind
LayerTypeRegistry/LayerInterface, but layer-type-specific behavior was still spread across core: ~94 comparisons of a layer'stypeagainst built-in ids, globe engine configs built inline in two duplicated blocks invisibility.js, filterer selection keyed by type, and all seven layer attachments constructed by ~1140 lines of hardcoded core code. This finishes the job: a layer type (or attachment) owns everything specific to it, core only dispatches, and manifest capabilities answer the classification questions core must ask while iterating every layer.The invariant is now enforced by a test rather than by convention: no file under
src/may compare a layer's or attachment'stypeagainst a built-in id, or single out a built-in attachment by name (.pairings,sub === 'image_overlays',['models'].includes(sub)).Proposed Changes
Layer types own their behavior
setVisibilitynow receives a real context ({visible, wasNeverOn, firstTimeOn, hadToMake, globeOnly, source, ...}), plusonToggleas sugar forsetVisibility.after, so the per-type toggle work (Vector's time-filter + initial-filter submit, Velocity's remake, Video's mute, Image's recolor) lives inplugins/core/layertypes/*/map/*.js.visibility.jsno longer builds LithoSphere/Cesium configs or callslitho.addLayer— each type hasglobe/config.js+globe/{cesium,lithosphere}/*.jsand goes throughGlobeRenderer. The two duplicated core blocks had already diverged; where they disagreed the toggle path won (addVisiblewas applying tile-onlythroughTileServer/COG:URL rewriting tomodel/3dtiles).config,filterandtimesurfaces: STAC expansion /kind/throughTileServernormalization and URL resolution (config),LocalFilterer/GeodatasetFilterer/ESFiltererselection (filter), and time availability + WMS param stamping (time).extends(one level) so a type inherits every surface and capability it doesn't define."module": "./x.js"exporting{map, globe, config, filter, time}. Every op exceptmakehas a correct-but-generic core default, so a working type can be one file.capabilitiesfields for the questions core asks while partitioning all layers —structural,map.stacking/redrawOnReorder/tracksLoad/refreshByRemake/stacEndpoint/picking/styling,time.histogram,defaultInteractions— replacing the type lists in ordering, picking, header handling and TimeUI eligibility.capabilities.time/filtering/identifywere declared in all 11 manifests and read nowhere; they're consumed now.resolve-plugin-deps.jsandcheckPluginDependenciesskipped both containers).expandLayersrecursed into sublayers without awaiting itself. Latent upstream, but fatal onceconfig.expandmade the loop async:dataFlatwas still filling whenMap_.initran and only 4 of 66 Reference Mission layers were made.type === 'point'check inresetLayerFills.pointwas avectoralias whosemakeLayercase was dropped ineb11ff87, so such a layer has never enteredL_.layers.layersince.Attachments own their construction
[CHANGE] All seven attachment constructors moved out of
LayerConstructors.js(−1692 lines) into their plugins'make. Core'sconstructSublayersnow asksLayerAttachmentRegistrywhich attachments apply to the host's type, in which order, and which need their siblings:[ADD] Attachment ops for the behavior that was left in core:
syncData(UncertaintyEllipses' globe teardown, ImageOverlays' z-index),setStyle(CoordinateMarkers' highlight restyle),setOpacity(UncertaintyEllipses' scaled stroke/fill, ImageOverlays' DOM opacity),onPeerToggle(Pairings reacting to a layer it watches), andpeerFeaturesFor.[ADD] Host metadata in each attachment manifest: render
order,sublayerKey(themodelattachment is still stored under the legacymodelskey),buildsAfterSiblings(labels).[CHANGE]
Photosphere.jsno longer reads.pairingsstate or computes pair az/el itself. It asksL_.getPeerFeatures(layerName, feature, {originOffset}), which dispatches to whichever attachment implementspeerFeaturesFor, and plots what comes back.[CHANGE] The gradient-polyline globe add/remove (including the in-flight-build guard) moved from
sublayers.jsinto the PathGradient plugin;gradient_polylineremains aGlobeRendererprimitive kind.Attachments own their settings, their click-mode half and their identity
configPathin every attachment manifest (e.g.image_overlays→variables.markerAttachments.image). An attachment's id, its key on the host and its config key are three different names; declaring the third lets core answer "does this host want this attachment?" once instead of seven copies ofF_.getIn(...)+enabled === true || enabled == null.pointToLayer. It decorates its host instead of building a sublayer, so it declareshost.decoratesHostand implementsdecorateFeature/globeStylein place ofmake— and consequently never appears in the host's Composite Layers list.applicableLayerTypesis resolved through one level ofextends, and the five attachments that declared no hosts now declare["vector","query"]— the only hosts that have ever constructed attachments. Configure therefore stops offering attachment settings onvectortileandmodellayers, where nothing was ever built; values already saved there are preserved on save rather than trimmed.configblock in every attachment manifest, replacing the fourAttachment - *tab-sets copy-pasted into the Vector, VectorTile, Query and Model metaconfigs (three of which had already drifted). Configure fetcheslayerAttachmentConfigs.jsonand composes the rows into the same four tabs, so a third-party attachment gets settings UI without editing any layer type.WaypointImage/WaypointModelno longer reimplement the image/model config with their own defaults next to the attachment's. They callL_.makeFeatureAttachment(id, ...), and the plugin owns theshow: 'click'half exactly as it owns the always-on half; deselection isL_.clearFeatureAttachments()instead of core knowing the two temp-layer names.description(per-instancetitlestill overrides), so fixing a description fixes the UI.Enforcement
tests/unit/layerTypeBranchGuardrail.spec.js— no built-in type comparison and no built-in attachment singled out by name undersrc/. Geometry types ('Point'),time.type, filter value types and menu-item types are inherently out of scope since it only inspects layer/attachmenttypecomparisons.tests/unit/layerTypeInheritance.spec.js, andupdateLayerAttachments.spec.jsassertions that every attachment has a validmake, declares a unique host order, and keeps its legacy sublayer key.pluginValidation.jsvalidates per surface (abuildConfigin a map module is an error), enum-checks the new capability fields, and requiresmakeon an attachment — except a host decoration, which has nothing to make.tests/unit/layerAttachmentTabs.spec.js— every field the composed Configure tabs write lives under some attachment's declaredconfigPath, a type inherits its parent's attachment settings, and a host no attachment applies to gets none.Authoring: docs, scaffolds and a loud contract
The code grew four new surfaces, a capability vocabulary and a whole family; the authoring materials still described #112's render contract, so the safe path for a new plugin was "copy an existing one".
plugin-cli/is a root-level directory (cli.js,lib/,scaffolds/<type>/), leaving/pluginsfor containers andplugin-state.jsononly.registries.jsonis CLI config and moves with it; the server still reads state from/plugins, so discovery is untouched.npm run plugins -- <cmd>is unchanged.plugin-cli/scaffolds/<type>/instead of ~500 lines of string arrays inside the CLI.createcopies the tree, substituting the name into paths and contents (__Name__,__name__,__flatname__,__snake_name__,__colon_name__). Being valid JSON/JS as they sit, they go through the same validator as any plugin — a broken scaffold now fails tests rather than someone's first plugin.create layerattachment, andplugins/core/layerattachments/README.mdwith the contract, every op and the core default it replaces, the capability table and a worked example. It was the one family with neither a scaffold nor a README.map.stackingon a map-rendering type,host.orderon a non-decoration attachment) warns. These previously failed as a silently mis-ordered or un-pickable layer.make(plusdestroyfor a type) and list the rest as commented ops with the default each would replace — an emptysetOpacitysilently overrides a working core default, so over-implementation is the failure mode worth designing against.config/filter/timesurfaces documented as contracts in the layertypes README, and thecapturesurface removed from it (it never existed). Same for theonHostToggleattachment op — no runtime dispatch, and a toggled host already reaches its attachments assetVisibility.loadop (superseded by thesourcesurface below). It was inLAYER_OPS, the validator, the README table and the scaffold, but core never dispatched it and no type implemented it — data acquisition, including the dynamic-extent refetch on pan/zoom, is core's (Layers_/capture/LayerCapturer), and it is deliberately staying there rather than becoming a per-type op. The op table also omittedrenderandonToggle, which core does dispatch.Vector/map.js,config.js,filter.js,globe/{cesium,lithosphere}.js, non-surface helpers underlib/. The leaf name repeated the plugin directory (Vector/map/vector.js), andglobe/config.js— the shared engine-neutral layer descriptor, nowglobe/layerConfig.js— read as the formalconfigsurface one directory away. Manifest keys are unchanged, since the surface comes from the key and never the path. The deadexport defaults in those helpers are gone.create's valid types and the discovery directories were two hand-kept lists, so the help text still advertised five families afterlayerattachmentlanded; both now derive from one map.{ before, main, after }, plusafterCommitonmake) as a comment beside its baremake. A bare function is sugar for{ main: fn }, which made Vector's phase object look like a different contract.LayerAttachmentRegistry.withOpstill read the pre-renamepaths.pluginkey, so it matched no attachment and clearing a clicked overlay/model silently did nothing. A test now covers both sides of that generated boundary, since a mismatch there fails as a no-op rather than an error.src/pre/*.js,configure/public/*Configs.json— single copies in the repo) now hold a lock across generate-then-assert. A worker running the CLI could otherwise regenerate away a fixture another worker had just written and was asserting on: ~1 run in 3 locally, invisible on CI, which uses one worker.Four surfaces the authoring experiments asked for
Eight throwaway plugins were written against this branch by separate agents (a globe extrusion renderer, an OGC API Features source, a time-series attachment, an interaction, a backend, a Configure-facing type, …). None had to touch core. What they did hit was core deciding something on their behalf with no way to participate:
[ADD] layertype
source.fetch(layerObj, ctx) -> GeoJSON, dispatched fromLayerCaptureron every acquisition (initial make, refresh interval, time requery, dynamic-extent pan). A url string can't express a POST body, auth headers, pagination or an SDK call, so a source that isn't "GET this url" had no home. Core keeps the entire dynamic-extent policy — extent (map or globe), debounce/settling, zoom gate, move threshold (including the/zsuffix), request staleness, clear-then-update, reload fan-out — which is why this is not theloadop returning: the two duplicated ~150-line branches incaptureVectorare now one core path pluscapture/dynamicExtent.js, andfetchis a pure "given this config and this ctx, give me GeoJSON".[ADD] layertype
legend.derive(layerObj), replacing this, carried in both the LayersTool and the LegendTool (twice, in the second):Configured legends (
legendurl,variables.legend) stay core's. This is for a legend that is the render — a single-band COG's colormap over its rescale range, a shader's ramp, a velocity magnitude scale — which only the type knows it has.Tile,Image,VelocityandDatadeclare it;derivereturningfalsemeans "not this layer after all".[ADD] attachment
onConfigChange(ctx)andmmgisAPI.setLayerAttachmentConfig(layerName, attachmentId, config). Retuning a live attachment (a gradient's ramp, a label's property) had no path at all: core writes the new settings into the host's live config at the attachment's declaredconfigPath— so everything that reads settings, the attachment included, sees them — then dispatches. The default is the blunt but always-correct host rebuild, so declaring the op is an optimization rather than a requirement.[ADD] interaction
configPath, so an interaction with per-layer settings is handed its own subtree asctx.configinstead of knowing where inlayerDatait lives — the same declaration attachments already make.[ADD]
raw(the engine namespace) on both globe contexts, matchingmctx.rawon the map. A globe module could reach Cesium/LithoSphere only by importing them itself.[FIX]
test:plugins:unitfailed with "No tests found" whenever no plugin shipped a@unitspec — which is every state of this repo, since the tagged specs only exist in generated plugins.Issues
Testing
npm run test:unit: 1033 passed (was 967; the new ones cover capability validation, theconfig.rowsmetaconfig schema, stale-registry detection, every scaffold generating a plugin that validates, the attachment op lookup below, and the four new surfaces — the dynamic-extent policysource.fetchsits inside is tested directly intests/unit/layerSource.spec.js: staleness, thresholds,/zscaling, ctx construction).npm run test:ci: passing.npm run plugins -- validate: all 58 plugins valid (1 pre-existing dependency warning forChemistryUse→ChemistryTool).npm run buildand the Configure build: compiled successfully. ESLint on changed files: no new errors (the 7'L' is not definedinPhotosphere.jsare pre-existing and unchanged onorigin/development).origin/development(feature on :8889, base on :8891) via scripted browser runs:sublayerType, child count, globe layer ids): byte-identical output on both branches for uncertainty ellipses, labels, coordinate markers, image overlays, path gradient (2D + 3D) and pairings.getPeerFeaturesreturns the same paired-layer and peer counts as recomputing from the attachment record directly.