From ede21454a292b346d791d17070b6510372a9ecce Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Fri, 9 Oct 2026 21:37:53 +0200 Subject: [PATCH] Draw offline hiking trails sharing a path as parallel lanes Several marked routes running down one path drew as a single line, only the topmost color visible. maplibre-gl-lanes puts each route in its own lane; OSM relations already share exact vertices, which is all it needs. One LaneLayer across every picked country; click hit-testing and the highlight move to queryLane/setHighlight. --- docs/third-party-notices.adoc | 14 + public/js/hiking_trails.js | 154 +- public/vendor/maplibre-gl-lanes/LICENSE | 21 + .../maplibre-gl-lanes/maplibre-gl-lanes.mjs | 3557 +++++++++++++++++ 4 files changed, 3674 insertions(+), 72 deletions(-) create mode 100644 public/vendor/maplibre-gl-lanes/LICENSE create mode 100644 public/vendor/maplibre-gl-lanes/maplibre-gl-lanes.mjs diff --git a/docs/third-party-notices.adoc b/docs/third-party-notices.adoc index 0e7b429..301960f 100644 --- a/docs/third-party-notices.adoc +++ b/docs/third-party-notices.adoc @@ -51,6 +51,20 @@ overview's polar pan limit, but MapLibre 6's `Map` no longer exposes `.transform` at all — guarded to skip that (cosmetic) adjustment instead of throwing when it's absent. +Also vendored under `public/vendor/` — MIT-licensed, attribution +required (`LICENSE` included alongside): + +[cols="1,1,2"] +|=== +| Library | Version | Source + +| maplibre-gl-lanes (offline hiking trails, `public/js/hiking_trails.js` +— trails sharing a path drawn as parallel lanes); its `dist/*.mjs` +build, minus the trailing `sourceMappingURL` comment +| 1.3.0 +| https://github.com/c0nsumer/maplibre-gl-lanes +|=== + Also vendored under `public/vendor/` — MIT-licensed (© 2013 Panayiotis Lipiridis), attribution required (this notice satisfies that): diff --git a/public/js/hiking_trails.js b/public/js/hiking_trails.js index 455c159..bb92dc9 100644 --- a/public/js/hiking_trails.js +++ b/public/js/hiking_trails.js @@ -2,6 +2,7 @@ import { WALKING_ICON } from "/js/icons.js"; import { firstSymbolLayerId } from "/js/radar.js"; import { escapeHtml } from "/js/gpx.js"; import { lineLength, formatDistance } from "/js/measure.js"; +import { buildLineGraph, orderLanesAsync, LaneLayer } from "/vendor/maplibre-gl-lanes/maplibre-gl-lanes.mjs"; // Overpass has permissive CORS (verified: Access-Control-Allow-Origin: *), // so this queries it directly from the browser — no server-side proxy @@ -40,22 +41,28 @@ const ATTRIBUTION = 'Trails © Waymarked Trails (CC-BY-SA), map data © OpenStreetMap contributors'; const OPACITY = 0.85; -// Offline: one real vector line layer per country the viewer actually -// picks, GeoJSON generated at build time by Maptool::HikingTrails straight +// Offline: real vector trails for each country the viewer actually picks, +// GeoJSON generated at build time by Maptool::HikingTrails straight // from the same Geofabrik .osm.pbf offline.pmtiles itself is built from // (see lib/maptool/hiking_trails.rb, PmtilesBuilder.extract_hiking_trails) // — no network needed, and no waiting on Overpass for click details either -// (queried locally via queryRenderedFeatures instead, see _handleClick +// (hit-tested locally via LaneLayer.queryLane instead, see _handleClick // below). One file per country rather than one combined file: a viewer // covering a dozen countries' worth of offline map data rarely wants all // of their hiking trails loaded at once, so this is opt-in per country // instead. -function offlineSourceId(code) { - return `hiking-trails-offline-${code}`; -} -function offlineLayerId(code) { - return `hiking-trails-offline-${code}-line`; -} +// +// Drawn with maplibre-gl-lanes rather than a plain line layer: a stretch +// of path several marked routes share (very common with KST blazes) is +// otherwise one line drawn over and over, only the topmost color visible. +// Lanes puts each route in its own parallel lane instead. It needs routes +// sharing a path to share exact vertices, which OSM relations over shared +// ways already do. One LaneLayer over every picked country, not one per +// country, so lanes at a border stay ordered together. Doesn't drape over +// 3D terrain (sinks under raised ground, warns on the console) — a known +// limitation of the library, not worth working around for an overlay. +const LANES_LAYER_ID = "hiking-trails-lanes"; +const LANE_SIZES = () => ({ spacing: 3.5, width: 3, casingWidth: 0.5 }); function offlineUrl(code) { return `/tiles/hiking_trails/${code}.geojson`; } @@ -76,11 +83,10 @@ function normalizedTrailColor(raw) { return /^#[0-9a-fA-F]{3,8}$/.test(trimmed) ? trimmed : DEFAULT_TRAIL_COLOR; } -// A highlight outline for whichever route(s) the last click matched — drawn -// as its own separate source/layer (real geometry, from either the offline -// vector feature or the online Overpass response) rather than a paint- -// property change on the base layer, since the online raster tiles have no -// paintable features to highlight at all. Same "yellow outline on a picked +// Online mode's highlight outline for whichever route(s) the last click +// matched — drawn as its own separate source/layer (real geometry, from the +// Overpass response), since the raster tiles have no paintable features to +// highlight at all. Offline lanes use LaneLayer.setHighlight instead. Same "yellow outline on a picked // route" treatment the earlier SVG prototype visualization used. const HIGHLIGHT_SOURCE_ID = "hiking-trails-highlight"; const HIGHLIGHT_LAYER_ID = "hiking-trails-highlight-line"; @@ -97,6 +103,9 @@ export class HikingTrailsControl { this._active = false; // the online-mode checkbox's own state this._offlineCountries = []; // codes with an extract available on the server this._selectedCountries = new Set(); // codes currently shown, offline mode + this._trailData = new Map(); // code -> fetched GeoJSON, kept across toggles/style swaps + this._routeInfo = new Map(); // String(relation id) -> feature properties, for the panel + this._lanes = null; // the current LaneLayer, null when no country is shown map.on("click", (event) => this._handleClick(event)); @@ -111,8 +120,8 @@ export class HikingTrailsControl { // here than try to carry it across a style swap. map.on("style.load", () => { if (this._active) this._addOnlineLayer(); - for (const code of this._selectedCountries) this._addOfflineLayer(code); this._clearHighlight(); + if (this._selectedCountries.size > 0) this._rebuildLanes(); }); } @@ -177,14 +186,56 @@ export class HikingTrailsControl { else this._removeOnlineLayer(); } - _toggleCountry(code, active) { - if (active) { - this._selectedCountries.add(code); - this._addOfflineLayer(code); - } else { + async _toggleCountry(code, active) { + if (!active) { this._selectedCountries.delete(code); - this._removeOfflineLayer(code); + this._rebuildLanes(); + return; } + this._selectedCountries.add(code); + if (!this._trailData.has(code)) { + let geojson; + try { + geojson = await (await fetch(offlineUrl(code))).json(); + } catch { + return; // degrade quietly, same story as every other optional layer here + } + // Normalized once here so an untrustworthy `colour` tag value never + // reaches the layer's color directly — same reasoning gpx.js's own + // feature.properties.color ||= TRACK_COLOR normalization gives. + for (const feature of geojson.features) { + feature.properties.color = normalizedTrailColor(feature.properties.colour); + } + this._trailData.set(code, geojson); + if (!this._selectedCountries.has(code)) return; // toggled back off while fetching + } + this._rebuildLanes(); + } + + // Rebuilt from scratch on every country toggle and style swap (setStyle + // drops custom layers too) — lane order depends on every route drawn, so + // there's no cheaper incremental path. Ordering runs in a worker; a newer + // rebuild started meanwhile wins, same token guard as _handleOnlineClick. + async _rebuildLanes() { + const build = (this._lanesBuild = {}); + const features = [...this._selectedCountries].flatMap((code) => this._trailData.get(code)?.features || []); + let graph = null; + if (features.length > 0) { + graph = buildLineGraph(features, { routeProperty: "id" }); + await orderLanesAsync(graph); + } + if (this._lanesBuild !== build) return; + + this._hidePanel(); + if (this._map.getLayer(LANES_LAYER_ID)) this._map.removeLayer(LANES_LAYER_ID); + this._lanes = null; + this._routeInfo = new Map(features.map((feature) => [String(feature.properties.id), feature.properties])); + if (!graph) return; + + this._lanes = new LaneLayer({ id: LANES_LAYER_ID, graph, sizes: LANE_SIZES, casingColor: "rgba(255, 255, 255, 0.8)" }); + // Below labels (same firstSymbolLayerId spot contour.js/radar.js use), + // so place names/POI icons stay readable on top of the trail lines. + this._map.addLayer(this._lanes, firstSymbolLayerId(this._map)); } _handleClick(event) { @@ -192,24 +243,20 @@ export class HikingTrailsControl { else if (this._active) this._handleOnlineClick(event); } - // The local vector layers have real features to hit-test directly — no - // network round-trip, so this resolves instantly. A genuine miss (empty - // result) just closes an already-open panel rather than showing a "not + // The lanes have real geometry to hit-test directly — no network + // round-trip, so this resolves instantly. Each route has its own lane, so + // a click picks exactly one, highlighted by the layer itself. A genuine + // miss just closes an already-open panel rather than showing a "not // found" message, same as clicking empty space anywhere else on the map. _handleOfflineClick(event) { - const layers = [...this._selectedCountries].map(offlineLayerId).filter((id) => this._map.getLayer(id)); - if (layers.length === 0) return; - - const features = this._map.queryRenderedFeatures(event.point, { layers }); - if (features.length === 0) { + const hit = this._lanes?.queryLane(event.point, 8); + const info = hit && this._routeInfo.get(String(hit.route)); + if (!info) { this._hidePanel(); return; } - - const matched = features.filter((feature, i) => features.findIndex((f) => f.properties.id === feature.properties.id) === i); - const html = matched.map((feature) => routeHtml(feature.properties)).join("
"); - this._setHighlight(matched.map((feature) => ({ geometry: feature.geometry, color: feature.properties.color }))); - this._showPanel(html); + this._lanes.setHighlight(hit.route); + this._showPanel(routeHtml(info)); } _showPanel(html) { @@ -277,44 +324,6 @@ export class HikingTrailsControl { if (this._map.getSource(ONLINE_SOURCE_ID)) this._map.removeSource(ONLINE_SOURCE_ID); } - async _addOfflineLayer(code) { - if (this._map.getSource(offlineSourceId(code))) return; - - let geojson; - try { - geojson = await (await fetch(offlineUrl(code))).json(); - } catch { - return; // degrade quietly, same story as every other optional layer here - } - // Toggled back off, or the base map switched, while this was fetching. - if (!this._selectedCountries.has(code) || this._map.getSource(offlineSourceId(code))) return; - - // Normalized once here (rather than a MapLibre expression in the paint - // property below) so an untrustworthy `colour` tag value never reaches - // a style property directly — same reasoning gpx.js's own - // feature.properties.color ||= TRACK_COLOR normalization gives. - for (const feature of geojson.features) { - feature.properties.color = normalizedTrailColor(feature.properties.colour); - } - - this._map.addSource(offlineSourceId(code), { type: "geojson", data: geojson }); - this._map.addLayer( - { - id: offlineLayerId(code), - type: "line", - source: offlineSourceId(code), - layout: { "line-cap": "round", "line-join": "round" }, - paint: { "line-color": ["get", "color"], "line-width": 2 }, - }, - firstSymbolLayerId(this._map), - ); - } - - _removeOfflineLayer(code) { - if (this._map.getLayer(offlineLayerId(code))) this._map.removeLayer(offlineLayerId(code)); - if (this._map.getSource(offlineSourceId(code))) this._map.removeSource(offlineSourceId(code)); - } - // features: an array of { geometry, color } — geometry a GeoJSON // LineString or MultiLineString (mixed is fine), color the trail's own // (already-normalized) colour — everything the last click matched, drawn @@ -342,6 +351,7 @@ export class HikingTrailsControl { } _clearHighlight() { + this._lanes?.setHighlight(null); if (this._map.getLayer(HIGHLIGHT_LAYER_ID)) this._map.removeLayer(HIGHLIGHT_LAYER_ID); if (this._map.getSource(HIGHLIGHT_SOURCE_ID)) this._map.removeSource(HIGHLIGHT_SOURCE_ID); } diff --git a/public/vendor/maplibre-gl-lanes/LICENSE b/public/vendor/maplibre-gl-lanes/LICENSE new file mode 100644 index 0000000..4ab58d6 --- /dev/null +++ b/public/vendor/maplibre-gl-lanes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Steve Vigneau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public/vendor/maplibre-gl-lanes/maplibre-gl-lanes.mjs b/public/vendor/maplibre-gl-lanes/maplibre-gl-lanes.mjs new file mode 100644 index 0000000..abe6a87 --- /dev/null +++ b/public/vendor/maplibre-gl-lanes/maplibre-gl-lanes.mjs @@ -0,0 +1,3557 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key != "symbol" ? key + "" : key, value); + +// src/core/graph.ts +function lngLatToMercator(lng, lat) { + let x = (lng + 180) / 360, s = Math.sin(lat * Math.PI / 180), y = 0.5 - 0.25 * Math.log((1 + s) / (1 - s)) / Math.PI; + return [x, y]; +} +function mercatorToLngLat(x, y) { + let lng = x * 360 - 180, lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180 / Math.PI; + return [lng, lat]; +} +function buildLineGraph(features, opts = {}) { + let routeProp = opts.routeProperty ?? "route", colorProp = opts.colorProperty ?? "color", nameProp = opts.nameProperty ?? "name", snap = opts.snapDegrees ?? 1e-7, uniform = opts.uniformProperties ?? [], routes = /* @__PURE__ */ new Map(), vertexPos = /* @__PURE__ */ new Map(), elem = /* @__PURE__ */ new Map(), adjacency = /* @__PURE__ */ new Map(), keyOf = (lng, lat) => { + let kx = Math.round(lng / snap), ky = Math.round(lat / snap); + return `${kx},${ky}`; + }; + for (let f of features) { + if (!f.geometry) continue; + let props = f.properties ?? {}, rid = props[routeProp]; + if (rid == null) continue; + let routeId = String(rid); + if (!routes.has(routeId)) { + let override = opts.routes?.[routeId] ?? {}; + routes.set(routeId, { + id: routeId, + color: override.color ?? props[colorProp] ?? "#888888", + name: override.name ?? props[nameProp] ?? void 0, + dash: override.dash, + dashColor: override.dashColor, + dashCap: override.dashCap, + casing: override.casing + }); + } + let lines = f.geometry.type === "LineString" ? [f.geometry.coordinates] : f.geometry.type === "MultiLineString" ? f.geometry.coordinates : [], uniformProps = uniform.length ? Object.fromEntries(uniform.map((k) => [k, props[k]])) : null; + for (let line of lines) { + let prevKey = null; + for (let c of line) { + let k = keyOf(c[0], c[1]); + if (vertexPos.has(k) || vertexPos.set(k, lngLatToMercator(c[0], c[1])), prevKey !== null && prevKey !== k) { + let forward = prevKey < k, ek = forward ? `${prevKey}|${k}` : `${k}|${prevKey}`, e = elem.get(ek); + if (!e) { + e = { u: forward ? prevKey : k, v: forward ? k : prevKey, routes: /* @__PURE__ */ new Map(), props: uniformProps, sig: null }, elem.set(ek, e); + for (let vk of [prevKey, k]) { + let s = adjacency.get(vk); + s || adjacency.set(vk, s = /* @__PURE__ */ new Set()), s.add(ek); + } + } + let dir = forward ? 1 : -1, prev = e.routes.get(routeId); + e.routes.set(routeId, prev === void 0 || prev === dir ? dir : 0); + } + prevKey = k; + } + } + } + let sigOf = (e) => { + if (e.sig === null) { + let routeSig = [...e.routes.keys()].sort().join("\0"); + e.sig = routeSig + "" + (e.props ? JSON.stringify(uniform.map((k) => e.props[k] ?? null)) : ""); + } + return e.sig; + }, junction = /* @__PURE__ */ new Map(), isJunction = (vk) => { + let is = junction.get(vk); + if (is === void 0) { + let inc = adjacency.get(vk); + if (inc.size !== 2) + is = !0; + else { + let [k1, k2] = inc; + is = sigOf(elem.get(k1)) !== sigOf(elem.get(k2)); + } + junction.set(vk, is); + } + return is; + }, nodeIndex = /* @__PURE__ */ new Map(), nodes = [], nodeFor = (vk) => { + let id = nodeIndex.get(vk); + if (id === void 0) { + let [x, y] = vertexPos.get(vk); + id = nodes.length, nodeIndex.set(vk, id), nodes.push({ id, x, y, ports: [], transitions: [] }); + } + return id; + }, edges = [], visited = /* @__PURE__ */ new Set(), walk = (startVk, firstEk) => { + let coordsKeys = [startVk], elemKeys = [], vk = startVk, ek = firstEk; + for (; ; ) { + visited.add(ek), elemKeys.push(ek); + let e = elem.get(ek), next = e.u === vk ? e.v : e.u; + if (coordsKeys.push(next), vk = next, isJunction(vk) || vk === startVk) break; + let inc = [...adjacency.get(vk)], nk = inc[0] === ek ? inc[1] : inc[0]; + if (visited.has(nk)) break; + ek = nk; + } + let first = elem.get(elemKeys[0]), a = nodeFor(coordsKeys[0]), b = nodeFor(coordsKeys[coordsKeys.length - 1]), coords = []; + for (let k of coordsKeys) { + let p = vertexPos.get(k); + coords.push(p[0], p[1]); + } + let id = edges.length, direction = /* @__PURE__ */ new Map(); + for (let [r, d] of first.routes) { + let walkForward = first.u === coordsKeys[0]; + direction.set(r, d === 0 ? 0 : walkForward ? d : -d); + } + let edge = { id, a, b, coords, routes: [...first.routes.keys()].sort(), direction, order: [] }; + first.props && (edge.properties = first.props), edges.push(edge), nodes[a].ports.push({ edge: id, end: "a" }), nodes[b].ports.push({ edge: id, end: "b" }); + }; + for (let vk of adjacency.keys()) + if (isJunction(vk)) + for (let ek of adjacency.get(vk)) + visited.has(ek) || walk(vk, ek); + for (let ek of elem.keys()) + visited.has(ek) || walk(elem.get(ek).u, ek); + let chains = deriveChains(nodes, edges, routes); + return { nodes, edges, routes, chains }; +} +function deriveChains(nodes, edges, routes) { + for (let n of nodes) n.transitions = []; + let chains = /* @__PURE__ */ new Map(), portIndex = (node, edge, end) => node.ports.findIndex((p) => p.edge === edge && p.end === end), outDir = (edge, end) => { + let c = edges[edge].coords, dx, dy; + if (end === "a") + dx = c[2] - c[0], dy = c[3] - c[1]; + else { + let n = c.length; + dx = c[n - 4] - c[n - 2], dy = c[n - 3] - c[n - 1]; + } + let l = Math.hypot(dx, dy) || 1; + return [dx / l, dy / l]; + }; + for (let rid of routes.keys()) { + let routeEdges = edges.filter((e) => e.routes.includes(rid)); + if (!routeEdges.length) { + chains.set(rid, []); + continue; + } + let unused = new Set(routeEdges.map((e) => e.id)), degree = /* @__PURE__ */ new Map(); + for (let e of routeEdges) + degree.set(e.a, (degree.get(e.a) ?? 0) + 1), degree.set(e.b, (degree.get(e.b) ?? 0) + 1); + let out = [], chainEndPorts = []; + for (; unused.size; ) { + let startEdge = null, startNode = -1; + for (let id of unused) { + let e = edges[id]; + if (degree.get(e.a) % 2 === 1 && (startEdge === null || startNode !== e.a && startNode !== e.b)) { + startEdge = e, startNode = e.a; + break; + } + if (degree.get(e.b) % 2 === 1) { + startEdge = e, startNode = e.b; + break; + } + } + if (!startEdge) { + let id = Math.min(...unused); + startEdge = edges[id], startNode = startEdge.a; + } + let steps = [], node = startNode, arriving = null, nextEdge = startEdge; + for (; nextEdge; ) { + let forward = nextEdge.a === node; + steps.push({ edge: nextEdge.id, forward }), unused.delete(nextEdge.id); + let far = forward ? nextEdge.b : nextEdge.a, farEnd = forward ? "b" : "a", d = outDir(nextEdge.id, farEnd); + arriving = [-d[0], -d[1]], node = far; + let best = null, bestDot = -1 / 0; + for (let p of nodes[node].ports) { + if (!unused.has(p.edge)) continue; + let o = outDir(p.edge, p.end), dot = o[0] * arriving[0] + o[1] * arriving[1]; + (dot > bestDot + 1e-9 || Math.abs(dot - bestDot) <= 1e-9 && best && p.edge < best.id) && (bestDot = dot, best = edges[p.edge]); + } + nextEdge = best; + } + let first = steps[0], last = steps[steps.length - 1], firstNode = first.forward ? edges[first.edge].a : edges[first.edge].b, lastNode = last.forward ? edges[last.edge].b : edges[last.edge].a, closed = steps.length > 1 && firstNode === lastNode, chain = { steps, closed }; + out.push(chain); + let stepEnd = (s) => ({ node: s.forward ? edges[s.edge].b : edges[s.edge].a, end: s.forward ? "b" : "a" }), stepStart = (s) => ({ node: s.forward ? edges[s.edge].a : edges[s.edge].b, end: s.forward ? "a" : "b" }), pairs = steps.length - (closed ? 0 : 1); + for (let i = 0; i < pairs; i++) { + let s = steps[i], t = steps[(i + 1) % steps.length], n = nodes[stepEnd(s).node]; + n.transitions.push({ route: rid, from: portIndex(n, s.edge, stepEnd(s).end), to: portIndex(n, t.edge, stepStart(t).end) }); + } + closed || (chainEndPorts.push({ node: firstNode, port: portIndex(nodes[firstNode], first.edge, stepStart(first).end), chain, end: "start" }), chainEndPorts.push({ node: lastNode, port: portIndex(nodes[lastNode], last.edge, stepEnd(last).end), chain, end: "end" })); + } + let seen = /* @__PURE__ */ new Set(); + for (let ce of chainEndPorts) { + let n = nodes[ce.node], own = n.ports[ce.port], arriveDir = outDir(own.edge, own.end).map((v) => -v), best = -1, bestDot = -1 / 0; + if (n.ports.forEach((p, i) => { + if (i === ce.port || !edges[p.edge].routes.includes(rid)) return; + let o = outDir(p.edge, p.end), dot = o[0] * arriveDir[0] + o[1] * arriveDir[1]; + dot > bestDot && (bestDot = dot, best = i); + }), best < 0) continue; + let key = `${ce.node}:${Math.min(ce.port, best)}:${Math.max(ce.port, best)}`; + seen.has(key) || (seen.add(key), n.transitions.push({ route: rid, from: ce.port, to: best }), ce.end === "start" ? ce.chain.mergeStart = n.ports[best].edge : ce.chain.mergeEnd = n.ports[best].edge); + } + chains.set(rid, out); + } + return chains; +} +function filterGraph(g, visible) { + let keepEdge = /* @__PURE__ */ new Map(), edges = []; + for (let e of g.edges) { + let routes2 = e.routes.filter(visible); + routes2.length && (keepEdge.set(e.id, edges.length), edges.push({ ...e, id: edges.length, routes: routes2, order: [], baseline: void 0, direction: new Map([...e.direction].filter(([r]) => visible(r))) })); + } + let nodes = g.nodes.map((n) => ({ + ...n, + transitions: [], + ports: n.ports.filter((p) => keepEdge.has(p.edge)).map((p) => ({ edge: keepEdge.get(p.edge), end: p.end })) + })), routes = new Map([...g.routes].filter(([r]) => visible(r))), chains = deriveChains(nodes, edges, routes); + return { nodes, edges, routes, chains }; +} + +// src/core/geometry.ts +function polylineLength(p) { + let len = 0; + for (let i = 2; i < p.length; i += 2) + len += Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]); + return len; +} +function reversed(p) { + let out = new Array(p.length); + for (let i = 0, j = p.length - 2; i < p.length; i += 2, j -= 2) + out[i] = p[j], out[i + 1] = p[j + 1]; + return out; +} +function startDirection(p) { + for (let i = 2; i < p.length; i += 2) { + let dx = p[i] - p[0], dy = p[i + 1] - p[1], l = Math.hypot(dx, dy); + if (l > 0) return [dx / l, dy / l]; + } + return [0, 0]; +} +function endDirection(p) { + let n = p.length; + for (let i = n - 4; i >= 0; i -= 2) { + let dx = p[n - 2] - p[i], dy = p[n - 1] - p[i + 1], l = Math.hypot(dx, dy); + if (l > 0) return [dx / l, dy / l]; + } + return [0, 0]; +} +function trimPolyline(p, fromStart, fromEnd, keepFraction = 0.2) { + let n = p.length / 2, seg = new Float64Array(Math.max(0, n - 1)), total = 0; + for (let i = 0; i < n - 1; i++) + seg[i] = Math.hypot(p[i * 2 + 2] - p[i * 2], p[i * 2 + 3] - p[i * 2 + 1]), total += seg[i]; + if (total <= 0) return p.slice(); + let maxCut = total * (1 - keepFraction); + if (fromStart + fromEnd > maxCut) { + let s = maxCut / (fromStart + fromEnd); + fromStart *= s, fromEnd *= s; + } + let startPt = pointAlongSegments(p, seg, fromStart), endPt = pointAlongSegments(p, seg, total - fromEnd), out = [startPt.x, startPt.y], acc = 0; + for (let i = 0; i < n - 1; i++) + acc += seg[i], acc > fromStart && acc < total - fromEnd && out.push(p[i * 2 + 2], p[i * 2 + 3]); + return out.push(endPt.x, endPt.y), out; +} +function pointAlongSegments(p, seg, dist) { + if (dist <= 0) return { x: p[0], y: p[1] }; + let acc = 0; + for (let i = 0; i < seg.length; i++) { + let s = seg[i]; + if (acc + s >= dist) { + let t = s > 0 ? (dist - acc) / s : 0; + return { x: p[i * 2] + (p[i * 2 + 2] - p[i * 2]) * t, y: p[i * 2 + 1] + (p[i * 2 + 3] - p[i * 2 + 1]) * t }; + } + acc += s; + } + return { x: p[p.length - 2], y: p[p.length - 1] }; +} +function simplify(p, tolerance) { + let n = p.length / 2; + if (n <= 2 || tolerance <= 0) return p.slice(); + let keep = new Uint8Array(n); + keep[0] = 1, keep[n - 1] = 1; + let tol2 = tolerance * tolerance, stack = [0, n - 1]; + for (; stack.length; ) { + let last = stack.pop(), first = stack.pop(), maxD = 0, idx = -1, ax = p[first * 2], ay = p[first * 2 + 1], bx = p[last * 2], by = p[last * 2 + 1]; + for (let i = first + 1; i < last; i++) { + let d = segmentDistanceSq(p[i * 2], p[i * 2 + 1], ax, ay, bx, by); + d > maxD && (maxD = d, idx = i); + } + maxD > tol2 && idx > 0 && (keep[idx] = 1, stack.push(first, idx, idx, last)); + } + let out = []; + for (let i = 0; i < n; i++) keep[i] && out.push(p[i * 2], p[i * 2 + 1]); + return out; +} +function simplifyRank(p) { + let n = p.length / 2, rank = new Float64Array(n); + if (n === 0 || (rank[0] = 1 / 0, rank[n - 1] = 1 / 0, n <= 2)) return rank; + let stack = [0, n - 1], cap = [1 / 0]; + for (; stack.length; ) { + let last = stack.pop(), first = stack.pop(), above = cap.pop(), maxD = 0, idx = -1, ax = p[first * 2], ay = p[first * 2 + 1], bx = p[last * 2], by = p[last * 2 + 1]; + for (let i = first + 1; i < last; i++) { + let d = segmentDistanceSq(p[i * 2], p[i * 2 + 1], ax, ay, bx, by); + d > maxD && (maxD = d, idx = i); + } + if (maxD > 0 && idx > 0) { + let r = maxD < above ? maxD : above; + rank[idx] = r, stack.push(first, idx, idx, last), cap.push(r, r); + } + } + return rank; +} +function simplifyRanked(p, rank, tolerance) { + let n = p.length / 2; + if (n <= 2 || tolerance <= 0) return p.slice(); + let tol2 = tolerance * tolerance, out = []; + for (let i = 0; i < n; i++) rank[i] > tol2 && out.push(p[i * 2], p[i * 2 + 1]); + return out; +} +function segmentDistanceSq(px, py, ax, ay, bx, by) { + let dx = bx - ax, dy = by - ay, l2 = dx * dx + dy * dy; + if (l2 > 0) { + let t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / l2)); + dx = ax + dx * t, dy = ay + dy * t; + } else + dx = ax, dy = ay; + return (px - dx) * (px - dx) + (py - dy) * (py - dy); +} +function dedupe(p, eps = 1e-9) { + let out = []; + for (let i = 0; i < p.length; i += 2) { + let n = out.length; + n >= 2 && Math.abs(out[n - 2] - p[i]) < eps && Math.abs(out[n - 1] - p[i + 1]) < eps || out.push(p[i], p[i + 1]); + } + return out; +} +var LOOP_WINDOW_OFFSETS = 12, INSIDE_MITER_LIMIT = 8; +function offsetPolyline(p, d, arcStepRad = Math.PI / 9) { + return offsetPolylineAnchored(p, d, arcStepRad).points; +} +function offsetPolylineAnchored(p, d, arcStepRad = Math.PI / 9) { + let src = dedupe(p), n = src.length / 2; + if (n < 2 || d === 0) return { points: src.slice(), anchors: src.slice() }; + let segs = new Float64Array((n - 1) * 4); + for (let i = 0; i < n - 1; i++) { + let ax = src[i * 2], ay = src[i * 2 + 1], bx = src[i * 2 + 2], by = src[i * 2 + 3], dx = bx - ax, dy = by - ay, l = Math.hypot(dx, dy), nx = dy / l, ny = -dx / l; + segs[i * 4] = ax + nx * d, segs[i * 4 + 1] = ay + ny * d, segs[i * 4 + 2] = bx + nx * d, segs[i * 4 + 3] = by + ny * d; + } + let out = [segs[0], segs[1]], anchors = [src[0], src[1]]; + for (let i = 1; i < n - 1; i++) { + let o0 = (i - 1) * 4, o1 = i * 4, s0x = segs[o0], s0y = segs[o0 + 1], s0X = segs[o0 + 2], s0Y = segs[o0 + 3], s1x = segs[o1], s1y = segs[o1 + 1], s1X = segs[o1 + 2], s1Y = segs[o1 + 3], vx = src[i * 2], vy = src[i * 2 + 1], cross = (s0X - s0x) * (s1Y - s1y) - (s0Y - s0y) * (s1X - s1x), dot = (s0X - s0x) * (s1X - s1x) + (s0Y - s0y) * (s1Y - s1y), outside = d > 0 == cross > 0; + if (Math.abs(cross) < 1e-9 * Math.max(1, Math.abs(dot))) { + out.push(s1x, s1y), anchors.push(vx, vy); + continue; + } + if (!outside) { + let ix = intersectLines(s0x, s0y, s0X, s0Y, s1x, s1y, s1X, s1Y); + ix && Math.hypot(ix[0] - vx, ix[1] - vy) <= INSIDE_MITER_LIMIT * Math.abs(d) ? (out.push(ix[0], ix[1]), anchors.push(vx, vy)) : (out.push(s0X, s0Y, s1x, s1y), anchors.push(vx, vy, vx, vy)); + continue; + } + let a0 = Math.atan2(s0Y - vy, s0X - vx), da = Math.atan2(s1y - vy, s1x - vx) - a0; + for (; da > Math.PI; ) da -= 2 * Math.PI; + for (; da < -Math.PI; ) da += 2 * Math.PI; + let steps = Math.max(1, Math.ceil(Math.abs(da) / arcStepRad)), r = Math.abs(d); + out.push(s0X, s0Y), anchors.push(vx, vy); + for (let k = 1; k < steps; k++) { + let a = a0 + da * k / steps; + out.push(vx + Math.cos(a) * r, vy + Math.sin(a) * r), anchors.push(vx, vy); + } + out.push(s1x, s1y), anchors.push(vx, vy); + } + out.push(segs[segs.length - 2], segs[segs.length - 1]), anchors.push(src[src.length - 2], src[src.length - 1]); + let open = removeLoopsAnchored(out, anchors, Math.abs(d) * LOOP_WINDOW_OFFSETS); + return dropEndHook(open, src[2] - src[0], src[3] - src[1], 0, 2 * Math.abs(d)), dropEndHook(open, src[src.length - 2] - src[src.length - 4], src[src.length - 1] - src[src.length - 3], 1, 2 * Math.abs(d)), open; +} +function dropEndHook(line, dx, dy, end, reach) { + let { points, anchors } = line, e = end ? points.length - 2 : 0, step = end ? -2 : 2, hook = 0; + for (let v = e + step; v >= 0 && v < points.length; v += step) { + let ex = points[e] - points[v], ey = points[e + 1] - points[v + 1]; + if (Math.hypot(ex, ey) > reach || (end ? 1 : -1) * (ex * dx + ey * dy) > 0) break; + hook++; + } + if (hook = Math.min(hook, points.length / 2 - 2), hook <= 0) return; + let from = end ? e - 2 * hook : 2; + points.splice(from, 2 * hook), anchors.splice(from, 2 * hook); +} +function offsetPolylineSlidingAnchored(p, d0, d1, stepPx = 4, via = []) { + let src = dedupe(p), n = src.length / 2; + if (n < 2) return { points: src.slice(), anchors: src.slice() }; + let dense = [src[0], src[1]]; + for (let i = 1; i < n; i++) { + let ax = src[i * 2 - 2], ay = src[i * 2 - 1], bx = src[i * 2], by = src[i * 2 + 1], l = Math.hypot(bx - ax, by - ay), k = Math.max(1, Math.ceil(l / stepPx)); + for (let j = 1; j <= k; j++) dense.push(ax + (bx - ax) * j / k, ay + (by - ay) * j / k); + } + let m = dense.length / 2, total = polylineLength(dense), out = new Array(dense.length), s = 0; + for (let i = 0; i < m; i++) { + let x = dense[i * 2], y = dense[i * 2 + 1]; + i > 0 && (s += Math.hypot(x - dense[i * 2 - 2], y - dense[i * 2 - 1])); + let d = d0; + if (total > 0) { + let s0 = 0, o0 = d0, s1 = total, o1 = d1; + for (let k = 0; k < via.length; k += 2) { + let at = via[k] * total; + if (at <= s) + s0 = at, o0 = via[k + 1]; + else { + s1 = at, o1 = via[k + 1]; + break; + } + } + d = s1 > s0 ? o0 + (o1 - o0) * (s - s0) / (s1 - s0) : o1; + } + let t0x = 0, t0y = 0, t1x = 0, t1y = 0; + if (i > 0) { + t0x = x - dense[i * 2 - 2], t0y = y - dense[i * 2 - 1]; + let l = Math.hypot(t0x, t0y) || 1; + t0x /= l, t0y /= l; + } + if (i < m - 1) { + t1x = dense[i * 2 + 2] - x, t1y = dense[i * 2 + 3] - y; + let l = Math.hypot(t1x, t1y) || 1; + t1x /= l, t1y /= l; + } + i === 0 && ([t0x, t0y] = [t1x, t1y]), i === m - 1 && ([t1x, t1y] = [t0x, t0y]); + let n0x = t0y, n0y = -t0x, n1x = t1y, n1y = -t1x, mx = n0x + n1x, my = n0y + n1y, ml = Math.hypot(mx, my); + if (ml < 1e-9) + mx = n0x, my = n0y; + else { + let cosHalf = Math.min(1, Math.max(0.5, ml / 2)); + mx = mx / ml / cosHalf, my = my / ml / cosHalf; + } + out[i * 2] = x + mx * d, out[i * 2 + 1] = y + my * d; + } + let widest = Math.max(Math.abs(d0), Math.abs(d1), 1); + for (let k = 1; k < via.length; k += 2) widest = Math.max(widest, Math.abs(via[k])); + return removeLoopsAnchored(out, dense, widest * LOOP_WINDOW_OFFSETS); +} +function selfIntersects(p) { + let n = p.length / 2; + for (let i = 0; i + 1 < n; i++) + for (let j = i + 2; j + 1 < n; j++) + if (intersectSegments(p[i * 2], p[i * 2 + 1], p[i * 2 + 2], p[i * 2 + 3], p[j * 2], p[j * 2 + 1], p[j * 2 + 2], p[j * 2 + 3])) return !0; + return !1; +} +function intersectLines(ax, ay, bx, by, cx, cy, dx, dy) { + let r = [bx - ax, by - ay], s = [dx - cx, dy - cy], denom = r[0] * s[1] - r[1] * s[0]; + if (Math.abs(denom) < 1e-12) return null; + let t = ((cx - ax) * s[1] - (cy - ay) * s[0]) / denom; + return [ax + r[0] * t, ay + r[1] * t]; +} +function intersectSegments(ax, ay, bx, by, cx, cy, dx, dy) { + let rX = bx - ax, rY = by - ay, sX = dx - cx, sY = dy - cy, denom = rX * sY - rY * sX; + if (Math.abs(denom) < 1e-12) return null; + let qpX = cx - ax, qpY = cy - ay, t = (qpX * sY - qpY * sX) / denom, u = (qpX * rY - qpY * rX) / denom; + return t <= 0 || t >= 1 || u <= 0 || u >= 1 ? null : { x: ax + rX * t, y: ay + rY * t, t, u }; +} +var OWN_LOOP_AREA_SHARE = 0.1; +function removeLoopsAnchored(p, anchors, window) { + let pts = p, anc = anchors, along = new Array(p.length / 2); + along[0] = 0; + for (let k = 1; k < along.length; k++) + along[k] = along[k - 1] + Math.hypot(p[k * 2] - p[k * 2 - 2], p[k * 2 + 1] - p[k * 2 - 1]); + let guard = 0; + for (; !(guard++ > 1e3); ) { + let n = pts.length / 2, found = null; + outer: for (let i = 0; i < n - 1 && !found; i++) { + let ax = pts[i * 2], ay = pts[i * 2 + 1], bx = pts[i * 2 + 2], by = pts[i * 2 + 3], minX = ax < bx ? ax : bx, maxX = ax < bx ? bx : ax, minY = ay < by ? ay : by, maxY = ay < by ? by : ay; + for (let j = i + 2; j < n - 1 && !(along[j] - along[i + 1] > window); j++) { + let cx = pts[j * 2], cy = pts[j * 2 + 1], dx = pts[j * 2 + 2], dy = pts[j * 2 + 3]; + if (cx < minX && dx < minX || cx > maxX && dx > maxX || cy < minY && dy < minY || cy > maxY && dy > maxY) continue; + let hit = intersectSegments(ax, ay, bx, by, cx, cy, dx, dy); + if (hit && !ownLoop(pts, anc, i, j, hit)) { + found = { i, j, x: hit.x, y: hit.y, t: hit.t, u: hit.u }; + break outer; + } + } + } + if (!found) break; + let cut = (found.i + 1) * 2, keep = (found.j + 1) * 2; + pts = pts.slice(0, cut).concat([found.x, found.y], pts.slice(keep)); + let ai = found.i * 2, t = found.t; + anc = anc.slice(0, cut).concat([anc[ai] + (anc[ai + 2] - anc[ai]) * t, anc[ai + 1] + (anc[ai + 3] - anc[ai + 1]) * t], anc.slice(keep)); + let alongCut = along[found.i] + (along[found.i + 1] - along[found.i]) * t; + along = along.slice(0, found.i + 1).concat([alongCut], along.slice(found.j + 1)); + } + return { points: pts, anchors: anc }; +} +function ownLoop(pts, anc, i, j, hit) { + let ring = (x0, y0, xs, k0, k1, x1, y1) => { + let a = 0, px = x0, py = y0; + for (let k = k0; k <= k1; k++) { + let x = xs[k * 2], y = xs[k * 2 + 1]; + a += px * y - x * py, px = x, py = y; + } + return a + px * y1 - x1 * py + x1 * y0 - x0 * y1; + }, offset = ring(hit.x, hit.y, pts, i + 1, j, hit.x, hit.y), ground = ring( + anc[i * 2] + (anc[i * 2 + 2] - anc[i * 2]) * hit.t, + anc[i * 2 + 1] + (anc[i * 2 + 3] - anc[i * 2 + 1]) * hit.t, + anc, + i + 1, + j, + anc[j * 2] + (anc[j * 2 + 2] - anc[j * 2]) * hit.u, + anc[j * 2 + 1] + (anc[j * 2 + 3] - anc[j * 2 + 1]) * hit.u + ); + return offset * ground > 0 && Math.abs(ground) >= OWN_LOOP_AREA_SHARE * Math.abs(offset); +} +function sampleCubicBezier(p0, p1, p2, p3, n) { + let out = []; + for (let i = 0; i <= n; i++) { + let t = i / n, mt = 1 - t, a = mt * mt * mt, b = 3 * mt * mt * t, c = 3 * mt * t * t, d = t * t * t; + out.push(a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]); + } + return out; +} +function connectorCurve(P, tp, Q, tq, samples, handleScale = 1) { + let K = 0.5523 * handleScale, chord = Math.hypot(Q[0] - P[0], Q[1] - P[1]), h = K * chord, I = intersectLines(P[0], P[1], P[0] + tp[0], P[1] + tp[1], Q[0], Q[1], Q[0] + tq[0], Q[1] + tq[1]); + if (I) { + let t = (I[0] - P[0]) * tp[0] + (I[1] - P[1]) * tp[1], u = (I[0] - Q[0]) * tq[0] + (I[1] - Q[1]) * tq[1]; + if (t > 0 && u > 0) { + let delta = (Math.hypot(I[0] - P[0], I[1] - P[1]) + Math.hypot(I[0] - Q[0], I[1] - Q[1])) / 2; + h = K * Math.min(delta, chord * 1.5); + } + } + let c1 = [P[0] + tp[0] * h, P[1] + tp[1] * h], c2 = [Q[0] + tq[0] * h, Q[1] + tq[1] * h]; + return sampleCubicBezier(P, c1, c2, Q, samples); +} +function ccwAngle(a, b) { + let d = -(Math.atan2(b[1], b[0]) - Math.atan2(a[1], a[0])); + for (; d < 0; ) d += 2 * Math.PI; + for (; d >= 2 * Math.PI; ) d -= 2 * Math.PI; + return d; +} +function smoothCatmullRom(p, segmentPx, maxPerSegment = 10, minBendDeg = 3, maxBendDeg = 180, cornerPx = 0) { + let n = p.length / 2; + if (n < 3) return p.slice(); + if (maxBendDeg < 180) { + let cosMax = Math.cos(maxBendDeg * Math.PI / 180), corners = null; + for (let i = 1; i < n - 1; i++) bendAt(p, i) < cosMax && (corners ?? (corners = [])).push(i); + if (corners) { + let out2 = [], from = 0; + for (let c of [...corners, n - 1]) { + let run = smoothCatmullRom(p.slice(from * 2, (c + 1) * 2), segmentPx, maxPerSegment, minBendDeg); + for (let k = out2.length ? 2 : 0; k < run.length; k += 2) out2.push(run[k], run[k + 1]); + from = c; + } + return cornerPx > 0 ? roundCorners(out2, p, corners, cornerPx) : out2; + } + } + let X = (i) => p[i * 2], Y = (i) => p[i * 2 + 1], bend = new Float64Array(n), cosMin = Math.cos(minBendDeg * Math.PI / 180); + for (let i = 1; i < n - 1; i++) { + let ax = X(i) - X(i - 1), ay = Y(i) - Y(i - 1), bx = X(i + 1) - X(i), by = Y(i + 1) - Y(i), la = Math.hypot(ax, ay) || 1, lb = Math.hypot(bx, by) || 1; + bend[i] = (ax * bx + ay * by) / (la * lb); + } + let out = [X(0), Y(0)]; + for (let i = 0; i < n - 1; i++) { + let curved = i > 0 && bend[i] < cosMin || i + 1 < n - 1 && bend[i + 1] < cosMin, len = Math.hypot(X(i + 1) - X(i), Y(i + 1) - Y(i)), steps = curved ? Math.max(1, Math.min(maxPerSegment, Math.round(len / segmentPx))) : 1; + if (steps === 1) { + out.push(X(i + 1), Y(i + 1)); + continue; + } + let p0x = i > 0 ? X(i - 1) : 2 * X(0) - X(1), p0y = i > 0 ? Y(i - 1) : 2 * Y(0) - Y(1), p1x = X(i), p1y = Y(i), p2x = X(i + 1), p2y = Y(i + 1), p3x = i + 2 < n ? X(i + 2) : 2 * X(n - 1) - X(n - 2), p3y = i + 2 < n ? Y(i + 2) : 2 * Y(n - 1) - Y(n - 2), t0 = 0, t1 = t0 + (Math.sqrt(Math.hypot(p1x - p0x, p1y - p0y)) || 1e-6), t2 = t1 + (Math.sqrt(Math.hypot(p2x - p1x, p2y - p1y)) || 1e-6), t3 = t2 + (Math.sqrt(Math.hypot(p3x - p2x, p3y - p2y)) || 1e-6); + for (let s = 1; s < steps; s++) { + let t = t1 + (t2 - t1) * s / steps, a1x = (t1 - t) / (t1 - t0) * p0x + (t - t0) / (t1 - t0) * p1x, a1y = (t1 - t) / (t1 - t0) * p0y + (t - t0) / (t1 - t0) * p1y, a2x = (t2 - t) / (t2 - t1) * p1x + (t - t1) / (t2 - t1) * p2x, a2y = (t2 - t) / (t2 - t1) * p1y + (t - t1) / (t2 - t1) * p2y, a3x = (t3 - t) / (t3 - t2) * p2x + (t - t2) / (t3 - t2) * p3x, a3y = (t3 - t) / (t3 - t2) * p2y + (t - t2) / (t3 - t2) * p3y, b1x = (t2 - t) / (t2 - t0) * a1x + (t - t0) / (t2 - t0) * a2x, b1y = (t2 - t) / (t2 - t0) * a1y + (t - t0) / (t2 - t0) * a2y, b2x = (t3 - t) / (t3 - t1) * a2x + (t - t1) / (t3 - t1) * a3x, b2y = (t3 - t) / (t3 - t1) * a2y + (t - t1) / (t3 - t1) * a3y; + out.push((t2 - t) / (t2 - t1) * b1x + (t - t1) / (t2 - t1) * b2x, (t2 - t) / (t2 - t1) * b1y + (t - t1) / (t2 - t1) * b2y); + } + out.push(p2x, p2y); + } + return out; +} +function splineStartDirection(p, segmentPx, maxPerSegment = 10, minBendDeg = 3, maxBendDeg = 180) { + if (p.length / 2 < 3 || maxBendDeg < 180 && bendAt(p, 1) < Math.cos(maxBendDeg * Math.PI / 180)) return startDirection(p); + let steps = splineSteps(p, 0, segmentPx, maxPerSegment, minBendDeg); + if (steps < 2) return startDirection(p); + let [x, y] = centripetalPoint(2 * p[0] - p[2], 2 * p[1] - p[3], p[0], p[1], p[2], p[3], p[4], p[5], 1 / steps), dx = x - p[0], dy = y - p[1], l = Math.hypot(dx, dy); + return l > 0 ? [dx / l, dy / l] : startDirection(p); +} +function splineEndDirection(p, segmentPx, maxPerSegment = 10, minBendDeg = 3, maxBendDeg = 180) { + let n = p.length / 2; + if (n < 3 || maxBendDeg < 180 && bendAt(p, n - 2) < Math.cos(maxBendDeg * Math.PI / 180)) return endDirection(p); + let steps = splineSteps(p, n - 2, segmentPx, maxPerSegment, minBendDeg); + if (steps < 2) return endDirection(p); + let o = (n - 1) * 2, [x, y] = centripetalPoint(p[o - 4], p[o - 3], p[o - 2], p[o - 1], p[o], p[o + 1], 2 * p[o] - p[o - 2], 2 * p[o + 1] - p[o - 1], (steps - 1) / steps), dx = p[o] - x, dy = p[o + 1] - y, l = Math.hypot(dx, dy); + return l > 0 ? [dx / l, dy / l] : endDirection(p); +} +function splineSteps(p, i, segmentPx, maxPerSegment, minBendDeg) { + let n = p.length / 2, cosMin = Math.cos(minBendDeg * Math.PI / 180); + if (!(i > 0 && bendAt(p, i) < cosMin || i + 1 < n - 1 && bendAt(p, i + 1) < cosMin)) return 1; + let len = Math.hypot(p[i * 2 + 2] - p[i * 2], p[i * 2 + 3] - p[i * 2 + 1]); + return Math.max(1, Math.min(maxPerSegment, Math.round(len / segmentPx))); +} +function roundCorners(out, src, corners, radiusPx) { + let n = src.length / 2, cum = new Float64Array(n); + for (let i = 1; i < n; i++) + cum[i] = cum[i - 1] + Math.hypot(src[i * 2] - src[i * 2 - 2], src[i * 2 + 1] - src[i * 2 - 1]); + let consumed = new Uint8Array(out.length), arcs = /* @__PURE__ */ new Map(), lastFwd = null; + for (let ci = 0; ci < corners.length; ci++) { + let c = corners[ci], prev = ci > 0 ? corners[ci - 1] : 0, next = ci + 1 < corners.length ? corners[ci + 1] : n - 1, r = Math.min(radiusPx, 0.4 * (cum[c] - cum[prev]), 0.4 * (cum[next] - cum[c])); + if (r < 0.05) continue; + let cx = src[c * 2], cy = src[c * 2 + 1], at = -1; + for (let i = 0; i < out.length; i += 2) + if (!consumed[i] && Math.abs(out[i] - cx) < 1e-6 && Math.abs(out[i + 1] - cy) < 1e-6) { + at = i; + break; + } + if (at <= 0 || at >= out.length - 2) continue; + let back = walkAlong(out, at, -2, r, lastFwd), fwd = walkAlong(out, at, 2, r, null); + if (r = Math.min(r, back.got, fwd.got), r < 0.05) continue; + let steps = Math.max(4, Math.min(12, Math.round(r / 2))), arc = []; + for (let i = 0; i <= steps; i++) { + let t = i / steps, m = 1 - t; + arc.push(m * m * back.x + 2 * m * t * cx + t * t * fwd.x, m * m * back.y + 2 * m * t * cy + t * t * fwd.y); + } + for (let i = back.idx + 2; i < fwd.idx; i += 2) consumed[i] = 1; + arcs.set(back.idx + 2, arc), lastFwd = { idx: fwd.idx, x: fwd.x, y: fwd.y }; + } + if (!arcs.size) return out; + let done = []; + for (let i = 0; i < out.length; i += 2) { + let arc = arcs.get(i); + if (arc) for (let k = 0; k < arc.length; k++) done.push(arc[k]); + consumed[i] || done.push(out[i], out[i + 1]); + } + return done; +} +function walkAlong(p, at, step, dist, spliced) { + let acc = 0, i = at; + for (; ; ) { + let j = i + step; + if (j < 0 || j + 1 >= p.length) return { x: p[i], y: p[i + 1], idx: i, got: acc }; + let stand = spliced !== null && step < 0 && i === spliced.idx, jx = stand ? spliced.x : p[j], jy = stand ? spliced.y : p[j + 1], seg = Math.hypot(jx - p[i], jy - p[i + 1]); + if (acc + seg >= dist) { + let t = seg > 1e-12 ? (dist - acc) / seg : 0; + return { x: p[i] + (jx - p[i]) * t, y: p[i + 1] + (jy - p[i + 1]) * t, idx: j, got: dist }; + } + acc += seg, i = j; + } +} +function bendAt(p, k) { + let ax = p[k * 2] - p[k * 2 - 2], ay = p[k * 2 + 1] - p[k * 2 - 1], bx = p[k * 2 + 2] - p[k * 2], by = p[k * 2 + 3] - p[k * 2 + 1]; + return (ax * bx + ay * by) / ((Math.hypot(ax, ay) || 1) * (Math.hypot(bx, by) || 1)); +} +function centripetalPoint(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, u) { + let t1 = 0 + (Math.sqrt(Math.hypot(p1x - p0x, p1y - p0y)) || 1e-6), t2 = t1 + (Math.sqrt(Math.hypot(p2x - p1x, p2y - p1y)) || 1e-6), t3 = t2 + (Math.sqrt(Math.hypot(p3x - p2x, p3y - p2y)) || 1e-6), t = t1 + (t2 - t1) * u, a1x = (t1 - t) / (t1 - 0) * p0x + (t - 0) / (t1 - 0) * p1x, a1y = (t1 - t) / (t1 - 0) * p0y + (t - 0) / (t1 - 0) * p1y, a2x = (t2 - t) / (t2 - t1) * p1x + (t - t1) / (t2 - t1) * p2x, a2y = (t2 - t) / (t2 - t1) * p1y + (t - t1) / (t2 - t1) * p2y, a3x = (t3 - t) / (t3 - t2) * p2x + (t - t2) / (t3 - t2) * p3x, a3y = (t3 - t) / (t3 - t2) * p2y + (t - t2) / (t3 - t2) * p3y, b1x = (t2 - t) / (t2 - 0) * a1x + (t - 0) / (t2 - 0) * a2x, b1y = (t2 - t) / (t2 - 0) * a1y + (t - 0) / (t2 - 0) * a2y, b2x = (t3 - t) / (t3 - t1) * a2x + (t - t1) / (t3 - t1) * a3x, b2y = (t3 - t) / (t3 - t1) * a2y + (t - t1) / (t3 - t1) * a3y; + return [(t2 - t) / (t2 - t1) * b1x + (t - t1) / (t2 - t1) * b2x, (t2 - t) / (t2 - t1) * b1y + (t - t1) / (t2 - t1) * b2y]; +} +function polylineBounds(p) { + let minX = 1 / 0, minY = 1 / 0, maxX = -1 / 0, maxY = -1 / 0; + for (let i = 0; i < p.length; i += 2) + p[i] < minX && (minX = p[i]), p[i] > maxX && (maxX = p[i]), p[i + 1] < minY && (minY = p[i + 1]), p[i + 1] > maxY && (maxY = p[i + 1]); + return { minX, minY, maxX, maxY }; +} +function boundsIntersect(a, b) { + return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY; +} + +// src/core/order.ts +function mulberry32(seed) { + let a = seed >>> 0; + return () => { + a = a + 1831565813 >>> 0; + let t = a; + return t = Math.imul(t ^ t >>> 15, t | 1), t ^= t + Math.imul(t ^ t >>> 7, t | 61), ((t ^ t >>> 14) >>> 0) / 4294967296; + }; +} +var SAME_NEXT = 0, DIFF_NEXT = 1, PERIPHERY = 2, ONE_WAY_SHARE = 0.3, ONE_WAY_STOP = 0.15, SECOND_PASS_STABILITY = 0.25, ALIGN_STABILITY = 0.1, ALIGN_DEPTHS = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 1 / 0], permTables = []; +function permTable(k) { + let t = permTables[k]; + if (t) return t; + t = []; + let rec = (rest, m) => { + if (!rest.length) { + t.push(m); + return; + } + for (let i = 0; i < rest.length; i++) rec(rest.slice(0, i).concat(rest.slice(i + 1)), m.concat(rest[i])); + }; + return rec(Array.from({ length: k }, (_, i) => i), []), permTables[k] = t, t; +} +var LaneOrderer = class { + constructor(g, opts = {}) { + __publicField(this, "g"); + __publicField(this, "w"); + __publicField(this, "opts"); + /** Route indices follow sorted ids, so comparing indices breaks ties as ids would. */ + __publicField(this, "routeIds"); + __publicField(this, "routeIndex"); + /** + * Every edge's lanes and lane positions live in two flat arrays, `lanes[e]` and `pos[e]` being + * views into them, so the cost evaluation can index by edge and route without a pointer chase. + */ + __publicField(this, "laneBuf"); + __publicField(this, "laneStart"); + __publicField(this, "laneCount"); + __publicField(this, "posBuf"); + __publicField(this, "lanes"); + __publicField(this, "pos"); + /** Port index of edge e at its a node (2e) and b node (2e + 1). */ + __publicField(this, "portAt"); + /** Per node and port, per route index: the ports the route continues to. */ + __publicField(this, "next"); + /** + * The same continuations flat, for the swap walk: the ports of node n start at `portBase[n]`, + * a port's edge is `portEdge`, and the ports route r continues to from port q are + * `nextList[nextStart[q * R + r] .. nextStart[q * R + r + 1])`, in the order `next` lists them. + */ + __publicField(this, "portBase"); + __publicField(this, "portEdge"); + __publicField(this, "nextStart"); + __publicField(this, "nextList"); + __publicField(this, "terms"); + /** Per seeded edge: lane position of each route index in the initial order (-1 when unseeded). */ + __publicField(this, "seedPos", []); + __publicField(this, "stability", 0); + __publicField(this, "stack", []); + __publicField(this, "edgeStamp", new Uint32Array(0)); + __publicField(this, "nodeStamp", new Uint32Array(0)); + __publicField(this, "stamp", 0); + __publicField(this, "nodeCache", new Float64Array(0)); + __publicField(this, "nodeFresh", new Float64Array(0)); + /** Timing of the last solve, ms, both passes together, for diagnostics. */ + __publicField(this, "timings", { greedy: 0, anneal: 0, descend: 0 }); + this.g = g, this.opts = opts, this.w = { + sameSegmentCrossing: opts.sameSegmentCrossing ?? 4, + diffSegmentCrossing: opts.diffSegmentCrossing ?? 1, + separation: opts.separation ?? 3, + periphery: opts.periphery ?? 0.5 + }; + let ids = /* @__PURE__ */ new Set(); + for (let e of g.edges) for (let r of e.routes) ids.add(r); + this.routeIds = [...ids].sort(), this.routeIndex = new Map(this.routeIds.map((r, i) => [r, i])); + let R = this.routeIds.length, E = g.edges.length; + this.laneStart = new Int32Array(E + 1), this.laneCount = new Int32Array(E); + for (let e = 0; e < E; e++) + this.laneCount[e] = g.edges[e].routes.length, this.laneStart[e + 1] = this.laneStart[e] + this.laneCount[e]; + this.laneBuf = new Int32Array(this.laneStart[E]), this.posBuf = new Int32Array(E * R).fill(-1), this.lanes = g.edges.map((e) => this.laneBuf.subarray(this.laneStart[e.id], this.laneStart[e.id + 1])), this.pos = g.edges.map((e) => e.routes.length > 1 ? this.posBuf.subarray(e.id * R, (e.id + 1) * R) : null), this.portAt = new Int32Array(2 * g.edges.length).fill(-1); + for (let n of g.nodes) n.ports.forEach((p, i) => this.portAt[2 * p.edge + (p.end === "a" ? 0 : 1)] = i); + this.next = g.nodes.map((node) => { + let next = node.ports.map(() => []); + for (let t of node.transitions) { + if (t.from < 0 || t.to < 0 || t.from === t.to) continue; + let r = this.routeIndex.get(t.route); + if (r !== void 0) + for (let [a, b] of [[t.from, t.to], [t.to, t.from]]) { + let l = next[a][r] ?? (next[a][r] = []); + l.includes(b) || l.push(b); + } + } + return next; + }), this.portBase = new Int32Array(g.nodes.length + 1); + for (let n = 0; n < g.nodes.length; n++) this.portBase[n + 1] = this.portBase[n] + g.nodes[n].ports.length; + let P = this.portBase[g.nodes.length]; + this.portEdge = new Int32Array(P), this.nextStart = new Int32Array(P * R + 1); + let list = []; + for (let n = 0; n < g.nodes.length; n++) + for (let pi = 0; pi < g.nodes[n].ports.length; pi++) { + let q = this.portBase[n] + pi; + this.portEdge[q] = g.nodes[n].ports[pi].edge; + for (let r = 0; r < R; r++) { + let l = this.next[n][pi][r]; + if (l) for (let p of l) list.push(p); + this.nextStart[q * R + r + 1] = list.length; + } + } + this.nextList = Int32Array.from(list), this.terms = this.buildTerms(), this.load(); + } + buildTerms() { + let g = this.g, buf = { kind: [], edge: [], out: [], a: [], b: [], next: [], nextOut: [], aLeft: [] }, start = new Int32Array(g.nodes.length + 1); + g.nodes.forEach((node, n) => { + start[n] = buf.kind.length; + let np = node.ports.length, dirs = node.ports.map((p) => { + let e = g.edges[p.edge]; + return p.end === "a" ? startDirection(e.coords) : endDirection(e.coords).map((v) => -v); + }), ccw = new Float64Array(np * np); + for (let i = 0; i < np; i++) for (let j = 0; j < np; j++) ccw[i * np + j] = ccwAngle(dirs[i], dirs[j]); + for (let pi = 0; pi < np; pi++) { + let port = node.ports[pi], e = g.edges[port.edge], k = e.routes.length; + if (k < 2) continue; + let out = port.end === "a" ? 1 : 0, nextOf = this.next[n][pi], routes = e.routes.map((r) => this.routeIndex.get(r)); + for (let i = 0; i < k; i++) { + let a = routes[i], aNexts = nextOf[a]; + if (!aNexts || !aNexts.length) { + push(buf, PERIPHERY, e.id, out, a, -1, -1, 0, 0); + continue; + } + for (let j = i + 1; j < k; j++) { + let b = routes[j], bNexts = nextOf[b]; + if (!(!bNexts || !bNexts.length)) + for (let aNext of aNexts) + for (let bNext of bNexts) + if (aNext === bNext) { + if (aNext < pi) continue; + let f = node.ports[aNext]; + push(buf, SAME_NEXT, e.id, out, a, b, f.edge, f.end === "a" ? 1 : 0, 0); + } else { + let aLeft = ccw[pi * np + aNext] < ccw[pi * np + bNext] ? 1 : 0; + push(buf, DIFF_NEXT, e.id, out, a, b, -1, 0, aLeft); + } + } + } + } + }), start[g.nodes.length] = buf.kind.length; + let R = this.routeIds.length, T = buf.kind.length, posA = new Int32Array(T), posB = new Int32Array(T), nextA = new Int32Array(T), nextB = new Int32Array(T); + for (let i = 0; i < T; i++) + posA[i] = buf.edge[i] * R + buf.a[i], posB[i] = buf.edge[i] * R + Math.max(0, buf.b[i]), nextA[i] = Math.max(0, buf.next[i]) * R + buf.a[i], nextB[i] = Math.max(0, buf.next[i]) * R + Math.max(0, buf.b[i]); + return { + start, + kind: Uint8Array.from(buf.kind), + edge: Int32Array.from(buf.edge), + out: Uint8Array.from(buf.out), + a: Int32Array.from(buf.a), + b: Int32Array.from(buf.b), + next: Int32Array.from(buf.next), + nextOut: Uint8Array.from(buf.nextOut), + aLeft: Uint8Array.from(buf.aLeft), + posA, + posB, + nextA, + nextB + }; + } + /** Read every edge's `order` into the solver state (sorted route ids where it is unusable). */ + load() { + this.g.edges.forEach((e, i) => { + let order = e.order.length === e.routes.length && sameSet(e.order, e.routes) ? e.order : e.routes.slice().sort(); + this.setLanes(i, order.map((r) => this.routeIndex.get(r))); + }); + } + store() { + this.g.edges.forEach((e, i) => e.order = Array.from(this.lanes[i], (r) => this.routeIds[r])); + } + setLanes(e, routes) { + let lanes = this.lanes[e]; + lanes.set(routes); + let pos = this.pos[e]; + if (pos) for (let i = 0; i < lanes.length; i++) pos[lanes[i]] = i; + } + /** Cost contributed by one node under the current edge orders. */ + nodeCost(nodeId) { + return this.load(), this.evalNode(nodeId); + } + totalCost() { + return this.load(), this.evalAll(); + } + evalAll() { + let c = 0; + for (let i = 0; i < this.g.nodes.length; i++) c += this.evalNode(i); + return c; + } + evalNode(n) { + let t = this.terms, tEdge = t.edge, tOut = t.out, tKind = t.kind, tNext = t.next, tNextOut = t.nextOut, tALeft = t.aLeft, tPosA = t.posA, tPosB = t.posB, tNextA = t.nextA, tNextB = t.nextB, pos = this.posBuf, count = this.laneCount, wPeriphery = this.w.periphery, wDiff = this.w.diffSegmentCrossing, wSame = this.w.sameSegmentCrossing, wSep = this.w.separation, cost = 0; + for (let i = t.start[n], end = t.start[n + 1]; i < end; i++) { + let k = count[tEdge[i]], pa = pos[tPosA[i]]; + tOut[i] || (pa = k - 1 - pa); + let kind = tKind[i]; + if (kind === PERIPHERY) { + pa !== 0 && pa !== k - 1 && (cost += wPeriphery); + continue; + } + let pb = pos[tPosB[i]]; + tOut[i] || (pb = k - 1 - pb); + let aLeft = pa < pb; + if (kind === DIFF_NEXT) { + tALeft[i] === 1 !== aLeft && (cost += wDiff); + continue; + } + let fa = pos[tNextA[i]], fb = pos[tNextB[i]]; + if (!tNextOut[i]) { + let kf = count[tNext[i]]; + fa = kf - 1 - fa, fb = kf - 1 - fb; + } + fa < fb === aLeft ? cost += wSame : Math.abs(pa - pb) === 1 && Math.abs(fa - fb) !== 1 && (cost += wSep); + } + return cost; + } + edgeLocalCost(e) { + let edge = this.g.edges[e]; + return (edge.a === edge.b ? this.evalNode(edge.a) : this.evalNode(edge.a) + this.evalNode(edge.b)) + this.edgePenalty(e); + } + edgePenalty(e) { + let sp = this.seedPos[e]; + if (!sp) return 0; + let lanes = this.lanes[e], inversions = 0; + for (let i = 0; i < lanes.length; i++) { + let sa = sp[lanes[i]]; + if (!(sa < 0)) + for (let j = i + 1; j < lanes.length; j++) { + let sb = sp[lanes[j]]; + sb >= 0 && sb < sa && inversions++; + } + } + return inversions * this.stability; + } + /** Try every permutation (k <= 6) or pairwise swaps (larger k) of one edge; keep the best. */ + optimizeEdge(e) { + let lanes = this.lanes[e], k = lanes.length; + if (k < 2) return !1; + let startCost = this.edgeLocalCost(e), best = lanes.slice(), bestCost = startCost; + if (k <= 6) { + let base = lanes.slice(), scratch = new Int32Array(k); + for (let p of permTable(k)) { + for (let i = 0; i < k; i++) scratch[i] = base[p[i]]; + this.setLanes(e, scratch); + let c = this.edgeLocalCost(e); + (c < bestCost - 1e-9 || Math.abs(c - bestCost) < 1e-9 && lexLess(scratch, best)) && (bestCost = c, best = scratch.slice()); + } + } else { + let improved = !0; + for (; improved; ) { + improved = !1; + for (let i = 0; i < k; i++) + for (let j = i + 1; j < k; j++) { + let p = best.slice(), tmp = p[i]; + p[i] = p[j], p[j] = tmp, this.setLanes(e, p); + let c = this.edgeLocalCost(e); + c < bestCost - 1e-9 && (bestCost = c, best = p, improved = !0); + } + } + } + return this.setLanes(e, best), bestCost < startCost - 1e-9; + } + descend(comp) { + let changed = !0, rounds = 0; + for (; changed && rounds++ < 100; ) { + changed = !1; + for (let e of comp.edges) this.optimizeEdge(e) && (changed = !0); + } + return this.componentCost(comp); + } + componentCost(comp) { + let c = 0; + for (let n of comp.nodes) c += this.evalNode(n); + if (this.seedPos.length) for (let e of comp.edges) c += this.edgePenalty(e); + return c; + } + /** + * Swap along every connected edge where the pair stays adjacent, so a crossing moves as a whole. + * That alone cannot remove a pair's crossing between two edges it shares, because both sides of + * it flip together. A one-way swap (`end` 0 or 1) leaves the start edge through that end only, + * and with `stop` may halt at any further edge, so such a crossing slides along the pair's run + * until it leaves it or reaches a junction where it costs less. + */ + swapAlong(e, i, touched, stamp, end = -1, stop = null) { + let g = this.g, a = this.lanes[e][i], b = this.lanes[e][i + 1], stack = this.stack, edgeStamp = this.edgeStamp, { portBase, portEdge, nextStart, nextList } = this, R = this.routeIds.length; + for (stack.length = 0, stack.push(e); stack.length; ) { + let cur = stack.pop(), pos = this.pos[cur]; + if (!pos || edgeStamp[cur] === stamp) continue; + edgeStamp[cur] = stamp; + let ia = pos[a], ib = pos[b]; + if (ia < 0 || ib < 0 || Math.abs(ia - ib) !== 1) continue; + touched.push(cur), this.swapPair(cur, a, b); + let edge = g.edges[cur]; + for (let side = 0; side < 2; side++) { + if (cur === e && end >= 0 && side !== end || cur !== e && stop && stop() < ONE_WAY_STOP) continue; + let nodeId = side === 0 ? edge.a : edge.b, pi = this.portAt[2 * cur + side]; + if (pi < 0) continue; + let q = portBase[nodeId] + pi, a0 = nextStart[q * R + a], a1 = nextStart[q * R + a + 1], b0 = nextStart[q * R + b], b1 = nextStart[q * R + b + 1]; + if (!(a0 === a1 || b0 === b1)) + for (let x = a0; x < a1; x++) { + let p = nextList[x]; + for (let y = b0; y < b1; y++) + if (nextList[y] === p) { + stack.push(portEdge[portBase[nodeId] + p]); + break; + } + } + } + } + } + /** + * Make edge `to` agree with edge `from` across `node`, so that no pair swaps sides between + * them, and carry that along `to`'s run for `depth` further edges. The routes keep the places + * they hold as a group, so the lanes around them do not move. + */ + alignRun(from, to, node, depth, touched, saved, stamp) { + let g = this.g, edgeStamp = this.edgeStamp; + edgeStamp[from] = stamp; + let frontier = [[to, from, node]]; + for (let d = 0; frontier.length; d++) { + let next = []; + for (let [h, x, n] of frontier) { + let posX = this.pos[x], posH = this.pos[h]; + if (edgeStamp[h] === stamp || !posX || !posH || g.edges[h].a === g.edges[h].b || g.edges[x].a === g.edges[x].b) continue; + edgeStamp[h] = stamp; + let xOut = g.edges[x].a === n, hOut = g.edges[h].a === n, portH = this.portAt[2 * h + (hOut ? 0 : 1)], nextOf = this.next[n][this.portAt[2 * x + (xOut ? 0 : 1)]], lx = this.lanes[x], lh = this.lanes[h], group = []; + for (let i = 0; i < lx.length; i++) { + let r = lx[xOut ? i : lx.length - 1 - i]; + posH[r] >= 0 && nextOf[r]?.includes(portH) && group.push(r); + } + if (group.length < 2) continue; + group.reverse(); + let slots = []; + for (let i = 0; i < lh.length; i++) { + let at = hOut ? i : lh.length - 1 - i; + group.includes(lh[at]) && slots.push(at); + } + if (slots.every((at, i) => lh[at] === group[i])) continue; + touched.push(h), saved.push(lh.slice()); + let aligned = lh.slice(); + if (slots.forEach((at, i) => aligned[at] = group[i]), this.setLanes(h, aligned), d >= depth) continue; + let far = hOut ? g.edges[h].b : g.edges[h].a; + for (let p of g.nodes[far].ports) p.edge !== h && edgeStamp[p.edge] !== stamp && next.push([p.edge, h, far]); + } + frontier = next; + } + } + /** + * Go to each node where a pair swaps sides between two edges it shares, and align one side's + * run to the other wherever that lowers the cost, best first. A search by random swaps finds + * the same orders and takes as long again as the first pass. + */ + alignSwaps(comp, changed) { + let g = this.g, t = this.terms, cache2 = this.nodeCache, nodeStamp = this.nodeStamp; + for (let n of comp.nodes) cache2[n] = this.evalNode(n); + let touched = [], saved = [], nodes = [], costs = [], attempt = (from, to, node, depth, keep) => { + let stamp = ++this.stamp; + if (touched.length = 0, saved.length = 0, this.alignRun(from, to, node, depth, touched, saved, stamp), !touched.length) return 0; + nodes.length = 0, costs.length = 0; + for (let e of touched) + for (let n of [g.edges[e].a, g.edges[e].b]) + nodeStamp[n] !== stamp && (nodeStamp[n] = stamp, nodes.push(n)); + let delta = 0; + for (let n of nodes) { + let c = this.evalNode(n); + costs.push(c), delta += c - cache2[n]; + } + if (this.stability > 0) { + let aligned = touched.map((e) => this.lanes[e].slice()); + for (let e of touched) delta += this.edgePenalty(e); + touched.forEach((e, i) => this.setLanes(e, saved[i])); + for (let e of touched) delta -= this.edgePenalty(e); + touched.forEach((e, i) => this.setLanes(e, aligned[i])); + } + return keep ? nodes.forEach((n, i) => cache2[n] = costs[i]) : touched.forEach((e, i) => this.setLanes(e, saved[i])), delta; + }; + for (let round = 0; round < 400; round++) { + let best = -1e-9, move = null, tried = /* @__PURE__ */ new Set(); + for (let n of comp.nodes) + for (let k = t.start[n]; k < t.start[n + 1]; k++) { + if (t.kind[k] !== SAME_NEXT) continue; + let e = t.edge[k], f = t.next[k], key = `${n}:${e}:${f}`; + if (tried.has(key)) continue; + let pe = this.pos[e], pf = this.pos[f], left = pe[t.a[k]] < pe[t.b[k]] == (t.out[k] === 1), leftNext = pf[t.a[k]] < pf[t.b[k]] == (t.nextOut[k] === 1); + if (left === leftNext) { + tried.add(key); + for (let [from, to] of [[e, f], [f, e]]) { + let reach = -1; + for (let depth of ALIGN_DEPTHS) { + let delta = attempt(from, to, n, depth, !1); + if (touched.length === reach) break; + reach = touched.length, delta < best && (best = delta, move = [from, to, n, depth]); + } + } + } + } + if (!move) return; + attempt(move[0], move[1], move[2], move[3], !0); + for (let e of touched) changed.push(e); + } + } + /** Apply one swap and return what it changes the cost by; `touched` and `nodes` say where. */ + swapDelta(e, i, end, stop, touched, nodes) { + let g = this.g, a = this.lanes[e][i], b = this.lanes[e][i + 1], stamp = ++this.stamp, nodeStamp = this.nodeStamp; + touched.length = 0, this.swapAlong(e, i, touched, stamp, end, stop), nodes.length = 0; + for (let t of touched) { + let edge = g.edges[t]; + nodeStamp[edge.a] !== stamp && (nodeStamp[edge.a] = stamp, nodes.push(edge.a)), nodeStamp[edge.b] !== stamp && (nodeStamp[edge.b] = stamp, nodes.push(edge.b)); + } + let delta = 0; + for (let n of nodes) + this.nodeFresh[n] = this.evalNode(n), delta += this.nodeFresh[n] - this.nodeCache[n]; + if (this.stability > 0) + for (let t of touched) { + let sp = this.seedPos[t]; + if (!sp || sp[a] < 0 || sp[b] < 0) continue; + let pos = this.pos[t]; + delta += pos[a] < pos[b] == sp[a] < sp[b] ? -this.stability : this.stability; + } + return delta; + } + swapPair(e, a, b) { + let pos = this.pos[e], lanes = this.lanes[e], ia = pos[a], ib = pos[b]; + lanes[ia] = b, lanes[ib] = a, pos[a] = ib, pos[b] = ia; + } + /** + * `oneWay` adds the one-way swaps, and keeps the best order the walk passes through rather + * than the one it ends on. The first pass of an unseeded solve runs without it. + */ + anneal(comp, moves, rand, oneWay) { + let candidates = comp.edges; + if (moves <= 0) return; + let T0 = 5, decay = Math.pow(0.3 / T0, 1 / moves), cache2 = this.nodeCache, fresh = this.nodeFresh; + for (let n of comp.nodes) cache2[n] = this.evalNode(n); + let touched = [], touchedNodes = [], running = 0, lowest = 0, kept = null, T = T0; + for (let m = 0; m < moves; m++, T *= decay) { + let e = candidates[Math.floor(rand() * candidates.length)], i = Math.floor(rand() * (this.lanes[e].length - 1)), a = this.lanes[e][i], b = this.lanes[e][i + 1], end = oneWay && rand() < ONE_WAY_SHARE ? rand() < 0.5 ? 0 : 1 : -1, delta = this.swapDelta(e, i, end, end >= 0 ? rand : null, touched, touchedNodes); + if (delta <= 0 || rand() < Math.exp(-delta / T)) { + for (let n of touchedNodes) cache2[n] = fresh[n]; + if (running += delta, oneWay && running < lowest - 1e-9) + if (lowest = running, !kept) kept = candidates.map((c) => this.lanes[c].slice()); + else for (let c = 0; c < candidates.length; c++) kept[c].set(this.lanes[candidates[c]]); + } else + for (let t of touched) this.swapPair(t, a, b); + } + kept && lowest < running - 1e-9 && candidates.forEach((c, k) => this.setLanes(c, kept[k])); + } + /** + * One scan of a hill climb on the annealing's own swaps, over every edge or over those marked + * in `only`. The edges it changes are added to `changed`. + */ + descendSwaps(comp, only, changed) { + let cache2 = this.nodeCache, fresh = this.nodeFresh; + for (let n of comp.nodes) cache2[n] = this.evalNode(n); + let touched = [], touchedNodes = []; + for (let e of comp.edges) + if (!(only && !only[e])) + for (let i = 0; i + 1 < this.lanes[e].length; i++) + for (let end = -1; end < 2; end++) { + let a = this.lanes[e][i], b = this.lanes[e][i + 1]; + if (this.swapDelta(e, i, end, null, touched, touchedNodes) < -1e-9) { + for (let n of touchedNodes) cache2[n] = fresh[n]; + for (let t of touched) changed.push(t); + } else + for (let t of touched) this.swapPair(t, a, b); + } + } + /** + * Both descents, each over the edges that share a node with one that just changed, until + * nothing changes. A change can only open a gain next to itself, so scanning the whole group + * again after each one costs several times as much and finds the same order. The first scan + * of swaps covers the whole group when `wholeFirst`, since no pass before it tried them. + */ + polish(comp, from, wholeFirst) { + let near = from ? this.beside(from) : null, whole = wholeFirst; + for (let round = 0; round < 200; round++) { + let changed = []; + for (let e of comp.edges) (!near || near[e]) && this.optimizeEdge(e) && changed.push(e); + if (this.descendSwaps(comp, whole ? null : near, changed), whole = !1, !changed.length) return; + near = this.beside(changed); + } + } + /** Marks the edges that share a node with any of `edges`, themselves included. */ + beside(edges) { + let g = this.g, mark = new Uint8Array(g.edges.length); + for (let e of edges) + for (let n of [g.edges[e].a, g.edges[e].b]) for (let p of g.nodes[n].ports) mark[p.edge] = 1; + return mark; + } + /** Multi-route edges in groups that share no node, each an independent problem; sorted for determinism. */ + components() { + let g = this.g, E = g.edges.length, parent = new Int32Array(E); + for (let e = 0; e < E; e++) parent[e] = e; + let find = (x) => { + for (; parent[x] !== x; ) x = parent[x] = parent[parent[x]]; + return x; + }; + for (let node of g.nodes) { + let first = -1; + for (let p of node.ports) + this.lanes[p.edge].length < 2 || (first < 0 ? first = p.edge : parent[find(p.edge)] = find(first)); + } + let byRoot = /* @__PURE__ */ new Map(); + for (let e = 0; e < E; e++) { + if (this.lanes[e].length < 2) continue; + let r = find(e), c = byRoot.get(r); + c || byRoot.set(r, c = { edges: [], nodes: [] }), c.edges.push(e); + } + let comps = [...byRoot.values()]; + for (let c of comps) { + let nodes = /* @__PURE__ */ new Set(); + for (let e of c.edges) + nodes.add(g.edges[e].a), nodes.add(g.edges[e].b); + c.nodes = [...nodes].sort((x, y) => x - y); + } + return comps.sort((x, y) => y.edges.length - x.edges.length || x.edges[0] - y.edges[0]); + } + /** + * Assign orders on all edges. Returns the final cost. + * + * An unseeded solve runs two passes. The first is greedy propagation and annealing with whole + * swaps. The second starts from the first's order and does no annealing: it aligns the runs + * across every pair the first left swapping sides, then descends, and charges each pair that + * leaves the first pass's order. A seeded solve is one pass from the caller's seed, annealed + * with the one-way swaps as well, at the caller's `stability`. + */ + solve() { + this.timings = { greedy: 0, anneal: 0, descend: 0 }; + let seed = this.opts.initial; + if (seed && seed.size > 0) return this.pass(seed, this.opts.stability ?? 1, "seeded"); + this.pass(null, 0, "first"); + let first = /* @__PURE__ */ new Map(); + for (let e of this.g.edges) e.routes.length > 1 && first.set(e.id, e.order.slice()); + return this.pass(first, SECOND_PASS_STABILITY, "second"); + } + pass(initial, stability, kind) { + let tStart = performance.now(), g = this.g, rand = mulberry32(this.opts.seed ?? 42), seeded = !!initial; + this.seedPos = g.edges.map(() => null), this.stability = seeded ? stability : 0; + for (let e of g.edges) { + let init = initial?.get(e.id)?.filter((r) => e.routes.includes(r)); + if (!init || !init.length) { + e.order = e.routes.slice().sort(); + continue; + } + if (e.order = init.concat(e.routes.filter((r) => !init.includes(r)).sort()), e.routes.length > 1) { + let sp = new Int32Array(this.routeIds.length).fill(-1); + init.forEach((r, i) => sp[this.routeIndex.get(r)] = i), this.seedPos[e.id] = sp; + } + } + this.load(), this.edgeStamp = new Uint32Array(g.edges.length), this.nodeStamp = new Uint32Array(g.nodes.length), this.nodeCache = new Float64Array(g.nodes.length), this.nodeFresh = new Float64Array(g.nodes.length), this.stamp = 0; + let seen = new Set(seeded ? g.edges.map((e) => e.id) : []), queue = g.edges.slice().sort((x, y) => y.routes.length - x.routes.length || x.id - y.id), frontier = []; + for (let start of queue) + if (!seen.has(start.id)) + for (frontier.push(start.id); frontier.length; ) { + let id = frontier.shift(); + if (seen.has(id)) continue; + seen.add(id), this.optimizeEdge(id); + let e = g.edges[id]; + for (let n of [e.a, e.b]) for (let p of g.nodes[n].ports) seen.has(p.edge) || frontier.push(p.edge); + } + let timings = this.timings; + timings.greedy += performance.now() - tStart; + let comps = this.components(), candidates = comps.reduce((s, c) => s + c.edges.length, 0), totalMoves = this.opts.annealMoves ?? Math.max(1e4, 400 * candidates); + for (let comp of comps) { + let t = performance.now(), bestCost = 1 / 0, best = []; + seeded && (bestCost = kind === "second" ? this.componentCost(comp) : this.descend(comp), best = comp.edges.map((e) => this.lanes[e].slice())), timings.descend += performance.now() - t, t = performance.now(); + let aligned = []; + kind === "second" ? (this.stability = ALIGN_STABILITY, this.alignSwaps(comp, aligned), this.stability = stability) : bestCost > 0 && this.anneal(comp, Math.ceil(totalMoves * comp.edges.length / candidates), rand, kind === "seeded"), timings.anneal += performance.now() - t, t = performance.now(), kind === "second" ? this.polish(comp, aligned, !0) : kind === "seeded" && this.polish(comp, null, !0); + let c = kind === "second" ? this.componentCost(comp) : this.descend(comp); + c < bestCost - 1e-9 ? (bestCost = c, best = comp.edges.map((e) => this.lanes[e].slice())) : best.forEach((l, i) => this.setLanes(comp.edges[i], l)), timings.descend += performance.now() - t; + } + return this.store(), this.evalAll(); + } +}; +function push(buf, kind, edge, out, a, b, next, nextOut, aLeft) { + buf.kind.push(kind), buf.edge.push(edge), buf.out.push(out), buf.a.push(a), buf.b.push(b), buf.next.push(next), buf.nextOut.push(nextOut), buf.aLeft.push(aLeft); +} +function sameSet(a, b) { + if (a.length !== b.length) return !1; + let s = new Set(a); + return b.every((x) => s.has(x)); +} +function lexLess(a, b) { + for (let i = 0; i < a.length; i++) { + if (a[i] < b[i]) return !0; + if (a[i] > b[i]) return !1; + } + return !1; +} +function orderLanes(g, opts) { + return new LaneOrderer(g, opts).solve(); +} +function snapshotKey(e) { + let c = e.coords, n = c.length / 2, cmp = (i, j) => c[2 * i] - c[2 * j] || c[2 * i + 1] - c[2 * j + 1], flipped = (cmp(0, n - 1) || (n > 2 ? cmp(1, n - 2) : 0)) > 0, v = (i) => { + let k = flipped ? n - 1 - i : i; + return `${c[2 * k].toFixed(9)},${c[2 * k + 1].toFixed(9)}`; + }; + return { key: `${v(0)}|${v(Math.floor(n / 2))}|${v(n - 1)}`, flipped }; +} +function snapshotLaneOrders(g) { + let edges = {}; + for (let e of g.edges) { + let { key, flipped } = snapshotKey(e), baseline = e.baseline === void 0 ? void 0 : flipped ? -e.baseline : e.baseline; + edges[key] = { order: flipped ? e.order.slice().reverse() : e.order.slice(), baseline }; + } + return { version: 3, edges }; +} +function snapshotEntry(snap, e) { + let edges = snap?.version === 3 ? snap.edges : null; + if (!edges || typeof edges != "object") return null; + let { key, flipped } = snapshotKey(e), hit = Object.hasOwn(edges, key) ? edges[key] : null, order = hit?.order; + if (!Array.isArray(order) || !order.every((r) => typeof r == "string") || new Set(order).size !== order.length) return null; + let b = hit.baseline, baseline = typeof b == "number" && Number.isFinite(b) ? flipped ? -b : b : void 0; + return { order: flipped ? order.slice().reverse() : order, baseline }; +} +function applyLaneOrders(g, snap) { + let n = 0; + for (let e of g.edges) { + let hit = snapshotEntry(snap, e), kept = hit ? hit.order.filter((r) => e.routes.includes(r)) : []; + e.order = kept.concat(e.routes.filter((r) => !kept.includes(r)).sort()); + let exact = kept.length === e.routes.length && kept.length === hit?.order.length; + e.baseline = exact ? hit.baseline : void 0, exact && n++; + } + return n; +} +function seedFromSnapshot(g, snap) { + let initial = /* @__PURE__ */ new Map(); + for (let e of g.edges) { + let hit = snapshotEntry(snap, e); + if (!hit) continue; + let kept = hit.order.filter((r) => e.routes.includes(r)); + kept.length && initial.set(e.id, kept); + } + return initial; +} + +// src/core/baselines.ts +function centeredPos(order, r) { + return (order.length - 1) / 2 - order.indexOf(r); +} +function stabilizeLanes(g, opts = {}) { + let maxDrift = opts.maxDrift ?? 0.5, driftWeight = opts.driftWeight ?? 1e-3, iterations = opts.iterations ?? 60, E = g.edges.length, b = new Float64Array(E), terms = g.edges.map(() => []); + for (let node of g.nodes) + for (let t of node.transitions) { + if (t.from < 0 || t.to < 0) continue; + let pu = node.ports[t.from], pv = node.ports[t.to], u = g.edges[pu.edge], v = g.edges[pv.edge]; + if (u === v) continue; + let su = pu.end === "b" ? 1 : -1, sv = pv.end === "a" ? 1 : -1, posU = centeredPos(u.order, t.route), posV = centeredPos(v.order, t.route); + terms[u.id].push({ other: v.id, coef: sv / su, constant: (sv * posV - su * posU) / su }), terms[v.id].push({ other: u.id, coef: su / sv, constant: (su * posU - sv * posV) / sv }); + } + let clamp = (x) => Math.max(-maxDrift, Math.min(maxDrift, x)); + for (let it = 0; it < iterations; it++) { + let moved = 0; + for (let e = 0; e < E; e++) { + let ts = terms[e]; + if (!ts.length) { + b[e] = 0; + continue; + } + let targets = ts.map((t) => ({ v: t.coef * b[t.other] + t.constant, w: 1 })); + targets.push({ v: 0, w: driftWeight }), targets.sort((x, y) => x.v - y.v); + let total = targets.reduce((s, t) => s + t.w, 0), acc = 0, median = 0; + for (let t of targets) + if (acc += t.w, acc >= total / 2) { + median = t.v; + break; + } + let next = clamp(median); + moved += Math.abs(next - b[e]), b[e] = next; + } + if (moved < 1e-9) break; + } + g.edges.forEach((e, i) => e.baseline = b[i]); +} + +// src/core/serialize.ts +function toTransfer(g) { + return { + nodes: g.nodes.map((n) => ({ id: n.id, x: n.x, y: n.y, ports: n.ports, transitions: n.transitions })), + edges: g.edges.map((e) => ({ id: e.id, a: e.a, b: e.b, coords: e.coords, routes: e.routes, order: e.order, direction: [...e.direction], baseline: e.baseline })), + routeIds: [...g.routes.keys()], + routes: [...g.routes.values()], + chains: [...g.chains] + }; +} +function unpackPaths(t) { + let n = t.route.length, paths = new Array(n); + for (let i = 0; i < n; i++) { + let from = t.offsets[i], to = t.offsets[i + 1], coords = new Array(to - from), anchors = new Array(to - from); + for (let k = from; k < to; k++) + coords[k - from] = t.coords[k], anchors[k - from] = t.anchors[k]; + paths[i] = { + route: t.routeIds[t.route[i]], + look: t.looks[t.look[i]], + coords, + anchors, + kind: t.kind[i] === 0 ? "lane" : "connector", + travel: t.travel[i], + startDistance: t.startDistance[i], + edge: t.edge[i], + node: t.node[i] >= 0 ? t.node[i] : void 0, + between: t.betweenU[i] >= 0 ? [t.betweenU[i], t.betweenV[i]] : void 0 + }; + } + return paths; +} + +// src/core/worker-client.ts +var workers = { order: null, layout: null }, failed = { order: !1, layout: !1 }, blobUrl = null, nextId = 1, pending = /* @__PURE__ */ new Map(), kindOf = (req) => req.kind === "layout" || req.kind === "dispose" ? "layout" : "order"; +function settle(p, error) { + let { req } = p; + if (req.kind === "layout") { + let base = { kind: "layout", id: req.id, session: req.session }; + p.resolve(error === null ? { ...base, needGraph: !0 } : { ...base, error }); + } else if (req.kind === "dispose") + p.resolve({ kind: "dispose", id: req.id }); + else { + let base = { kind: "order", id: req.id }; + p.resolve(error === null ? { ...base, disposed: !0 } : { ...base, error }); + } +} +function settleAll(kind, error) { + for (let [id, p] of pending) + kind !== null && p.worker !== kind || (pending.delete(id), settle(p, error)); +} +function getWorker(kind) { + let have = workers[kind]; + if (have) return have; + if (failed[kind] || typeof Worker > "u") return null; + let worker; + try { + blobUrl || (blobUrl = URL.createObjectURL(new Blob(['"use strict";(()=>{var Fn=Object.defineProperty;var Rn=(e,n,t)=>n in e?Fn(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t;var B=(e,n,t)=>Rn(e,typeof n!="symbol"?n+"":n,t);function Fe(e){let n=e.edges.map(o=>({...o,direction:new Map(o.direction??[]),order:o.order.slice()})),t=e.routes?new Map(e.routes.map(o=>[o.id,o])):new Map(e.routeIds.map(o=>[o,{id:o,color:"#000"}]));return{nodes:e.nodes,edges:n,routes:t,chains:new Map(e.chains)}}function Re(e){let n=e.length,t=new Uint32Array(n+1),o=0;for(let l=0;l0)return[t/s,o/s]}return[0,0]}function Y(e){let n=e.length;for(let t=n-4;t>=0;t-=2){let o=e[n-2]-e[t],s=e[n-1]-e[t+1],i=Math.hypot(o,s);if(i>0)return[o/i,s/i]}return[0,0]}function we(e,n,t,o=.2){let s=e.length/2,i=new Float64Array(Math.max(0,s-1)),d=0;for(let f=0;fl){let f=l/(n+t);n*=f,t*=f}let r=$e(e,i,n),c=$e(e,i,d-t),a=[r.x,r.y],u=0;for(let f=0;fn&&u=t){let d=i>0?(t-o)/i:0;return{x:e[s*2]+(e[s*2+2]-e[s*2])*d,y:e[s*2+1]+(e[s*2+3]-e[s*2+1])*d}}o+=i}return{x:e[e.length-2],y:e[e.length-1]}}function ie(e,n){let t=e.length/2;if(t<=2||n<=0)return e.slice();let o=new Uint8Array(t);o[0]=1,o[t-1]=1;let s=n*n,i=[0,t-1];for(;i.length;){let l=i.pop(),r=i.pop(),c=0,a=-1,u=e[r*2],f=e[r*2+1],m=e[l*2],h=e[l*2+1];for(let g=r+1;gc&&(c=b,a=g)}c>s&&a>0&&(o[a]=1,i.push(r,a,a,l))}let d=[];for(let l=0;lr&&(r=g,c=h)}if(r>0&&c>0){let h=rs&&i.push(e[d*2],e[d*2+1]);return i}function Ye(e,n,t,o,s,i){let d=s-t,l=i-o,r=d*d+l*l;if(r>0){let c=Math.max(0,Math.min(1,((e-t)*d+(n-o)*l)/r));d=t+d*c,l=o+l*c}else d=t,l=o;return(e-d)*(e-d)+(n-l)*(n-l)}function He(e,n=1e-9){let t=[];for(let o=0;o=2&&Math.abs(t[s-2]-e[o])0==v>0;if(Math.abs(v)<1e-9*Math.max(1,Math.abs(x))){d.push(b,p),l.push(y,k);continue}if(!M){let N=xe(f,m,h,g,b,p,A,w);N&&Math.hypot(N[0]-y,N[1]-k)<=Tn*Math.abs(n)?(d.push(N[0],N[1]),l.push(y,k)):(d.push(h,g,b,p),l.push(y,k,y,k));continue}let L=Math.atan2(g-k,h-y),C=Math.atan2(p-k,b-y)-L;for(;C>Math.PI;)C-=2*Math.PI;for(;C<-Math.PI;)C+=2*Math.PI;let O=Math.max(1,Math.ceil(Math.abs(C)/t)),U=Math.abs(n);d.push(h,g),l.push(y,k);for(let N=1;N=0&&us||(o?1:-1)*(f*n+m*t)>0)break;c++}if(c=Math.min(c,i.length/2-2),c<=0)return;let a=o?l-2*c:2;i.splice(a,2*c),d.splice(a,2*c)}function ae(e,n,t,o=4,s=[]){let i=He(e),d=i.length/2;if(d<2)return{points:i.slice(),anchors:i.slice()};let l=[i[0],i[1]];for(let m=1;m0&&(u+=Math.hypot(h-l[m*2-2],g-l[m*2-1]));let b=n;if(c>0){let C=0,O=n,U=c,N=t;for(let _=0;_C?O+(N-O)*(u-C)/(U-C):N}let p=0,A=0,w=0,y=0;if(m>0){p=h-l[m*2-2],A=g-l[m*2-1];let C=Math.hypot(p,A)||1;p/=C,A/=C}if(m=1||b<=0||b>=1?null:{x:e+r*g,y:n+c*g,t:g,u:b}}var Nn=.1;function fe(e,n,t){let o=e,s=n,i=new Array(e.length/2);i[0]=0;for(let l=1;l1e3);){let l=o.length/2,r=null;e:for(let h=0;ht);x++){let M=o[x*2],I=o[x*2+1],L=o[x*2+2],R=o[x*2+3];if(My&&L>y||Iv&&R>v)continue;let C=en(g,b,p,A,M,I,L,R);if(C&&!Un(o,s,h,x,C)){r={i:h,j:x,x:C.x,y:C.y,t:C.t,u:C.u};break e}}}if(!r)break;let c=(r.i+1)*2,a=(r.j+1)*2;o=o.slice(0,c).concat([r.x,r.y],o.slice(a));let u=r.i*2,f=r.t;s=s.slice(0,c).concat([s[u]+(s[u+2]-s[u])*f,s[u+1]+(s[u+3]-s[u+1])*f],s.slice(a));let m=i[r.i]+(i[r.i+1]-i[r.i])*f;i=i.slice(0,r.i+1).concat([m],i.slice(r.j+1))}return{points:o,anchors:s}}function Un(e,n,t,o,s){let i=(r,c,a,u,f,m,h)=>{let g=0,b=r,p=c;for(let A=u;A<=f;A++){let w=a[A*2],y=a[A*2+1];g+=b*y-w*p,b=w,p=y}return g+b*h-m*p+m*c-r*h},d=i(s.x,s.y,e,t+1,o,s.x,s.y),l=i(n[t*2]+(n[t*2+2]-n[t*2])*s.t,n[t*2+1]+(n[t*2+3]-n[t*2+1])*s.t,n,t+1,o,n[o*2]+(n[o*2+2]-n[o*2])*s.u,n[o*2+1]+(n[o*2+3]-n[o*2+1])*s.u);return d*l>0&&Math.abs(l)>=Nn*Math.abs(d)}function _n(e,n,t,o,s){let i=[];for(let d=0;d<=s;d++){let l=d/s,r=1-l,c=r*r*r,a=3*r*r*l,u=3*r*l*l,f=l*l*l;i.push(c*e[0]+a*n[0]+u*t[0]+f*o[0],c*e[1]+a*n[1]+u*t[1]+f*o[1])}return i}function Te(e,n,t,o,s,i=1){let d=.5523*i,l=Math.hypot(t[0]-e[0],t[1]-e[1]),r=d*l,c=xe(e[0],e[1],e[0]+n[0],e[1]+n[1],t[0],t[1],t[0]+o[0],t[1]+o[1]);if(c){let f=(c[0]-e[0])*n[0]+(c[1]-e[1])*n[1],m=(c[0]-t[0])*o[0]+(c[1]-t[1])*o[1];if(f>0&&m>0){let h=(Math.hypot(c[0]-e[0],c[1]-e[1])+Math.hypot(c[0]-t[0],c[1]-t[1]))/2;r=d*Math.min(h,l*1.5)}}let a=[e[0]+n[0]*r,e[1]+n[1]*r],u=[t[0]+o[0]*r,t[1]+o[1]*r];return _n(e,a,u,t,s)}function ve(e,n){let t=-(Math.atan2(n[1],n[0])-Math.atan2(e[1],e[0]));for(;t<0;)t+=2*Math.PI;for(;t>=2*Math.PI;)t-=2*Math.PI;return t}function he(e,n,t=10,o=3,s=180,i=0){let d=e.length/2;if(d<3)return e.slice();if(s<180){let f=Math.cos(s*Math.PI/180),m=null;for(let h=1;h0?Gn(h,e,m,i):h}}let l=f=>e[f*2],r=f=>e[f*2+1],c=new Float64Array(d),a=Math.cos(o*Math.PI/180);for(let f=1;f0&&c[f]0?l(f-1):2*l(0)-l(1),p=f>0?r(f-1):2*r(0)-r(1),A=l(f),w=r(f),y=l(f+1),k=r(f+1),v=f+20?[c/u,a/u]:J(e)}function tn(e,n,t=10,o=3,s=180){let i=e.length/2;if(i<3||s<180&&de(e,i-2)0?[a/f,u/f]:Y(e)}function on(e,n,t,o,s){let i=e.length/2,d=Math.cos(s*Math.PI/180);if(!(n>0&&de(e,n)0?t[a-1]:0,m=a+1=e.length-2)continue;let A=Xe(e,p,-2,h,r),w=Xe(e,p,2,h,null);if(h=Math.min(h,A.got,w.got),h<.05)continue;let y=Math.max(4,Math.min(12,Math.round(h/2))),k=[];for(let v=0;v<=y;v++){let x=v/y,M=1-x;k.push(M*M*A.x+2*M*x*g+x*x*w.x,M*M*A.y+2*M*x*b+x*x*w.y)}for(let v=A.idx+2;v=e.length)return{x:e[d],y:e[d+1],idx:d,got:i};let r=s!==null&&t<0&&d===s.idx,c=r?s.x:e[l],a=r?s.y:e[l+1],u=Math.hypot(c-e[d],a-e[d+1]);if(i+u>=o){let f=u>1e-12?(o-i)/u:0;return{x:e[d]+(c-e[d])*f,y:e[d+1]+(a-e[d+1])*f,idx:l,got:o}}i+=u,d=l}}function de(e,n){let t=e[n*2]-e[n*2-2],o=e[n*2+1]-e[n*2-1],s=e[n*2+2]-e[n*2],i=e[n*2+3]-e[n*2+1];return(t*s+o*i)/((Math.hypot(t,o)||1)*(Math.hypot(s,i)||1))}function sn(e,n,t,o,s,i,d,l,r){let a=0+(Math.sqrt(Math.hypot(t-e,o-n))||1e-6),u=a+(Math.sqrt(Math.hypot(s-t,i-o))||1e-6),f=u+(Math.sqrt(Math.hypot(d-s,l-i))||1e-6),m=a+(u-a)*r,h=(a-m)/(a-0)*e+(m-0)/(a-0)*t,g=(a-m)/(a-0)*n+(m-0)/(a-0)*o,b=(u-m)/(u-a)*t+(m-a)/(u-a)*s,p=(u-m)/(u-a)*o+(m-a)/(u-a)*i,A=(f-m)/(f-u)*s+(m-u)/(f-u)*d,w=(f-m)/(f-u)*i+(m-u)/(f-u)*l,y=(u-m)/(u-0)*h+(m-0)/(u-0)*b,k=(u-m)/(u-0)*g+(m-0)/(u-0)*p,v=(f-m)/(f-a)*b+(m-a)/(f-a)*A,x=(f-m)/(f-a)*p+(m-a)/(f-a)*w;return[(u-m)/(u-a)*y+(m-a)/(u-a)*v,(u-m)/(u-a)*k+(m-a)/(u-a)*x]}function rn(e){let n=1/0,t=1/0,o=-1/0,s=-1/0;for(let i=0;io&&(o=e[i]),e[i+1]s&&(s=e[i+1]);return{minX:n,minY:t,maxX:o,maxY:s}}function an(e,n){return e.minX<=n.maxX&&e.maxX>=n.minX&&e.minY<=n.maxY&&e.maxY>=n.minY}function Dn(e){let n=e>>>0;return()=>{n=n+1831565813>>>0;let t=n;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}}var cn=0,ln=1,un=2,Bn=.3,Vn=.15,zn=.25,jn=.1,$n=[0,1,2,3,4,6,8,12,16,24,1/0],dn=[];function Wn(e){let n=dn[e];if(n)return n;n=[];let t=(o,s)=>{if(!o.length){n.push(s);return}for(let i=0;is),[]),dn[e]=n,n}var Ae=class{constructor(n,t={}){B(this,"g");B(this,"w");B(this,"opts");B(this,"routeIds");B(this,"routeIndex");B(this,"laneBuf");B(this,"laneStart");B(this,"laneCount");B(this,"posBuf");B(this,"lanes");B(this,"pos");B(this,"portAt");B(this,"next");B(this,"portBase");B(this,"portEdge");B(this,"nextStart");B(this,"nextList");B(this,"terms");B(this,"seedPos",[]);B(this,"stability",0);B(this,"stack",[]);B(this,"edgeStamp",new Uint32Array(0));B(this,"nodeStamp",new Uint32Array(0));B(this,"stamp",0);B(this,"nodeCache",new Float64Array(0));B(this,"nodeFresh",new Float64Array(0));B(this,"timings",{greedy:0,anneal:0,descend:0});this.g=n,this.opts=t,this.w={sameSegmentCrossing:t.sameSegmentCrossing??4,diffSegmentCrossing:t.diffSegmentCrossing??1,separation:t.separation??3,periphery:t.periphery??.5};let o=new Set;for(let r of n.edges)for(let c of r.routes)o.add(c);this.routeIds=[...o].sort(),this.routeIndex=new Map(this.routeIds.map((r,c)=>[r,c]));let s=this.routeIds.length,i=n.edges.length;this.laneStart=new Int32Array(i+1),this.laneCount=new Int32Array(i);for(let r=0;rthis.laneBuf.subarray(this.laneStart[r.id],this.laneStart[r.id+1])),this.pos=n.edges.map(r=>r.routes.length>1?this.posBuf.subarray(r.id*s,(r.id+1)*s):null),this.portAt=new Int32Array(2*n.edges.length).fill(-1);for(let r of n.nodes)r.ports.forEach((c,a)=>this.portAt[2*c.edge+(c.end==="a"?0:1)]=a);this.next=n.nodes.map(r=>{let c=r.ports.map(()=>[]);for(let a of r.transitions){if(a.from<0||a.to<0||a.from===a.to)continue;let u=this.routeIndex.get(a.route);if(u!==void 0)for(let[f,m]of[[a.from,a.to],[a.to,a.from]]){let h=c[f][u]??(c[f][u]=[]);h.includes(m)||h.push(m)}}return c}),this.portBase=new Int32Array(n.nodes.length+1);for(let r=0;r{o[u]=t.kind.length;let f=a.ports.length,m=a.ports.map(g=>{let b=n.edges[g.edge];return g.end==="a"?J(b.coords):Y(b.coords).map(p=>-p)}),h=new Float64Array(f*f);for(let g=0;gthis.routeIndex.get(v));for(let v=0;v{let o=n.order.length===n.routes.length&&Xn(n.order,n.routes)?n.order:n.routes.slice().sort();this.setLanes(t,o.map(s=>this.routeIndex.get(s)))})}store(){this.g.edges.forEach((n,t)=>n.order=Array.from(this.lanes[t],o=>this.routeIds[o]))}setLanes(n,t){let o=this.lanes[n];o.set(t);let s=this.pos[n];if(s)for(let i=0;i=0&&r=0&&v!==i||p!==n&&d&&d()=0&&k[O]?.includes(y)&&M.push(O)}if(M.length<2)continue;M.reverse();let I=[];for(let C=0;Cx[C]===M[O]))continue;i.push(m),d.push(x.slice());let L=x.slice();if(I.forEach((C,O)=>L[C]=M[O]),this.setLanes(m,L),u>=s)continue;let R=w?r.edges[m].b:r.edges[m].a;for(let C of r.nodes[R].ports)C.edge!==m&&c[C.edge]!==l&&f.push([C.edge,m,R])}a=f}}alignSwaps(n,t){let o=this.g,s=this.terms,i=this.nodeCache,d=this.nodeStamp;for(let f of n.nodes)i[f]=this.evalNode(f);let l=[],r=[],c=[],a=[],u=(f,m,h,g,b)=>{let p=++this.stamp;if(l.length=0,r.length=0,this.alignRun(f,m,h,g,l,r,p),!l.length)return 0;c.length=0,a.length=0;for(let w of l)for(let y of[o.edges[w].a,o.edges[w].b])d[y]!==p&&(d[y]=p,c.push(y));let A=0;for(let w of c){let y=this.evalNode(w);a.push(y),A+=y-i[w]}if(this.stability>0){let w=l.map(y=>this.lanes[y].slice());for(let y of l)A+=this.edgePenalty(y);l.forEach((y,k)=>this.setLanes(y,r[k]));for(let y of l)A-=this.edgePenalty(y);l.forEach((y,k)=>this.setLanes(y,w[k]))}return b?c.forEach((w,y)=>i[w]=a[y]):l.forEach((w,y)=>this.setLanes(w,r[y])),A};for(let f=0;f<400;f++){let m=-1e-9,h=null,g=new Set;for(let b of n.nodes)for(let p=s.start[b];p0)for(let m of i){let h=this.seedPos[m];if(!h||h[r]<0||h[c]<0)continue;let g=this.pos[m];f+=g[r]=0?o:null,u,f);if(x<=0||o()this.lanes[M].slice());else for(let M=0;Mthis.setLanes(p,g[A]))}descendSwaps(n,t,o){let s=this.nodeCache,i=this.nodeFresh;for(let r of n.nodes)s[r]=this.evalNode(r);let d=[],l=[];for(let r of n.edges)if(!(t&&!t[r]))for(let c=0;c+1{for(;o[l]!==l;)l=o[l]=o[o[l]];return l};for(let l of n.nodes){let r=-1;for(let c of l.ports)this.lanes[c.edge].length<2||(r<0?r=c.edge:o[s(c.edge)]=s(r))}let i=new Map;for(let l=0;lc-a)}return d.sort((l,r)=>r.edges.length-l.edges.length||l.edges[0]-r.edges[0])}solve(){this.timings={greedy:0,anneal:0,descend:0};let n=this.opts.initial;if(n&&n.size>0)return this.pass(n,this.opts.stability??1,"seeded");this.pass(null,0,"first");let t=new Map;for(let o of this.g.edges)o.routes.length>1&&t.set(o.id,o.order.slice());return this.pass(t,zn,"second")}pass(n,t,o){let s=performance.now(),i=this.g,d=Dn(this.opts.seed??42),l=!!n;this.seedPos=i.edges.map(()=>null),this.stability=l?t:0;for(let g of i.edges){let b=n?.get(g.id)?.filter(p=>g.routes.includes(p));if(!b||!b.length){g.order=g.routes.slice().sort();continue}if(g.order=b.concat(g.routes.filter(p=>!b.includes(p)).sort()),g.routes.length>1){let p=new Int32Array(this.routeIds.length).fill(-1);b.forEach((A,w)=>p[this.routeIndex.get(A)]=w),this.seedPos[g.id]=p}}this.load(),this.edgeStamp=new Uint32Array(i.edges.length),this.nodeStamp=new Uint32Array(i.nodes.length),this.nodeCache=new Float64Array(i.nodes.length),this.nodeFresh=new Float64Array(i.nodes.length),this.stamp=0;let r=new Set(l?i.edges.map(g=>g.id):[]),c=i.edges.slice().sort((g,b)=>b.routes.length-g.routes.length||g.id-b.id),a=[];for(let g of c)if(!r.has(g.id))for(a.push(g.id);a.length;){let b=a.shift();if(r.has(b))continue;r.add(b),this.optimizeEdge(b);let p=i.edges[b];for(let A of[p.a,p.b])for(let w of i.nodes[A].ports)r.has(w.edge)||a.push(w.edge)}let u=this.timings;u.greedy+=performance.now()-s;let f=this.components(),m=f.reduce((g,b)=>g+b.edges.length,0),h=this.opts.annealMoves??Math.max(1e4,400*m);for(let g of f){let b=performance.now(),p=1/0,A=[];l&&(p=o==="second"?this.componentCost(g):this.descend(g),A=g.edges.map(k=>this.lanes[k].slice())),u.descend+=performance.now()-b,b=performance.now();let w=[];o==="second"?(this.stability=jn,this.alignSwaps(g,w),this.stability=t):p>0&&this.anneal(g,Math.ceil(h*g.edges.length/m),d,o==="seeded"),u.anneal+=performance.now()-b,b=performance.now(),o==="second"?this.polish(g,w,!0):o==="seeded"&&this.polish(g,null,!0);let y=o==="second"?this.componentCost(g):this.descend(g);ythis.lanes[k].slice())):A.forEach((k,v)=>this.setLanes(g.edges[v],k)),u.descend+=performance.now()-b}return this.store(),this.evalAll()}};function Ne(e,n,t,o,s,i,d,l,r){e.kind.push(n),e.edge.push(t),e.out.push(o),e.a.push(s),e.b.push(i),e.next.push(d),e.nextOut.push(l),e.aLeft.push(r)}function Xn(e,n){if(e.length!==n.length)return!1;let t=new Set(e);return n.every(o=>t.has(o))}function Zn(e,n){for(let t=0;tn[t])return!1}return!1}function fn(e,n){return(e.length-1)/2-e.indexOf(n)}function hn(e,n={}){let t=n.maxDrift??.5,o=n.driftWeight??.001,s=n.iterations??60,i=e.edges.length,d=new Float64Array(i),l=e.edges.map(()=>[]);for(let c of e.nodes)for(let a of c.transitions){if(a.from<0||a.to<0)continue;let u=c.ports[a.from],f=c.ports[a.to],m=e.edges[u.edge],h=e.edges[f.edge];if(m===h)continue;let g=u.end==="b"?1:-1,b=f.end==="a"?1:-1,p=fn(m.order,a.route),A=fn(h.order,a.route);l[m.id].push({other:h.id,coef:b/g,constant:(b*A-g*p)/g}),l[h.id].push({other:m.id,coef:g/b,constant:(g*p-b*A)/b})}let r=c=>Math.max(-t,Math.min(t,c));for(let c=0;c({v:A.coef*d[A.other]+A.constant,w:1}));m.push({v:0,w:o}),m.sort((A,w)=>A.v-w.v);let h=m.reduce((A,w)=>A+w.w,0),g=0,b=0;for(let A of m)if(g+=A.w,g>=h/2){b=A.v;break}let p=r(b);a+=Math.abs(p-d[u]),d[u]=p}if(a<1e-9)break}e.edges.forEach((c,a)=>c.baseline=d[a])}var Jn=1/5,gn=-.5,Pe=2,Yn=.15,Hn=.1,mn=.01;function Kn(e,n,t,o,s){let i=e.length,d=16;for(;d<2*i;)d*=2;let l=(h,g)=>(Math.imul(h,73856093)^Math.imul(g,19349663))&d-1,r=new Int32Array(i),c=new Int32Array(d+1);for(let h=0;h=c[y];v--){let x=a[v];if(x-h=qn))return e;let i=Math.PI/2*n,d=0;for(let S=2;SMath.PI?D-=2*Math.PI:D<-Math.PI&&(D+=2*Math.PI),S+=D,SE&&(E=S),F=G}if(E-PP+E;)P+=E,S++,E=Math.hypot(e[S*2]-e[S*2-2],e[S*2+1]-e[S*2-1]);let G=E>0?Math.min(1,(T-P)/E):0;a[F]=e[S*2-2]+(e[S*2]-e[S*2-2])*G,u[F]=e[S*2-1]+(e[S*2+1]-e[S*2-1])*G}}let f=new Uint8Array(c),m=[];if(Kn(a,u,n,Math.floor(i/r)+1,(S,P,E)=>{let F=Math.max(0,S-Pe),T=Math.min(c-1,S+Pe),G=Math.max(0,P-Pe),D=Math.min(c-1,P+Pe),V=a[T]-a[F],z=u[T]-u[F],j=a[D]-a[G],$=u[D]-u[G];if(V*j+z*$>gn*Math.hypot(V,z)*Math.hypot(j,$))return;let X=a[P]-a[S],K=u[P]-u[S];m.push(S,P,E),f[S]|=V*K-z*X>0?1:2,f[P]|=$*X-j*K>0?1:2}),!m.length)return e;let h=[];for(let S=0;Sm[S+2]-m[P+2]);let g=n*n,b=(S,P)=>(a[S]-a[P])*(a[S]-a[P])+(u[S]-u[P])*(u[S]-u[P]),p=new Uint8Array(c),A=[];for(let S of h){let P=m[S],E=m[S+1];if(p[P]===1||p[E]===1||p[P]&&p[E])continue;let F=P,T=E,G=-1;for(;;){if(E-P<=2){G=P+E>>1;break}let $=b(P+1,E-1),X=b(P+1,E),K=b(P,E-1),H=Math.min($,X,K);if(H>=g)break;$===H?(P++,E--):X===H?P++:E--}G>=0&&(P=G-1,E=G+1);let D={ai:[],bi:[],tip:G},V=P<0||E>c-1;for(;!V;){if(p[P]===1||p[E]===1){V=!0;break}D.ai.push(P),D.bi.push(E);let $=P>0&&E0?b(P-1,E):1/0,K=E=g)break;$===H?(P--,E++):X===H?P--:E++}if(V||!D.ai.length){p[F]||(p[F]=2),p[T]||(p[T]=2);continue}let z=D.ai[D.ai.length-1],j=D.bi[D.bi.length-1];G>=0?p.fill(1,z,j+1):(p.fill(1,z,D.ai[0]+1),p.fill(1,D.bi[0],j+1)),A.push(D)}let w=n/2,y=S=>{let P=S*r,E=Math.min((P-t)/n,(d-P-o)/n);return E<=0?0:E>=1?1:E*E*(3-2*E)},k=new Float64Array(c),v=new Float64Array(c),x=new Uint8Array(c),M=!1;for(let S of A){let P=!1;for(let F=0;F=0){let F=a[S.tip],T=u[S.tip],G=0,D=F,V=T,z=0;for(;G=w){let ze=(w-z)/(Oe-z);D+=(Z-D)*ze,V+=(re-V)*ze;break}D=Z,V=re,z=Oe}if(G>=S.ai.length||y(S.ai[G])<1||y(S.bi[G])<1)continue;let j=S.ai[G],$=S.bi[G],X=Math.hypot(a[j]-a[$],u[j]-u[$])/2;if(X<1e-6)continue;let K=(a[j]-a[$])/(2*X),H=(u[j]-u[$])/(2*X),En=(K*(T-V)-H*(F-D)>0?1:-1)*Math.PI,On=Math.atan2(H,K);for(let Z=j;Z<=$;Z++){let re=On+En*(Z-j)/($-j);k[Z]=D+Math.cos(re)*w-a[Z],v[Z]=V+Math.sin(re)*w-u[Z],x[Z]=1}M=!0,E=G+1}for(let F=E;F=n||D<1e-6)continue;let V=(n-D)/2,z=(a[T]-a[G])/D,j=(u[T]-u[G])/D;!x[T]&&Math.hypot(k[T],v[T])1e-9?Math.max(P,E)/E:0;k[S]=L[S]*F,v[S]=R[S]*F}let C=new Uint8Array(c);for(let S=0;S{for(;U0){let E=Math.hypot(e[U*2]-e[U*2-2],e[U*2+1]-e[U*2-1]);if(N+E>S)return;N+=E}let P=Math.min(c-1,Math.floor(N/r));!C[P]&&!C[Math.min(c-1,P+1)]&&O.push(e[U*2],e[U*2+1])}};for(let S=0;So.has(i)?t[s++]:i)}function ot(e){let n="";for(let s of e.edges)s.order.length>1&&(n+=`${s.id}:${s.order.join(",")};`);let t=bn.get(e);if(t&&t.signature===n)return t;let o={signature:n,order:st(e)};return bn.set(e,o),o}function st(e){let n=[],t=[],o=new Map;for(let h of e.nodes){if(h.ports.length<2)continue;let b=h.ports.map(v=>{let x=e.edges[v.edge];return v.end==="a"?J(x.coords):Y(x.coords).map(M=>-M)}),p=(v,x)=>ve(b[v],b[x]),A=(v,x)=>{let M=h.ports[v],I=e.edges[M.edge].order,L=I.indexOf(x);return M.end==="a"?L:I.length-1-L},w=h.transitions.filter(v=>v.from>=0&&v.to>=0&&v.from!==v.to),y=new Map;for(let v of w){let x=Math.min(v.from,v.to),M=Math.max(v.from,v.to),I=`${x}:${M}`,L=y.get(I);L||y.set(I,L={u:x,v:M,routes:[]}),L.routes.includes(v.route)||L.routes.push(v.route)}for(let v of y.values())if(!(v.routes.length<2))for(let x of w){if(v.routes.includes(x.route))continue;let M=[x.from,x.to].filter(L=>L===v.u||L===v.v),I=[];if(M.length){if(M.length===1){let L=M[0],R=x.from===L?x.to:x.from,C=v.u===L?v.v:v.u,O=p(L,R)U1&&n.push({group:I,across:x.route})}let k=new Set(w.map(v=>v.route));h.ports.forEach((v,x)=>{for(let M of e.edges[v.edge].routes){if(w.some(L=>L.route===M&&(L.from===x||L.to===x)))continue;let I=!1;for(let L of k)L===M||!h.ports.some((R,C)=>C!==x&&e.edges[R.edge].routes.includes(L))||(t.push([M,L]),I=!0);I&&o.set(M,(o.get(M)??0)+1)}})}if(!n.length)return[];let s=new Set;for(let h of n){s.add(h.across);for(let g of h.group)s.add(g)}if(s.size>Qn)return[];let i=[...s].sort((h,g)=>(o.get(g)??0)-(o.get(h)??0)||(hg?1:0)),d=new Map(i.map((h,g)=>[h,g])),l=i.length,r=i.map(()=>[]);for(let h of n){let g=0;for(let p of h.group)g|=1<0);for(let[h,g]of t){let b=d.get(h),p=d.get(g);b!==void 0&&p!==void 0&&(c[b]|=1<typeof s=="number"&&Number.isFinite(s)&&s>=i;if(!t||!o(t.spacing,0)||!o(t.width,0)||!o(t.casingWidth,0)||t.spacing===0||t.width===0){let s=t&&typeof t=="object"?JSON.stringify({spacing:t.spacing,width:t.width,casingWidth:t.casingWidth},(i,d)=>d===void 0?"undefined":typeof d=="number"&&!Number.isFinite(d)?String(d):d):String(t);throw new TypeError(`maplibre-gl-lanes: \\`sizes(${n})\\` must return {spacing, width, casingWidth} as finite numbers in px, with spacing and width above 0; got ${s}`)}return t}function bt(e){if(typeof e!="function")throw new TypeError("maplibre-gl-lanes: `sizes` must be a function of the zoom that returns {spacing, width, casingWidth}")}function vn(e,n,t,o={}){let s=performance.now(),i=o.smooth??!0,d=o.openFolds??!0,l=o.laneStyle??null,r=pt(t,n),c=o.cache?o.cache.stateFor(e,n,r,i,d,l):An(e,n,r,i,d,l),a=o.bounds??null,u=o.routes?new Set(o.routes):null,f=new Uint8Array(e.edges.length);for(let p of e.edges){if(!a){f[p.id]=1;continue}p.bounds||(p.bounds=rn(p.coords)),f[p.id]=an(p.bounds,a)?1:0}let m=[],h=new Map,g=0,b=0;for(let p of e.edges)if(!(!f[p.id]||c.short[p.id])){b++;for(let A of p.routes){if(u&&!u.has(A))continue;let w=Le(c,p.id,A);w&&(h.set(te(c,p.id,A),w),g+=w.coords.length/2,m.push(w))}}for(let p=0;pf[v.edge])||(Be(c,w),w.mode!==1||u&&!u.has(y.route)))continue;let k=w.slide;g+=k.coords.length/2,m.push(k)}for(let w of A)if(!(w.mode===1||u&&!u.has(w.tail.route)))for(let{edge:y}of w.tail.edges){let k=te(c,y,w.tail.route);if(!f[y]||h.has(k))continue;let v=Le(c,y,w.tail.route);v&&(h.set(k,v),g+=v.coords.length/2,m.push(v))}for(let w of c.junctions[p].islands)if(!(u&&!u.has(w.route)||!w.steps.some(y=>f[y.edge])))for(let y of Pn(c,w)){if(y.kind==="lane"){let k=te(c,y.edge,w.route);if(h.has(k))continue;h.set(k,y)}g+=y.coords.length/2,m.push(y)}}for(let p=0;p-T)}let A=P=>(P.routes.length-1)/2*d+l/2+r,w=(P,E,F)=>{let T=m[P.id*f+u.get(E)];return F?T:-T},y=new Uint8Array(a),k=P=>e.nodes[P].ports.length,v=new Float64Array(e.nodes.length);for(let P of e.nodes){let E=0;for(let F of P.ports)E=Math.max(E,e.edges[F.edge].routes.length);v[P.id]=E*d}let x=[],M=new Float64Array(2*a),I=new Float64Array(2*a),L=new Uint8Array(2*a*f);for(let P=0;P{for(let T of P.ports)if(T.edge!==E){for(let G of e.edges[T.edge].routes)if(G!==F)return!0}return!1};for(let P of e.edges)if(!y[P.id])for(let E of P.routes){let F=u.get(E);for(let T=0;T<2;T++){let G=e.nodes[T===0?P.a:P.b];!L[(2*P.id+T)*f+F]&&N(G,P.id,E)&&U.set(E,(U.get(E)??0)+1)}}let _=yn(e,[...e.routes.keys()].sort((P,E)=>(U.get(E)??0)-(U.get(P)??0)||(PE?1:0)));return{graph:e,zoom:n,scale:c,spacing:d,width:l,casing:r,smooth:o,openFolds:s,laneStyle:i,looks:new Map,routeIndex:u,routeCount:f,offsets:m,simplePx:h,edgeLen:g,dirA:b,dirB:p,short:y,junctions:x,groups:new Array(x.length),tails:new Array(x.length),continues:L,frontA:R,frontB:C,mergedEdges:O,drawOrder:_,preparedPx:new Array(a),lanes:new Array(a*f),lanePaths:new Array(a*f),connPaths:new Map,phased:new Set}}function wn(e,n){for(let t of e.edges)if(n[t])return!0;return!1}function Ue(e,n){let t=e.groups[n];if(t)return{groups:t,tails:e.tails[n]};let o=e.graph,s=e.junctions[n],i=new Map,d=[];for(let c of s.transitions){let a=s.ports[c.u],u=s.ports[c.v],f=oe(e,a.edge,c.route,a.end==="b"),m=oe(e,u.edge,c.route,u.end==="a"),h=ce(o.edges[a.edge].direction.get(c.route))*(a.end==="b"?1:-1),g=ce(o.edges[u.edge].direction.get(c.route))*(u.end==="a"?1:-1),b=`${a.edge}:${u.edge}:${(f-m).toFixed(3)}`,p=i.get(b);p||(p={edgeU:a.edge,edgeV:u.edge,members:[]},i.set(b,p),d.push(p)),p.members.push({route:c.route,key:ge(e,c.route,a.edge,a.end,u.edge,u.end),pu:a,pv:u,lateral:f,lateralOut:m,travel:h||g,via:c.via})}let l=new Map;for(let c of d)for(let a of c.members)for(let u of a.via){let f=`${a.route}|${u.edge}`,m=l.get(f);m?m.push(a):l.set(f,[a])}for(let c of l.values())if(c.length>1)for(let a of c)a.forked=!0;let r=[];for(let c of s.tails){let a=s.ports[c.port],u=c.edges[c.side==="end"?c.edges.length-1:0],f=c.edges[c.side==="end"?0:c.edges.length-1],m=ce(o.edges[a.edge].direction.get(c.route))*(c.side==="end"?a.end==="b"?1:-1:a.end==="a"?1:-1),h={tail:c,port:a,mode:0,key:c.side==="end"?ge(e,c.route,a.edge,a.end,u.edge,u.forward?"b":"a"):ge(e,c.route,u.edge,u.forward?"a":"b",a.edge,a.end)};r.push(h);let g=c.side==="end"?a.edge:f.edge,b=c.side==="end"?f.edge:a.edge;d.push({edgeU:g,edgeV:b,members:[{route:c.route,pu:a,pv:a,lateral:0,lateralOut:0,travel:m,via:[],plan:h,key:c.side==="end"?ge(e,c.route,g,a.end,b,f.forward?"a":"b"):ge(e,c.route,g,f.forward?"b":"a",b,a.end)}]})}return e.groups[n]=d,e.tails[n]=r,{groups:d,tails:r}}function te(e,n,t){return n*e.routeCount+e.routeIndex.get(t)}function ge(e,n,t,o,s,i){let d=2*e.graph.edges.length;return((t*2+(o==="b"?1:0))*d+s*2+(i==="b"?1:0))*e.routeCount+e.routeIndex.get(n)}function oe(e,n,t,o){let s=e.offsets[te(e,n,t)];return o?s:-s}function ke(e,n,t,o){return e.continues[(2*n+t)*e.routeCount+e.routeIndex.get(o)]!==0}function Me(e,n,t){let o=e.graph.routes.get(t),s=e.laneStyle?e.laneStyle(n,t):null,i=s?.color||o?.color||"#888",d=s?.dash??o?.dash,l=s?.dashColor??o?.dashColor,r=s?.dashCap??o?.dashCap,c=`${i}|${d?d[0]+","+d[1]:""}|${l??""}|${r??""}`,a=e.looks.get(c);return a||e.looks.set(c,a={color:i,dash:d,dashColor:l,dashCap:r}),a}function se(e,n){let t=e.preparedPx[n];if(!t){let o=e.simplePx[n];t=e.smooth?ie(he(o,me,10,3,pe,Ie*e.spacing),Se):o;let s=e.graph.edges[n];if(e.openFolds&&s.routes.length>1&&!e.short[n]){let i=((s.routes.length-1)/2+Math.abs(s.baseline??0))*e.spacing;t=pn(t,2*i+e.width+2*e.casing,e.frontA[n]+e.spacing,e.frontB[n]+e.spacing)}e.preparedPx[n]=t}return t}function De(e,n,t){let o=se(e,n);return t?o:W(o)}function le(e,n,t){let o=te(e,n,t),s=e.lanes[o];if(s!==void 0)return s;let i=e.graph,d=i.edges[n],l=se(e,n),r;if(e.short[n])r=q(l,e.offsets[o]);else{let c=ke(e,n,0,t)?e.frontA[n]:0,a=ke(e,n,1,t)?e.frontB[n]:0,u=c>0||a>0?we(l,c,a):l;r=d.order.length===1&&e.offsets[o]!==0?yt(u,e.offsets[o],ke(e,n,0,t),ke(e,n,1,t),lt*e.spacing,e.spacing):q(u,e.offsets[o]);let f=i.nodes[d.a],m=i.nodes[d.b];c>0&&(r.anchors[0]=f.x*e.scale,r.anchors[1]=f.y*e.scale),a>0&&(r.anchors[r.anchors.length-2]=m.x*e.scale,r.anchors[r.anchors.length-1]=m.y*e.scale)}return e.lanes[o]=r,r}function yt(e,n,t,o,s,i){if(!t&&!o)return q(e,0);let d=t?i:0,l=o?i:0,r=Q(e)-d-l;if(r<1)return q(e,n);let c=we(e,d,l,0),a=o?s:Math.min(s,r),u=t?s:Math.min(s,r),f=y=>1-y*y*(3-2*y),m=y=>n*Math.max(t?f(Math.min(1,y/a)):0,o?f(Math.min(1,(r-y)/u)):0),h=(y,k,v)=>{let x=Math.max(1,Math.ceil((v-k)/s*ut)),M=[];for(let I=1;I0&&g.push(q(ye(e,d),n));let b=t?a:0,p=o?u:0;b+p>r-1?g.push(h(c,0,r)):(b>0&&g.push(h(ye(c,b),0,b)),g.push(q(we(c,b,p,0),0)),p>0&&g.push(h(ue(c,p),r-p,r))),l>0&&g.push(q(ue(e,l),n));let A=[],w=[];return g.forEach((y,k)=>{for(let v=k>0?2:0;vDe(e,w.edge,w.forward)),l=o.edges[o.side==="end"?o.edges.length-1:0],r=oe(e,l.edge,o.route,l.forward),c=oe(e,s.edge,o.route,o.side==="end"?s.end==="b":s.end==="a"),a=se(e,s.edge),u=o.side==="end"?ue(s.end==="b"?a:W(a),s.end==="b"?e.frontB[s.edge]:e.frontA[s.edge]):ye(s.end==="a"?a:W(a),s.end==="a"?e.frontA[s.edge]:e.frontB[s.edge]),f=o.side==="end"?_e(e,[u,...d]):_e(e,[...d,u]),m=o.side==="end"?ae(f,c,r):ae(f,r,c);if(m.points.length<4)return;let h=o.side==="end"?s.end==="b"?i.points:W(i.points):s.end==="a"?i.points:W(i.points);o.side==="end"?(m.points[0]=h[h.length-2],m.points[1]=h[h.length-1]):(m.points[m.points.length-2]=h[0],m.points[m.points.length-1]=h[1]);let{points:g,anchors:b}=fe(m.points,m.anchors,xn*e.spacing);if(g.length<4)return;let p=ce(t.edges[s.edge].direction.get(o.route))*(o.side==="end"?s.end==="b"?1:-1:s.end==="a"?1:-1),A=o.side==="end"?[s.edge,l.edge]:[l.edge,s.edge];n.slide={route:o.route,look:Me(e,t.edges[s.edge],o.route),coords:g,anchors:b,kind:"connector",travel:p,startDistance:0,edge:A[0],node:s.node,between:A},e.connPaths.set(n.key,n.slide),n.mode=1}function Pn(e,n){if(n.paths)return n.paths;let t=e.graph,o=n.steps[0],s=n.steps[n.steps.length-1],i=_e(e,n.steps.map(a=>De(e,a.edge,a.forward))),d=oe(e,o.edge,n.route,o.forward),l=oe(e,s.edge,n.route,s.forward),r=ae(i,d,l);if(r.points.length>=4){let a=t.edges[o.edge],u=ce(a.direction.get(n.route))*(o.forward?1:-1);return n.paths=[{route:n.route,look:Me(e,a,n.route),coords:r.points,anchors:r.anchors,kind:"connector",travel:u,startDistance:0,edge:o.edge,node:o.forward?a.a:a.b,between:[o.edge,s.edge]}]}let c=[];for(let a of n.steps){let u=Le(e,a.edge,n.route);u&&c.push(u)}return n.paths=c}function Mt(e,n){let t=e.graph,o=new Map;for(let w of t.chains.get(n)??[])for(let y of w.steps)o.set(y.edge,y.forward);let s=w=>o.get(w)===!1?"a":"b",i=[],d=new Map,l=[],r=(w,y)=>{w&&!d.has(w)&&(d.set(w,i.length),i.push(w),l.push(y))},c=w=>r(Le(e,w,n),o.get(w)!==!1);for(let w of t.edges)!e.short[w.id]&&w.routes.includes(n)&&c(w.id);for(let w=0;wx.route===n)){kn(e,v);for(let x of v.members)x.route!==n||x.plan?.mode===1||r(e.connPaths.get(x.key),!!x.plan||x.pu.end===s(x.pu.edge))}for(let v of e.junctions[w].islands)if(v.route===n)for(let x of Pn(e,v))r(x,!0)}if(!i.length)return;let a=i.length,u=new Map,f=new Array(2*a);for(let w=0;w{let v=m[w];v<0&&(m[w]=v=Q(i[w].coords));let x=l[w]?y?k-v:k:y?k+v:k;h[w*2]=x,h[w*2+1]=l[w]?y?k:k+v:y?k:k-v,g[w]=1},p=0;e:for(let w of t.chains.get(n)??[])for(let y of w.steps){let k=e.lanePaths[te(e,y.edge,n)];if(k){p=d.get(k);break e}}let A=[];for(let w=-1;w=0){if(g[w])continue;p=w}b(p,0,0),A.length=0,A.push(p);for(let k=0;k>1;g[L]||(b(L,I&1,M),A.push(L))}}}let y=0;for(let k of A)y=Math.min(y,h[k*2],h[k*2+1]);for(let k of A){let v=h[k*2]-y;i[k].startDistance=l[k]?v:-v}}}function kn(e,n){let t=n.members[0].plan;if(t&&(Be(e,t),t.mode===1))return;let o=!1;for(let c of n.members)e.connPaths.has(c.key)||(o=!0);if(!o)return;let s=e.graph,i=[],d=new Map,l=!1;for(let c of n.members){if(e.connPaths.set(c.key,null),c.plan)continue;let a=le(e,c.pu.edge,c.route),u=le(e,c.pv.edge,c.route),f=c.pu.end==="b"?a.points:W(a.points),m=c.pv.end==="a"?u.points:W(u.points),h=c.via.length?null:Ge(f,m,e.spacing);d.set(c,{from:f,to:m,made:h}),h&&h.coords.length>=4&&wt(e,c,h.coords)&&(l=!0)}for(let c of n.members){if(c.plan){xt(e,c,i);continue}let{from:a,to:u,made:f}=d.get(c);if((c.via.length||l)&&vt(e,c,a,u))continue;let m=f??Ge(a,u,e.spacing);m.coords.length<4||i.push({member:c,lateral:c.lateral,own:m.coords,coords:m.coords,corner:m.corner})}let r=i.filter(c=>!c.corner);r.length>1&&kt(r,e.spacing);for(let c of i){let a=c.member,u=c.coords,f=s.nodes[a.pu.node],m=new Array(u.length);for(let h=0;h{for(let p=0;pu.push(a[k]/b,oe(e,y.edge,n.route,y.forward)));let p=y=>n.lateral+(n.lateralOut-n.lateral)*y,A=y=>{let k=0,v=n.lateral,x=0;for(let M=0;M<=u.length;M+=2){let I=Mk&&(x=Math.max(x,Math.abs(L-v)/((I-k)*b))),k=I,v=L}return x},w=1;if(A(1)>Mn){let y=0,k=1;for(let v=0;v<12;v++){let x=(y+k)/2;A(x)>Mn?k=x:y=x}w=y}for(let y=0;y{for(;l[x]!==x;)x=l[x]=l[l[x]];return x};for(let x of e.edges)n[x.id]&&(l[r(x.a)]=r(x.b));let c=new Map,a=new Array(d),u=new Int32Array(2*e.edges.length).fill(-1);for(let x=0;x{let L=[],R=(C,O)=>{f.push({edge:C,entryNode:O}),m[C]++;let U=e.edges[C],N=U.a===O?U.b:U.a,_=e.nodes[N],S=!1,P=null;for(let E of _.transitions){if(E.route!==x||E.from<0||E.to<0)continue;let F=_.ports[E.from],T=_.ports[E.to],G=F.edge===C&&T.edge!==C?T:T.edge===C&&F.edge!==C?F:null;G&&(n[G.edge]&&m[G.edge]||(S=!0,n[G.edge]?R(G.edge,N):L.push({edge:G.edge,end:G.end,node:N,walked:P??(P=f.slice())})))}S||L.push({edge:-1,end:"a",node:N,walked:P??(P=f.slice())}),m[C]--,f.pop()};return R(M,I),L},g=new Map,b=new Set;for(let x of e.nodes){let M=a[x.id],I=r(x.id);for(let L of x.transitions){if(L.from<0||L.to<0)continue;let R=x.ports[L.from],C=x.ports[L.to];if(!n[R.edge]&&!n[C.edge]){let _=u[R.edge*2+(R.end==="a"?0:1)],S=u[C.edge*2+(C.end==="a"?0:1)];_>=0&&S>=0&&M.transitions.push({route:L.route,u:_,v:S,via:[]});continue}if(n[R.edge]&&n[C.edge])continue;let O=!n[R.edge],U=O?R:C,N=u[U.edge*2+(U.end==="a"?0:1)];if(!(N<0))for(let _ of h(L.route,(O?C:R).edge,x.id)){let S=_.walked.map(V=>({edge:V.edge,forward:e.edges[V.edge].a===V.entryNode})),P=O?S:S.reverse().map(V=>({edge:V.edge,forward:!V.forward}));if(_.edge<0){let V=`${I}|${L.route}|${N}|${P.map(z=>z.edge).join(",")}`;if(b.has(V))continue;b.add(V),M.tails.push({route:L.route,port:N,side:O?"end":"start",edges:P});continue}let E=u[_.edge*2+(_.end==="a"?0:1)];if(E<0||a[_.node]!==M)continue;let F=`${I}|${L.route}|${Math.min(N,E)}|${Math.max(N,E)}`,T=P.reduce((V,z)=>V+t[z.edge],0),G=O?{route:L.route,u:N,v:E,via:P}:{route:L.route,u:E,v:N,via:P},D=g.get(F);if(D){T{let M=p.get(x);return M===void 0&&p.set(x,M=p.size),M};for(let x of e.routes.keys())A(x);for(let x of e.chains.keys())A(x);for(let x of c.values()){for(let M of x.transitions)A(M.route);for(let M of x.tails)A(M.route)}let w=p.size,y=new Uint8Array(e.edges.length*w);for(let x of c.values()){for(let M of x.transitions){let I=p.get(M.route);for(let L of M.via)y[L.edge*w+I]=1}for(let M of x.tails){let I=p.get(M.route);for(let L of M.edges)y[L.edge*w+I]=1}}let k=(x,M)=>x.forward===M?e.edges[x.edge].a:e.edges[x.edge].b,v=(x,M,I,L)=>{let R=new Set,C=e.nodes[I];for(let O of C.transitions){if(O.route!==M||O.from<0||O.to<0)continue;let U=C.ports[O.from],N=C.ports[O.to];U.edge===L&&n[N.edge]&&R.add(N.edge),N.edge===L&&n[U.edge]&&R.add(U.edge)}if(!R.size)return-1;for(let O of C.transitions){if(O.route!==M||O.from<0||O.to<0)continue;let U=C.ports[O.from],N=C.ports[O.to],_=R.has(N.edge)&&!n[U.edge]?U:R.has(U.edge)&&!n[N.edge]?N:null;if(_&&_.edge!==L)return u[_.edge*2+(_.end==="a"?0:1)]}return-1};for(let[x,M]of e.chains){let I=p.get(x);for(let L of M){let R=L.steps;for(let C=0;C({edge:F.edge,forward:F.forward}));for(let F of U)y[F.edge*w+I]=1;let N=U[0],_=U[U.length-1],S=a[k(N,!0)],P=v(S,x,k(N,!0),N.edge),E=v(S,x,k(_,!1),_.edge);P>=0&&E>=0&&P!==E?S.transitions.push({route:x,u:P,v:E,via:U}):P>=0?S.tails.push({route:x,port:P,side:"end",edges:U}):E>=0?S.tails.push({route:x,port:E,side:"start",edges:U}):S.islands.push({route:x,steps:U}),C=O-1}}}return[...c.values()]}function Pt(e,n,t,o,s,i){let d=new Float64Array(2*e.edges.length),l=new Float64Array(2*e.edges.length),r=new Uint8Array(2*e.edges.length*i),c=(a,u)=>{r[(2*a.edge+(a.end==="a"?0:1))*i+s.get(u)]=1};for(let a of n){let u=a.ports,f=new Float64Array(u.length),m=new Uint8Array(u.length);for(let g of a.transitions)m[g.u]=1,m[g.v]=1,c(u[g.u],g.route),c(u[g.v],g.route);for(let g of a.tails)m[g.port]=1,c(u[g.port],g.route);for(let g=0;g0?6*o:0:w=(u[b].hw+u[g].hw*p)/A,w=Math.max(0,w),w>0&&(w+=it*o),f[g]=Math.max(f[g],Math.min(w,8*o))}let h=new Float64Array(u.length);for(let g of a.transitions){let b=u[g.u],p=u[g.v],A=t(e.edges[b.edge],g.route,b.end==="b"),w=t(e.edges[p.edge],g.route,p.end==="a"),y=Math.abs(A-w);if(y<1e-6)continue;let k=Math.max(2.5*y,3*o);h[g.u]=Math.max(h[g.u],k/2),h[g.v]=Math.max(h[g.v],k/2)}for(let g=0;gl[2*g.edge+(g.end==="a"?0:1)]=h[b]);for(let g of a.tails){let b=u[g.port],p=g.edges[g.side==="end"?0:g.edges.length-1],A=t(e.edges[b.edge],g.route,b.end==="b"),w=t(e.edges[p.edge],g.route,p.forward),y=Math.abs(A-w);y<1e-6||(f[g.port]=Math.max(f[g.port],Math.max(2.5*y,3*o)/2))}u.forEach((g,b)=>d[2*g.edge+(g.end==="a"?0:1)]=f[b])}return{need:d,change:l,continues:r}}function ue(e,n){let t=e.length/2;if(n<=0||t<2)return[e[e.length-2],e[e.length-1]];let o=[],s=n;for(let i=t-1;i>0;i--){let d=e[i*2-2],l=e[i*2-1],r=e[i*2],c=e[i*2+1],a=Math.hypot(r-d,c-l);if(o.unshift(r,c),a>=s){let u=1-s/a;return o.unshift(d+(r-d)*u,l+(c-l)*u),o}s-=a}return o.unshift(e[0],e[1]),o}function ye(e,n){return W(ue(W(e),n))}function ce(e){return e===1?1:e===-1?-1:0}function kt(e,n){let t=e[0],o=Q(t.own);for(let i of e){let d=Q(i.own);d>o&&(t=i,o=d)}if(t.own.length<=4||o1.1||c<.9)continue;let a=Math.hypot(r[0]-i.own[0],r[1]-i.own[1]),u=Math.hypot(r[r.length-2]-i.own[i.own.length-2],r[r.length-1]-i.own[i.own.length-1]);a>n/2||u>n/2||(r[0]=i.own[0],r[1]=i.own[1],r[r.length-2]=i.own[i.own.length-2],r[r.length-1]=i.own[i.own.length-1],i.coords=r)}}function Lt(e){let n=0;for(let t=4;t.9999)return{coords:[o[0],o[1],s[0],s[1]],corner:!1};let a=St(o,i,s,l,r,t);if(a)return{coords:a,corner:!0};let u=Math.max(4,Math.min(32,Math.round(r/Math.max(2,t/3)))),f=Te(o,i,s,l,u),m=(s[0]-o[0])*i[0]+(s[1]-o[1])*i[1],h=(o[0]-s[0])*l[0]+(o[1]-s[1])*l[1];if(m<0||h<0)for(let g=.5;g>.1&&Qe(f);g/=2)f=Te(o,i,s,l,u,g);return{coords:f,corner:!1}}function St(e,n,t,o,s,i){let d=xe(e[0],e[1],e[0]+n[0],e[1]+n[1],t[0],t[1],t[0]+o[0],t[1]+o[1]);if(!d)return null;let l=(d[0]-e[0])*n[0]+(d[1]-e[1])*n[1],r=(d[0]-t[0])*o[0]+(d[1]-t[1])*o[1];if(l<=.01||r<=.01||l+r>gt*s||Math.acos(Math.max(-1,Math.min(1,-(n[0]*o[0]+n[1]*o[1]))))*180/Math.PIthis.v.length;)this.growV();for(;this.ni+t>this.i.length;)this.growI()}growV(){let n=new Float32Array(this.v.length*2);n.set(this.v),this.v=n;let t=new Uint8Array(this.c.length*2);t.set(this.c),this.c=t}growI(){let n=new Uint32Array(this.i.length*2);n.set(this.i),this.i=n}vertex(n,t,o,s,i,d,l,r,c,a){(this.nv+1)*ee>this.v.length&&this.growV();let u=this.nv*ee;this.v[u]=n,this.v[u+1]=t,this.v[u+2]=o,this.v[u+3]=s,this.v[u+4]=i,this.v[u+5]=d,this.v[u+6]=l,this.v[u+7]=r,this.v[u+8]=c;let f=this.nv*4;return this.c[f]=a[0],this.c[f+1]=a[1],this.c[f+2]=a[2],this.c[f+3]=a[3],this.nv++}tri(n,t,o){this.ni+3>this.i.length&&this.growI(),this.i[this.ni++]=n,this.i[this.ni++]=t,this.i[this.ni++]=o}quad(n,t,o,s){this.tri(n,t,o),this.tri(t,s,o)}};function Et(e){let n=typeof OffscreenCanvas<"u"?new OffscreenCanvas(1,1).getContext("2d",{willReadFrequently:!0}):typeof document<"u"?document.createElement("canvas").getContext("2d",{willReadFrequently:!0}):null;if(!n)return null;n.fillStyle="#000000",n.fillStyle=e;let t=n.fillStyle;if(n.fillStyle="#ffffff",n.fillStyle=e,t!==n.fillStyle||typeof t!="string")return null;if(/^(#|rgba?\\()/i.test(t))return t;n.clearRect(0,0,1,1),n.fillRect(0,0,1,1);let[o,s,i,d]=n.getImageData(0,0,1,1).data;return`rgba(${o}, ${s}, ${i}, ${d/255})`}function Ve(e,n=!1){let t=new Uint8Array([136,136,136,255]),o=e.trim(),s=/^#([0-9a-f]{3,8})$/i.exec(o);if(s){let r=s[1];return r.length===3||r.length===4?(t[0]=parseInt(r[0]+r[0],16),t[1]=parseInt(r[1]+r[1],16),t[2]=parseInt(r[2]+r[2],16),t[3]=r.length===4?parseInt(r[3]+r[3],16):255):(r.length===6||r.length===8)&&(t[0]=parseInt(r.slice(0,2),16),t[1]=parseInt(r.slice(2,4),16),t[2]=parseInt(r.slice(4,6),16),t[3]=r.length===8?parseInt(r.slice(6,8),16):255),t}let i={black:"#000000",white:"#ffffff",red:"#ff0000",green:"#008000",blue:"#0000ff",yellow:"#ffff00",orange:"#ffa500",purple:"#800080",gray:"#808080",grey:"#808080",silver:"#c0c0c0",navy:"#000080",magenta:"#ff00ff",fuchsia:"#ff00ff",lime:"#00ff00",brown:"#a52a2a",teal:"#008080",maroon:"#800000"};if(i[o.toLowerCase()])return Ve(i[o.toLowerCase()]);let d=/^rgba?\\(([^)]+)\\)$/i.exec(o);if(d){let r=d[1].split(/[\\s,/]+/).filter(c=>c.length);for(let c=0;c<3&&c3){let c=r[3].endsWith("%")?parseFloat(r[3])/100:parseFloat(r[3]);t[3]=Math.round(Math.max(0,Math.min(1,c))*255)}return t}let l=n?null:Et(o);return l?Ve(l,!0):t}var Ee=class{constructor(){B(this,"chunks",new WeakMap);B(this,"out",null);B(this,"key","")}clear(){this.chunks=new WeakMap,this.out=null,this.key=""}builderFor(n,t){let o=`${n.scale}|${n.origin[0]}|${n.origin[1]}|${n.unitsPerMercator??1}`;return o!==this.key&&(this.chunks=new WeakMap,this.key=o),this.out||(this.out=new Ce(Math.max(64,t*3),Math.max(192,t*9))),this.out.reset(),this.out}emit(n,t,o,s){let i=this.chunks.get(t);if(i&&i.startDistance===t.startDistance&&i.color===t.look.color){Ot(n,i);return}let d=n.nv,l=n.ni;if(In(n,t.coords,t.anchors,o,t.startDistance,s),i===void 0){this.chunks.set(t,null);return}let r=new Uint32Array(n.ni-l);for(let c=0;cA){d.push([A,s.ni-A]),l.push(h),r.push(b),a.push(y);let k=s.ni,v=[];if(n.width&&b.dash&&b.dash[0]===0&&b.dash[1]>0)for(let x of p){let M=s.ni;Ft(s,x.coords,x.anchors,w,x.startDistance,b.dash[1]*n.width,n),Ln(v,M,s.ni-M,x.edge)}c.push([k,s.ni-k]),u.push(v)}}}return{vertices:s.v.subarray(0,s.nv*ee),colors:s.c.subarray(0,s.nv*4),indices:s.i.subarray(0,s.ni),vertexCount:s.nv,indexCount:s.ni,groups:d,groupRoutes:l,groupLooks:r,groupDots:c,groupPieces:a,groupDotPieces:u}}function Ft(e,n,t,o,s,i,d){let l=n.length/2;if(l<2)return;let r=0;for(let y=1;yb+p;)b+=p,g++,p=Math.hypot(n[g*2]-n[g*2-2],n[g*2+1]-n[g*2-1]);let x=p>0?Math.max(0,Math.min(1,(v-b)/p)):0,M=(g-1)*2,I=g*2,L=n[M]+(n[I]-n[M])*x,R=n[M+1]+(n[I+1]-n[M+1])*x,C=t[M]+(t[I]-t[M])*x,O=t[M+1]+(t[I+1]-t[M+1])*x,U=(C/f-m[0])*h,N=(O/f-m[1])*h,_=e.vertex(U,N,L-C,R-O,-1,-1,-1,-1,k,o),S=e.vertex(U,N,L-C,R-O,1,-1,1,-1,k,o),P=e.vertex(U,N,L-C,R-O,1,1,1,1,k,o),E=e.vertex(U,N,L-C,R-O,-1,1,-1,1,k,o);e.tri(_,S,P),e.tri(_,P,E)}}function In(e,n,t,o,s,i){let d=[],l=[];for(let M=0;M=2&&Math.abs(d[I-2]-n[M])<1e-6&&Math.abs(d[I-1]-n[M+1])<1e-6||(d.push(n[M],n[M+1]),l.push(t[M],t[M+1]))}let r=d.length/2;if(r<2)return;let{scale:c,origin:a}=i,u=i.unitsPerMercator??1,f=M=>(l[M*2]/c-a[0])*u,m=M=>(l[M*2+1]/c-a[1])*u,h=M=>d[M*2]-l[M*2],g=M=>d[M*2+1]-l[M*2+1],b=new Float64Array(r);for(let M=1;Ms<0?-s-b[M]:s+b[M],A=-1,w=-1,y=(M,I,L,R,C,O)=>{let U=f(M),N=m(M),_=h(M),S=g(M),P=p(M),E=e.vertex(U,N,_,S,I,L,R,C,P,o),F=e.vertex(U,N,_,S,-I,-L,-R,-C,P,o);O&&A>=0&&e.quad(A,w,E,F),A=E,w=F},k=(M,I,L,R,C,O)=>{let U=f(M),N=m(M),_=h(M),S=g(M),P=p(M),E=e.vertex(U,N,_,S,0,0,0,0,P,o),F=Math.atan2(L,I),T=Math.atan2(C,R)-F;for(;T>Math.PI;)T-=2*Math.PI;for(;T<-Math.PI;)T+=2*Math.PI;Math.abs(Math.abs(T)-Math.PI)<1e-6&&(T=O?-Math.PI:Math.PI);let G=Math.max(1,Math.ceil(Math.abs(T)*180/Math.PI/Ct)),D=e.vertex(U,N,_,S,I,L,I,L,P,o);for(let V=1;V<=G;V++){let z=F+T*V/G,j=Math.cos(z),$=Math.sin(z),X=e.vertex(U,N,_,S,j,$,j,$,P,o);e.tri(E,D,X),D=X}},v=new Float64Array(r-1),x=new Float64Array(r-1);for(let M=0;M0,L=M1e-6?1/G:1/0;if(D<=It)y(M,E*D,F*D,E,F,!0);else{let z=R*U-C*O>0;y(M,N,_,N,_,!0),z?k(M,N,_,S,P,!1):k(M,-N,-_,-S,-P,!0),y(M,S,P,S,P,!1)}}}function Rt(e){let n=performance.now(),t=Fe(e.graph),o=new Ae(t,e.order).solve();return e.stabilize!==!1&&hn(t,e.stabilize??{}),{id:e.id,cost:o,orders:t.edges.map(s=>s.order),baselines:t.edges.map(s=>s.baseline),ms:performance.now()-n}}var ne=new Map,Tt=16;function Nt(e){let n=Fe(e.graph),t=new Int32Array(n.edges.length),o=0;for(let i of n.edges)t[i.id]=o,o+=i.routes.length;let s={id:e.session,graph:n,origin:e.origin??[0,0],unitsPerMercator:e.unitsPerMercator??1,layoutCache:new be,fullCache:new be,meshCache:new Ee,lookAt:t,looks:null,laneStyle:(i,d)=>{let l=s.looks;if(!l)return null;let r=i.routes.indexOf(d);return r<0?null:l[t[i.id]+r]}};return s}function Ut(e){if(e.graph)for(ne.set(e.session,Nt(e));ne.size>Tt;)ne.delete(ne.keys().next().value);let n=ne.get(e.session);if(!n)return{...Cn(e),needGraph:!0};ne.delete(e.session),ne.set(e.session,n),e.looks!==void 0&&(n.looks=e.looks,n.layoutCache.clear(),n.fullCache.clear(),n.meshCache.clear());let t=e.extent==="full",o=performance.now(),s=vn(n.graph,e.zoom,()=>e.style,{bounds:t?null:e.bounds,smooth:e.smooth,openFolds:e.openFolds,routes:e.routes??void 0,laneStyle:n.looks?n.laneStyle:void 0,cache:t?n.fullCache:n.layoutCache}),i=performance.now(),d=t?_t:Sn(s.paths,{scale:s.scale,origin:n.origin,unitsPerMercator:n.unitsPerMercator,drawOrder:s.drawOrder,width:e.style.width,cache:n.meshCache}),l=performance.now();return{kind:"layout",id:e.id,session:e.session,zoom:s.zoom,scale:s.scale,bounds:s.bounds,mergedEdges:s.mergedEdges,drawOrder:s.drawOrder,stats:s.stats,paths:Re(s.paths),vertices:d.vertices.slice(),colors:d.colors.slice(),indices:d.indices.slice(),vertexCount:d.vertexCount,indexCount:d.indexCount,groups:d.groups,groupRoutes:d.groupRoutes,groupLooks:d.groupLooks,groupDots:d.groupDots,groupPieces:d.groupPieces,groupDotPieces:d.groupDotPieces,layoutMs:i-o,meshMs:l-i}}var _t={vertices:new Float32Array(0),colors:new Uint8Array(0),indices:new Uint32Array(0),vertexCount:0,indexCount:0,groups:[],groupRoutes:[],groupLooks:[],groupDots:[],groupPieces:[],groupDotPieces:[]};function Cn(e){return{kind:"layout",id:e.id,session:e.session,zoom:e.zoom,scale:512*Math.pow(2,e.zoom),bounds:e.bounds,mergedEdges:[],drawOrder:[],stats:{edges:0,edgesBuilt:0,edgesMerged:0,nodes:0,vertices:0,ms:0},paths:Re([]),vertices:new Float32Array(0),colors:new Uint8Array(0),indices:new Uint32Array(0),vertexCount:0,indexCount:0,groups:[],groupRoutes:[],groupLooks:[],groupDots:[],groupPieces:[],groupDotPieces:[],layoutMs:0,meshMs:0}}function Gt(e){return[...je(e.paths),e.vertices.buffer,e.colors.buffer,e.indices.buffer]}typeof self<"u"&&typeof self.postMessage=="function"&&typeof document>"u"&&(self.onmessage=e=>{let n=e.data;try{if(n.kind==="dispose")ne.delete(n.session),self.postMessage({kind:"dispose",id:n.id});else if(n.kind==="layout"){let t=Ut(n);self.postMessage(t,Gt(t))}else self.postMessage(Rt(n))}catch(t){let o=String(t?.stack??t);n.kind==="layout"?self.postMessage({...Cn(n),error:o}):n.kind==="dispose"?self.postMessage({kind:"dispose",id:n.id,error:o}):self.postMessage({id:n.id,cost:NaN,orders:[],baselines:[],ms:0,error:o})}});})();\n'], { type: "text/javascript" }))), worker = new Worker(blobUrl); + } catch { + return failed[kind] = !0, null; + } + if (!worker) + return failed[kind] = !0, null; + worker.onmessage = (ev) => { + let p = pending.get(ev.data.id); + p && (pending.delete(ev.data.id), p.resolve(ev.data)); + }; + let broken = (reason) => { + workers[kind] === worker && (console.warn(`maplibre-gl-lanes: ${kind} worker failed, falling back to the main thread:`, reason), worker.terminate(), workers[kind] = null, failed[kind] = !0, settleAll(kind, reason)); + }; + return worker.onerror = (ev) => broken(ev.message || "the worker script failed"), worker.onmessageerror = () => broken("a message from the worker could not be read"), workers[kind] = worker, worker; +} +function nextRequestId() { + return nextId++; +} +function sendToWorker(req, transfer) { + let kind = kindOf(req); + if (req.kind === "dispose" && !workers.layout) return null; + let w = getWorker(kind); + return w ? new Promise((resolve) => { + pending.set(req.id, { worker: kind, req, resolve }), w.postMessage(req, transfer ?? []); + }) : null; +} +function disposeWorkers() { + for (let kind of ["order", "layout"]) + workers[kind]?.terminate(), workers[kind] = null, failed[kind] = !1; + blobUrl && URL.revokeObjectURL(blobUrl), blobUrl = null, settleAll(null, null); +} + +// src/core/order-async.ts +async function orderLanesAsync(g, opts = {}) { + let order = { ...opts.order }; + opts.seed && (order.initial = seedFromSnapshot(g, opts.seed)); + let req = { kind: "order", id: nextRequestId(), graph: toTransfer(g), order, stabilize: opts.stabilize }, sent = opts.sync ? null : sendToWorker(req); + if (!sent) { + let cost = new LaneOrderer(g, order).solve(); + return opts.stabilize !== !1 && stabilizeLanes(g, opts.stabilize ?? {}), cost; + } + let res = await sent; + return res.disposed ? orderLanesAsync(g, { ...opts, sync: !0 }) : res.error ? (console.warn("maplibre-gl-lanes: ordering in worker failed, running on main thread:", res.error), orderLanesAsync(g, { ...opts, sync: !0 })) : (g.edges.forEach((e, i) => { + e.order = res.orders[i], e.baseline = res.baselines[i]; + }), res.cost); +} + +// src/core/folds.ts +var STEP_PER_CLEARANCE = 1 / 5, MAX_LEG_COS = -0.5, TANGENT_SAMPLES = 2, SMOOTH_PER_CLEARANCE = 0.15, SIMPLIFY_TOLERANCE_PX = 0.1, MIN_PUSH_PX = 0.01; +function scanPairs(x, y, reach, minSep, found) { + let n = x.length, size = 16; + for (; size < 2 * n; ) size *= 2; + let hash = (cx, cy) => (Math.imul(cx, 73856093) ^ Math.imul(cy, 19349663)) & size - 1, bucket = new Int32Array(n), start = new Int32Array(size + 1); + for (let i = 0; i < n; i++) + bucket[i] = hash(Math.floor(x[i] / reach), Math.floor(y[i] / reach)), start[bucket[i] + 1]++; + for (let b = 0; b < size; b++) start[b + 1] += start[b]; + let items = new Int32Array(n), fill = start.slice(0, size); + for (let i = 0; i < n; i++) items[fill[bucket[i]]++] = i; + let r2 = reach * reach, seen = new Int32Array(9); + for (let i = 0; i < n - minSep; i++) { + let cx = Math.floor(x[i] / reach), cy = Math.floor(y[i] / reach), cells = 0; + for (let gy = cy - 1; gy <= cy + 1; gy++) + for (let gx = cx - 1; gx <= cx + 1; gx++) { + let b = hash(gx, gy), again = !1; + for (let q = 0; q < cells; q++) seen[q] === b && (again = !0); + if (!again) { + seen[cells++] = b; + for (let q = start[b + 1] - 1; q >= start[b]; q--) { + let j = items[q]; + if (j - i < minSep) break; + let dx = x[j] - x[i], dy = y[j] - y[i], d2 = dx * dx + dy * dy; + d2 < r2 && found(i, j, d2); + } + } + } + } +} +var MIN_CLEARANCE_PX = 1; +function openFolds(p, clearance, guardA, guardB) { + let n0 = p.length / 2; + if (n0 < 3 || !(clearance >= MIN_CLEARANCE_PX)) return p; + let arcMin = Math.PI / 2 * clearance, total = 0; + for (let i = 2; i < p.length; i += 2) total += Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]); + if (total - guardA - guardB <= arcMin) return p; + { + let heading = 0, lo = 0, hi = 0, last = Math.atan2(p[3] - p[1], p[2] - p[0]); + for (let i = 4; i < p.length; i += 2) { + let a = Math.atan2(p[i + 1] - p[i - 1], p[i] - p[i - 2]), turn = a - last; + turn > Math.PI ? turn -= 2 * Math.PI : turn < -Math.PI && (turn += 2 * Math.PI), heading += turn, heading < lo && (lo = heading), heading > hi && (hi = heading), last = a; + } + if (hi - lo < Math.acos(MAX_LEG_COS)) return p; + } + let count = Math.max(2, Math.ceil(total / (clearance * STEP_PER_CLEARANCE))), step = total / count, n = count + 1, x = new Float64Array(n), y = new Float64Array(n); + { + let seg = 1, segStart = 0, segLen = Math.hypot(p[2] - p[0], p[3] - p[1]); + for (let i = 0; i < n; i++) { + let s = i === count ? total : i * step; + for (; seg < n0 - 1 && s > segStart + segLen; ) + segStart += segLen, seg++, segLen = Math.hypot(p[seg * 2] - p[seg * 2 - 2], p[seg * 2 + 1] - p[seg * 2 - 1]); + let t = segLen > 0 ? Math.min(1, (s - segStart) / segLen) : 0; + x[i] = p[seg * 2 - 2] + (p[seg * 2] - p[seg * 2 - 2]) * t, y[i] = p[seg * 2 - 1] + (p[seg * 2 + 1] - p[seg * 2 - 1]) * t; + } + } + let side = new Uint8Array(n), seeds = []; + if (scanPairs(x, y, clearance, Math.floor(arcMin / step) + 1, (i, j, d2) => { + let i0 = Math.max(0, i - TANGENT_SAMPLES), i1 = Math.min(n - 1, i + TANGENT_SAMPLES), j0 = Math.max(0, j - TANGENT_SAMPLES), j1 = Math.min(n - 1, j + TANGENT_SAMPLES), tix = x[i1] - x[i0], tiy = y[i1] - y[i0], tjx = x[j1] - x[j0], tjy = y[j1] - y[j0]; + if (tix * tjx + tiy * tjy > MAX_LEG_COS * Math.hypot(tix, tiy) * Math.hypot(tjx, tjy)) return; + let dx2 = x[j] - x[i], dy2 = y[j] - y[i]; + seeds.push(i, j, d2), side[i] |= tix * dy2 - tiy * dx2 > 0 ? 1 : 2, side[j] |= tjy * dx2 - tjx * dy2 > 0 ? 1 : 2; + }), !seeds.length) return p; + let order = []; + for (let s = 0; s < seeds.length; s += 3) order.push(s); + order.sort((a, b) => seeds[a + 2] - seeds[b + 2]); + let c2 = clearance * clearance, dist2 = (i, j) => (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]), taken = new Uint8Array(n), folds = []; + for (let s of order) { + let i = seeds[s], j = seeds[s + 1]; + if (taken[i] === 1 || taken[j] === 1 || taken[i] && taken[j]) continue; + let seedI = i, seedJ = j, tip = -1; + for (; ; ) { + if (j - i <= 2) { + tip = i + j >> 1; + break; + } + let both = dist2(i + 1, j - 1), a = dist2(i + 1, j), b = dist2(i, j - 1), best = Math.min(both, a, b); + if (best >= c2) break; + both === best ? (i++, j--) : a === best ? i++ : j--; + } + tip >= 0 && (i = tip - 1, j = tip + 1); + let fold = { ai: [], bi: [], tip }, blocked = i < 0 || j > n - 1; + for (; !blocked; ) { + if (taken[i] === 1 || taken[j] === 1) { + blocked = !0; + break; + } + fold.ai.push(i), fold.bi.push(j); + let both = i > 0 && j < n - 1 ? dist2(i - 1, j + 1) : 1 / 0, a = i > 0 ? dist2(i - 1, j) : 1 / 0, b = j < n - 1 ? dist2(i, j + 1) : 1 / 0, best = Math.min(both, a, b); + if (best >= c2) break; + both === best ? (i--, j++) : a === best ? i-- : j++; + } + if (blocked || !fold.ai.length) { + taken[seedI] || (taken[seedI] = 2), taken[seedJ] || (taken[seedJ] = 2); + continue; + } + let lo = fold.ai[fold.ai.length - 1], hi = fold.bi[fold.bi.length - 1]; + tip >= 0 ? taken.fill(1, lo, hi + 1) : (taken.fill(1, lo, fold.ai[0] + 1), taken.fill(1, fold.bi[0], hi + 1)), folds.push(fold); + } + let R = clearance / 2, ramp = (i) => { + let s = i * step, t = Math.min((s - guardA) / clearance, (total - s - guardB) / clearance); + return t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t); + }, dx = new Float64Array(n), dy = new Float64Array(n), capped = new Uint8Array(n), moved = !1; + for (let f of folds) { + let stack = !1; + for (let k = 0; k < f.ai.length && !stack; k++) stack = side[f.ai[k]] === 3 || side[f.bi[k]] === 3; + if (stack) continue; + let from = 0; + if (f.tip >= 0) { + let tx = x[f.tip], ty = y[f.tip], k = 0, mx = tx, my = ty, reached = 0; + for (; k < f.ai.length; k++) { + let qx = (x[f.ai[k]] + x[f.bi[k]]) / 2, qy = (y[f.ai[k]] + y[f.bi[k]]) / 2, far = Math.hypot(qx - tx, qy - ty); + if (far >= R) { + let t = (R - reached) / (far - reached); + mx += (qx - mx) * t, my += (qy - my) * t; + break; + } + mx = qx, my = qy, reached = far; + } + if (k >= f.ai.length || ramp(f.ai[k]) < 1 || ramp(f.bi[k]) < 1) continue; + let a = f.ai[k], b = f.bi[k], h = Math.hypot(x[a] - x[b], y[a] - y[b]) / 2; + if (h < 1e-6) continue; + let ux = (x[a] - x[b]) / (2 * h), uy = (y[a] - y[b]) / (2 * h), sweep = (ux * (ty - my) - uy * (tx - mx) > 0 ? 1 : -1) * Math.PI, a0 = Math.atan2(uy, ux); + for (let q = a; q <= b; q++) { + let ang = a0 + sweep * (q - a) / (b - a); + dx[q] = mx + Math.cos(ang) * R - x[q], dy[q] = my + Math.sin(ang) * R - y[q], capped[q] = 1; + } + moved = !0, from = k + 1; + } + for (let k = from; k < f.ai.length; k++) { + let a = f.ai[k], b = f.bi[k], d = Math.hypot(x[a] - x[b], y[a] - y[b]); + if (d >= clearance || d < 1e-6) continue; + let push2 = (clearance - d) / 2, ux = (x[a] - x[b]) / d, uy = (y[a] - y[b]) / d; + !capped[a] && Math.hypot(dx[a], dy[a]) < push2 && (dx[a] = ux * push2, dy[a] = uy * push2), !capped[b] && Math.hypot(dx[b], dy[b]) < push2 && (dx[b] = -ux * push2, dy[b] = -uy * push2), moved = !0; + } + } + if (!moved) return p; + let half = Math.max(1, Math.round(SMOOTH_PER_CLEARANCE * clearance / step)), sx = Float64Array.from(dx), sy = Float64Array.from(dy); + for (let d of [sx, sy]) + for (let pass = 0; pass < 2; pass++) { + let src = Float64Array.from(d), sum = 0; + for (let i = -half; i <= half; i++) sum += src[Math.min(n - 1, Math.max(0, i))]; + for (let i = 0; i < n; i++) + d[i] = sum / (2 * half + 1), sum += src[Math.min(n - 1, i + half + 1)] - src[Math.max(0, i - half)]; + } + for (let i = 0; i < n; i++) { + if (capped[i]) continue; + let given = Math.hypot(dx[i], dy[i]), averaged = Math.hypot(sx[i], sy[i]), scale = averaged > 1e-9 ? Math.max(given, averaged) / averaged : 0; + dx[i] = sx[i] * scale, dy[i] = sy[i] * scale; + } + let live = new Uint8Array(n); + for (let i = 0; i < n; i++) { + let w = ramp(i); + if (Math.abs(dx[i] * w) < MIN_PUSH_PX && Math.abs(dy[i] * w) < MIN_PUSH_PX) { + dx[i] = dy[i] = 0; + continue; + } + dx[i] *= w, dy[i] *= w, live.fill(1, Math.max(0, i - 1), Math.min(n, i + 2)); + } + let out = [], vertex = 0, cum = 0, flush = (upTo) => { + for (; vertex < n0; vertex++) { + if (vertex > 0) { + let l = Math.hypot(p[vertex * 2] - p[vertex * 2 - 2], p[vertex * 2 + 1] - p[vertex * 2 - 1]); + if (cum + l > upTo) return; + cum += l; + } + let at = Math.min(n - 1, Math.floor(cum / step)); + !live[at] && !live[Math.min(n - 1, at + 1)] && out.push(p[vertex * 2], p[vertex * 2 + 1]); + } + }; + for (let i = 0; i < n; i++) { + if (!live[i]) continue; + flush(i * step); + let run = []; + for (; i < n && live[i]; i++) run.push(x[i] + dx[i], y[i] + dy[i]); + for (let v of simplify(run, SIMPLIFY_TOLERANCE_PX)) out.push(v); + } + return flush(1 / 0), out; +} + +// src/core/stacking.ts +var MAX_ROUTES = 16, WEAVE_COST = 1e3, END_OVER_COST = 1, MOVE_COST = 1e-3, cache = /* @__PURE__ */ new WeakMap(); +function restack(g, base) { + let stack = stackingOf(g).order; + if (!stack.length) return base; + let involved = new Set(stack), next = 0; + return base.map((r) => involved.has(r) ? stack[next++] : r); +} +function stackingOf(g) { + let signature = ""; + for (let e of g.edges) e.order.length > 1 && (signature += `${e.id}:${e.order.join(",")};`); + let known = cache.get(g); + if (known && known.signature === signature) return known; + let made = { signature, order: solve(g) }; + return cache.set(g, made), made; +} +function solve(g) { + let weaves = [], endsUnder = [], endsCount = /* @__PURE__ */ new Map(); + for (let node of g.nodes) { + if (node.ports.length < 2) continue; + let dirs = node.ports.map((p) => { + let e = g.edges[p.edge]; + return p.end === "a" ? startDirection(e.coords) : endDirection(e.coords).map((v) => -v); + }), sweep = (from, to) => ccwAngle(dirs[from], dirs[to]), position = (port, route) => { + let p = node.ports[port], order2 = g.edges[p.edge].order, i = order2.indexOf(route); + return p.end === "a" ? i : order2.length - 1 - i; + }, turns = node.transitions.filter((t) => t.from >= 0 && t.to >= 0 && t.from !== t.to), groups = /* @__PURE__ */ new Map(); + for (let t of turns) { + let u = Math.min(t.from, t.to), v = Math.max(t.from, t.to), key = `${u}:${v}`, grp = groups.get(key); + grp || groups.set(key, grp = { u, v, routes: [] }), grp.routes.includes(t.route) || grp.routes.push(t.route); + } + for (let grp of groups.values()) + if (!(grp.routes.length < 2)) + for (let t of turns) { + if (grp.routes.includes(t.route)) continue; + let shared = [t.from, t.to].filter((p) => p === grp.u || p === grp.v), crossed = []; + if (shared.length) { + if (shared.length === 1) { + let leg = shared[0], mine = t.from === leg ? t.to : t.from, theirs = grp.u === leg ? grp.v : grp.u, left = sweep(leg, mine) < sweep(leg, theirs), at = position(leg, t.route); + crossed = grp.routes.filter((r) => at < position(leg, r) !== left); + } + } else { + let through = sweep(grp.u, grp.v); + sweep(grp.u, t.from) < through != sweep(grp.u, t.to) < through && (crossed = grp.routes); + } + crossed.length > 1 && weaves.push({ group: crossed, across: t.route }); + } + let passing = new Set(turns.map((t) => t.route)); + node.ports.forEach((p, pi) => { + for (let r of g.edges[p.edge].routes) { + if (turns.some((t) => t.route === r && (t.from === pi || t.to === pi))) continue; + let under = !1; + for (let o of passing) + o === r || !node.ports.some((q, qi) => qi !== pi && g.edges[q.edge].routes.includes(o)) || (endsUnder.push([r, o]), under = !0); + under && endsCount.set(r, (endsCount.get(r) ?? 0) + 1); + } + }); + } + if (!weaves.length) return []; + let ids = /* @__PURE__ */ new Set(); + for (let w of weaves) { + ids.add(w.across); + for (let r of w.group) ids.add(r); + } + if (ids.size > MAX_ROUTES) return []; + let routes = [...ids].sort((x, y) => (endsCount.get(y) ?? 0) - (endsCount.get(x) ?? 0) || (x < y ? -1 : x > y ? 1 : 0)), index = new Map(routes.map((r, i) => [r, i])), n = routes.length, groupsOf = routes.map(() => []); + for (let w of weaves) { + let mask = 0; + for (let r of w.group) mask |= 1 << index.get(r); + let list = groupsOf[index.get(w.across)]; + list.includes(mask) || list.push(mask); + } + let over = routes.map(() => 0); + for (let [r, o] of endsUnder) { + let a = index.get(r), b = index.get(o); + a !== void 0 && b !== void 0 && (over[a] |= 1 << b); + } + let full = (1 << n) - 1, cost = new Float64Array(full + 1).fill(1 / 0), last = new Int8Array(full + 1).fill(-1); + cost[0] = 0; + for (let drawn = 0; drawn < full; drawn++) { + if (cost[drawn] === 1 / 0) continue; + let place = 0; + for (let b = drawn; b; b &= b - 1) place++; + for (let x = 0; x < n; x++) { + if (drawn & 1 << x) continue; + let c = cost[drawn] + Math.abs(x - place) * MOVE_COST; + for (let mask of groupsOf[x]) { + let below = drawn & mask; + below && below !== mask && (c += WEAVE_COST); + } + for (let b = drawn & over[x]; b; b &= b - 1) c += END_OVER_COST; + let then = drawn | 1 << x; + c < cost[then] - 1e-12 && (cost[then] = c, last[then] = x); + } + } + let order = []; + for (let drawn = full; drawn; drawn ^= 1 << last[drawn]) order.push(routes[last[drawn]]); + return order.reverse(); +} + +// src/core/layout.ts +var SIMPLIFY_TOLERANCE_PX2 = 0.25, GENERALIZE_PER_OFFSET = 0.25, SMOOTH_SEGMENT_PX = 5, MIN_FRONT_MARGIN_LANES = 0.6, MAX_FRONT_FRACTION = 0.42, CLIQUE_MIN_REF_LANES = 1.5, MAX_SLIDE_SLOPE = 0.4, SOLO_EASE_LANES = 12, SOLO_EASE_STEPS = 6, MERGE_BELOW_BUNDLE_WIDTHS = 1, MAX_MERGE_PASSES = 4, CORNER_BEND_DEG = 60, CORNER_RADIUS_LANES = 1.5, CORNER_MIN_TURN_DEG = 60, CORNER_MAX_REACH = 1.5, FOLLOW_ABOVE_LANES = 1, SNAP_HOOK_LANES = 3, LayoutCache = class { + constructor() { + __publicField(this, "state", null); + } + clear() { + this.state = null; + } + /** @internal */ + stateFor(g, zoom, style, smooth, openFolds2, laneStyle) { + let s = this.state; + return s && s.graph === g && s.zoom === zoom && s.spacing === style.spacing && s.width === style.width && s.casing === style.casingWidth && s.smooth === smooth && s.openFolds === openFolds2 && s.laneStyle === laneStyle ? s : this.state = zoomState(g, zoom, style, smooth, openFolds2, laneStyle); + } +}; +function checkedSizes(sizesAt, zoom) { + requireSizesFunction(sizesAt); + let s = sizesAt(zoom), ok = (v, min) => typeof v == "number" && Number.isFinite(v) && v >= min; + if (!s || !ok(s.spacing, 0) || !ok(s.width, 0) || !ok(s.casingWidth, 0) || s.spacing === 0 || s.width === 0) { + let got = s && typeof s == "object" ? JSON.stringify({ spacing: s.spacing, width: s.width, casingWidth: s.casingWidth }, (_k, v) => v === void 0 ? "undefined" : typeof v == "number" && !Number.isFinite(v) ? String(v) : v) : String(s); + throw new TypeError(`maplibre-gl-lanes: \`sizes(${zoom})\` must return {spacing, width, casingWidth} as finite numbers in px, with spacing and width above 0; got ${got}`); + } + return s; +} +function requireSizesFunction(sizesAt) { + if (typeof sizesAt != "function") + throw new TypeError("maplibre-gl-lanes: `sizes` must be a function of the zoom that returns {spacing, width, casingWidth}"); +} +function layoutAtZoom(g, zoom, styleAt, opts = {}) { + let t0 = performance.now(), smooth = opts.smooth ?? !0, open = opts.openFolds ?? !0, laneStyle = opts.laneStyle ?? null, style = checkedSizes(styleAt, zoom), st = opts.cache ? opts.cache.stateFor(g, zoom, style, smooth, open, laneStyle) : zoomState(g, zoom, style, smooth, open, laneStyle), bounds = opts.bounds ?? null, only = opts.routes ? new Set(opts.routes) : null, inView = new Uint8Array(g.edges.length); + for (let e of g.edges) { + if (!bounds) { + inView[e.id] = 1; + continue; + } + e.bounds || (e.bounds = polylineBounds(e.coords)), inView[e.id] = boundsIntersect(e.bounds, bounds) ? 1 : 0; + } + let paths = [], laneIndex = /* @__PURE__ */ new Map(), vertices = 0, edgesBuilt = 0; + for (let e of g.edges) + if (!(!inView[e.id] || st.short[e.id])) { + edgesBuilt++; + for (let r of e.routes) { + if (only && !only.has(r)) continue; + let path = lanePathOf(st, e.id, r); + path && (laneIndex.set(laneKey(st, e.id, r), path), vertices += path.coords.length / 2, paths.push(path)); + } + } + for (let ji = 0; ji < st.junctions.length; ji++) { + if (!touchesView(st.junctions[ji], inView)) continue; + let tails = planJunction(st, ji).tails; + for (let plan of tails) { + let t = plan.tail; + if (!inView[plan.port.edge] && !t.edges.some((s) => inView[s.edge]) || (resolveTail(st, plan), plan.mode !== 1 || only && !only.has(t.route))) continue; + let path = plan.slide; + vertices += path.coords.length / 2, paths.push(path); + } + for (let plan of tails) + if (!(plan.mode === 1 || only && !only.has(plan.tail.route))) + for (let { edge } of plan.tail.edges) { + let k = laneKey(st, edge, plan.tail.route); + if (!inView[edge] || laneIndex.has(k)) continue; + let path = lanePathOf(st, edge, plan.tail.route); + path && (laneIndex.set(k, path), vertices += path.coords.length / 2, paths.push(path)); + } + for (let isl of st.junctions[ji].islands) + if (!(only && !only.has(isl.route) || !isl.steps.some((s) => inView[s.edge]))) + for (let path of islandPaths(st, isl)) { + if (path.kind === "lane") { + let k = laneKey(st, path.edge, isl.route); + if (laneIndex.has(k)) continue; + laneIndex.set(k, path); + } + vertices += path.coords.length / 2, paths.push(path); + } + } + for (let ji = 0; ji < st.junctions.length; ji++) + if (touchesView(st.junctions[ji], inView)) { + for (let grp of planJunction(st, ji).groups) + if (!(!inView[grp.edgeU] || !inView[grp.edgeV])) { + buildGroup(st, grp); + for (let m of grp.members) { + if (m.plan?.mode === 1 || only && !only.has(m.route)) continue; + let path = st.connPaths.get(m.key); + path && (vertices += path.coords.length / 2, paths.push(path)); + } + } + } + for (let p of paths) + p.look.dash && !st.phased.has(p.route) && (assignDashPhase(st, p.route), st.phased.add(p.route)); + return { + zoom, + scale: st.scale, + paths, + drawOrder: st.drawOrder, + bounds, + mergedEdges: st.mergedEdges, + stats: { edges: g.edges.length, edgesBuilt, edgesMerged: st.mergedEdges.length, nodes: g.nodes.length, vertices, ms: performance.now() - t0 } + }; +} +function zoomState(g, zoom, style, smooth, openFolds2, laneStyle) { + let { spacing, width, casingWidth: casing } = style, scale = 512 * Math.pow(2, zoom), E = g.edges.length, routeIndex = /* @__PURE__ */ new Map(); + for (let r of g.routes.keys()) routeIndex.set(r, routeIndex.size); + for (let e of g.edges) for (let r of e.routes) routeIndex.has(r) || routeIndex.set(r, routeIndex.size); + let routeCount = Math.max(1, routeIndex.size), offsets = new Float64Array(E * routeCount); + for (let e of g.edges) { + let k = e.order.length; + for (let p = 0; p < k; p++) { + let r = routeIndex.get(e.order[p]); + r !== void 0 && (offsets[e.id * routeCount + r] = ((k - 1) / 2 - p + (e.baseline ?? 0)) * spacing); + } + } + let simplePx = new Array(E), edgeLen = new Float64Array(E), dirA = new Array(E), dirB = new Array(E); + for (let e of g.edges) { + let widest = ((e.routes.length - 1) / 2 + Math.abs(e.baseline ?? 0)) * spacing; + e.simplifyRank || (e.simplifyRank = simplifyRank(e.coords)); + let p = simplifyRanked(e.coords, e.simplifyRank, Math.max(SIMPLIFY_TOLERANCE_PX2, GENERALIZE_PER_OFFSET * widest) / scale); + for (let i = 0; i < p.length; i++) p[i] *= scale; + simplePx[e.id] = p, edgeLen[e.id] = polylineLength(p), dirA[e.id] = smooth ? splineStartDirection(p, SMOOTH_SEGMENT_PX, 10, 3, CORNER_BEND_DEG) : startDirection(p), dirB[e.id] = (smooth ? splineEndDirection(p, SMOOTH_SEGMENT_PX, 10, 3, CORNER_BEND_DEG) : endDirection(p)).map((v) => -v); + } + let halfWidth = (e) => (e.routes.length - 1) / 2 * spacing + width / 2 + casing, laneOffsetTravel = (e, r, forward) => { + let off = offsets[e.id * routeCount + routeIndex.get(r)]; + return forward ? off : -off; + }, short = new Uint8Array(E), degree = (n) => g.nodes[n].ports.length, bundleWidth = new Float64Array(g.nodes.length); + for (let n of g.nodes) { + let widest = 0; + for (let p of n.ports) widest = Math.max(widest, g.edges[p.edge].routes.length); + bundleWidth[n.id] = widest * spacing; + } + let junctions = [], need = new Float64Array(2 * E), change = new Float64Array(2 * E), continues = new Uint8Array(2 * E * routeCount); + for (let pass = 0; pass < MAX_MERGE_PASSES; pass++) { + junctions = buildJunctions(g, short, edgeLen, dirA, dirB, halfWidth), { need, change, continues } = junctionFronts(g, junctions, laneOffsetTravel, spacing, routeIndex, routeCount); + let merged = !1; + for (let e of g.edges) { + if (short[e.id] || e.a === e.b || degree(e.a) < 2 || degree(e.b) < 2) continue; + let len = edgeLen[e.id]; + (len < MERGE_BELOW_BUNDLE_WIDTHS * Math.max(bundleWidth[e.a], bundleWidth[e.b]) || len < change[2 * e.id] + change[2 * e.id + 1]) && (short[e.id] = 1, merged = !0); + } + if (!merged) break; + } + let frontA = new Float64Array(E), frontB = new Float64Array(E), mergedEdges = []; + for (let e of g.edges) { + if (short[e.id]) { + mergedEdges.push(e.id); + continue; + } + let cap = edgeLen[e.id] * MAX_FRONT_FRACTION; + frontA[e.id] = Math.min(need[2 * e.id], cap), frontB[e.id] = Math.min(need[2 * e.id + 1], cap); + } + let endsUnder = /* @__PURE__ */ new Map(), underOther = (node, edge, r) => { + for (let p of node.ports) + if (p.edge !== edge) { + for (let o of g.edges[p.edge].routes) if (o !== r) return !0; + } + return !1; + }; + for (let e of g.edges) + if (!short[e.id]) + for (let r of e.routes) { + let ri = routeIndex.get(r); + for (let end = 0; end < 2; end++) { + let node = g.nodes[end === 0 ? e.a : e.b]; + !continues[(2 * e.id + end) * routeCount + ri] && underOther(node, e.id, r) && endsUnder.set(r, (endsUnder.get(r) ?? 0) + 1); + } + } + let drawOrder = restack(g, [...g.routes.keys()].sort((x, y) => (endsUnder.get(y) ?? 0) - (endsUnder.get(x) ?? 0) || (x < y ? -1 : x > y ? 1 : 0))); + return { + graph: g, + zoom, + scale, + spacing, + width, + casing, + smooth, + openFolds: openFolds2, + laneStyle, + looks: /* @__PURE__ */ new Map(), + routeIndex, + routeCount, + offsets, + simplePx, + edgeLen, + dirA, + dirB, + short, + junctions, + groups: new Array(junctions.length), + tails: new Array(junctions.length), + continues, + frontA, + frontB, + mergedEdges, + drawOrder, + preparedPx: new Array(E), + lanes: new Array(E * routeCount), + lanePaths: new Array(E * routeCount), + connPaths: /* @__PURE__ */ new Map(), + phased: /* @__PURE__ */ new Set() + }; +} +function touchesView(j, inView) { + for (let edge of j.edges) if (inView[edge]) return !0; + return !1; +} +function planJunction(st, ji) { + let known = st.groups[ji]; + if (known) return { groups: known, tails: st.tails[ji] }; + let g = st.graph, j = st.junctions[ji], byKey = /* @__PURE__ */ new Map(), groups = []; + for (let t of j.transitions) { + let pu = j.ports[t.u], pv = j.ports[t.v], lateral = offsetTravel(st, pu.edge, t.route, pu.end === "b"), lateralOut = offsetTravel(st, pv.edge, t.route, pv.end === "a"), tu = travelSign(g.edges[pu.edge].direction.get(t.route)) * (pu.end === "b" ? 1 : -1), tv = travelSign(g.edges[pv.edge].direction.get(t.route)) * (pv.end === "a" ? 1 : -1), key = `${pu.edge}:${pv.edge}:${(lateral - lateralOut).toFixed(3)}`, grp = byKey.get(key); + grp || (grp = { edgeU: pu.edge, edgeV: pv.edge, members: [] }, byKey.set(key, grp), groups.push(grp)), grp.members.push({ + route: t.route, + key: connKey(st, t.route, pu.edge, pu.end, pv.edge, pv.end), + pu, + pv, + lateral, + lateralOut, + travel: tu || tv, + via: t.via + }); + } + let crossings = /* @__PURE__ */ new Map(); + for (let grp of groups) + for (let m of grp.members) + for (let step of m.via) { + let k = `${m.route}|${step.edge}`, list = crossings.get(k); + list ? list.push(m) : crossings.set(k, [m]); + } + for (let list of crossings.values()) if (list.length > 1) for (let m of list) m.forked = !0; + let tails = []; + for (let t of j.tails) { + let p = j.ports[t.port], far = t.edges[t.side === "end" ? t.edges.length - 1 : 0], step = t.edges[t.side === "end" ? 0 : t.edges.length - 1], travel = travelSign(g.edges[p.edge].direction.get(t.route)) * (t.side === "end" ? p.end === "b" ? 1 : -1 : p.end === "a" ? 1 : -1), plan = { + tail: t, + port: p, + mode: 0, + key: t.side === "end" ? connKey(st, t.route, p.edge, p.end, far.edge, far.forward ? "b" : "a") : connKey(st, t.route, far.edge, far.forward ? "a" : "b", p.edge, p.end) + }; + tails.push(plan); + let u = t.side === "end" ? p.edge : step.edge, v = t.side === "end" ? step.edge : p.edge; + groups.push({ + edgeU: u, + edgeV: v, + members: [{ + route: t.route, + pu: p, + pv: p, + lateral: 0, + lateralOut: 0, + travel, + via: [], + plan, + key: t.side === "end" ? connKey(st, t.route, u, p.end, v, step.forward ? "a" : "b") : connKey(st, t.route, u, step.forward ? "b" : "a", v, p.end) + }] + }); + } + return st.groups[ji] = groups, st.tails[ji] = tails, { groups, tails }; +} +function laneKey(st, edge, route) { + return edge * st.routeCount + st.routeIndex.get(route); +} +function connKey(st, route, edgeU, endU, edgeV, endV) { + let ends = 2 * st.graph.edges.length; + return ((edgeU * 2 + (endU === "b" ? 1 : 0)) * ends + edgeV * 2 + (endV === "b" ? 1 : 0)) * st.routeCount + st.routeIndex.get(route); +} +function offsetTravel(st, edge, route, forward) { + let off = st.offsets[laneKey(st, edge, route)]; + return forward ? off : -off; +} +function continuesThrough(st, edge, end, route) { + return st.continues[(2 * edge + end) * st.routeCount + st.routeIndex.get(route)] !== 0; +} +function lookOf(st, e, r) { + let meta = st.graph.routes.get(r), over = st.laneStyle ? st.laneStyle(e, r) : null, color = over?.color || meta?.color || "#888", dash = over?.dash ?? meta?.dash, dashColor = over?.dashColor ?? meta?.dashColor, dashCap = over?.dashCap ?? meta?.dashCap, key = `${color}|${dash ? dash[0] + "," + dash[1] : ""}|${dashColor ?? ""}|${dashCap ?? ""}`, look = st.looks.get(key); + return look || st.looks.set(key, look = { color, dash, dashColor, dashCap }), look; +} +function preparedPx(st, edge) { + let px = st.preparedPx[edge]; + if (!px) { + let simple = st.simplePx[edge]; + px = st.smooth ? simplify(smoothCatmullRom(simple, SMOOTH_SEGMENT_PX, 10, 3, CORNER_BEND_DEG, CORNER_RADIUS_LANES * st.spacing), SIMPLIFY_TOLERANCE_PX2) : simple; + let e = st.graph.edges[edge]; + if (st.openFolds && e.routes.length > 1 && !st.short[edge]) { + let widest = ((e.routes.length - 1) / 2 + Math.abs(e.baseline ?? 0)) * st.spacing; + px = openFolds(px, 2 * widest + st.width + 2 * st.casing, st.frontA[edge] + st.spacing, st.frontB[edge] + st.spacing); + } + st.preparedPx[edge] = px; + } + return px; +} +function orientedPx(st, edge, forward) { + let px = preparedPx(st, edge); + return forward ? px : reversed(px); +} +function laneOf(st, edge, route) { + let k = laneKey(st, edge, route), hit = st.lanes[k]; + if (hit !== void 0) return hit; + let g = st.graph, e = g.edges[edge], px = preparedPx(st, edge), lane; + if (st.short[edge]) + lane = offsetPolylineAnchored(px, st.offsets[k]); + else { + let cutA = continuesThrough(st, edge, 0, route) ? st.frontA[edge] : 0, cutB = continuesThrough(st, edge, 1, route) ? st.frontB[edge] : 0, trimmed = cutA > 0 || cutB > 0 ? trimPolyline(px, cutA, cutB) : px; + lane = e.order.length === 1 && st.offsets[k] !== 0 ? easedSoloLane(trimmed, st.offsets[k], continuesThrough(st, edge, 0, route), continuesThrough(st, edge, 1, route), SOLO_EASE_LANES * st.spacing, st.spacing) : offsetPolylineAnchored(trimmed, st.offsets[k]); + let na = g.nodes[e.a], nb = g.nodes[e.b]; + cutA > 0 && (lane.anchors[0] = na.x * st.scale, lane.anchors[1] = na.y * st.scale), cutB > 0 && (lane.anchors[lane.anchors.length - 2] = nb.x * st.scale, lane.anchors[lane.anchors.length - 1] = nb.y * st.scale); + } + return st.lanes[k] = lane, lane; +} +function easedSoloLane(line, offset, holdA, holdB, ease, flat) { + if (!holdA && !holdB) return offsetPolylineAnchored(line, 0); + let flatA = holdA ? flat : 0, flatB = holdB ? flat : 0, len = polylineLength(line) - flatA - flatB; + if (len < 1) return offsetPolylineAnchored(line, offset); + let inner = trimPolyline(line, flatA, flatB, 0), easeA = holdB ? ease : Math.min(ease, len), easeB = holdA ? ease : Math.min(ease, len), held = (t) => 1 - t * t * (3 - 2 * t), at = (s) => offset * Math.max( + holdA ? held(Math.min(1, s / easeA)) : 0, + holdB ? held(Math.min(1, (len - s) / easeB)) : 0 + ), slide = (piece, s0, s1) => { + let steps = Math.max(1, Math.ceil((s1 - s0) / ease * SOLO_EASE_STEPS)), via = []; + for (let i = 1; i < steps; i++) via.push(i / steps, at(s0 + (s1 - s0) * i / steps)); + return offsetPolylineSlidingAnchored(piece, at(s0), at(s1), 4, via); + }, parts = []; + flatA > 0 && parts.push(offsetPolylineAnchored(firstPortion(line, flatA), offset)); + let headLen = holdA ? easeA : 0, tailLen = holdB ? easeB : 0; + headLen + tailLen > len - 1 ? parts.push(slide(inner, 0, len)) : (headLen > 0 && parts.push(slide(firstPortion(inner, headLen), 0, headLen)), parts.push(offsetPolylineAnchored(trimPolyline(inner, headLen, tailLen, 0), 0)), tailLen > 0 && parts.push(slide(lastPortion(inner, tailLen), len - tailLen, len))), flatB > 0 && parts.push(offsetPolylineAnchored(lastPortion(line, flatB), offset)); + let points = [], anchors = []; + return parts.forEach((part, i) => { + for (let j = i > 0 ? 2 : 0; j < part.points.length; j++) + points.push(part.points[j]), anchors.push(part.anchors[j]); + }), { points, anchors }; +} +function lanePathOf(st, edge, route) { + let k = laneKey(st, edge, route), hit = st.lanePaths[k]; + if (hit !== void 0) return hit; + let lane = laneOf(st, edge, route); + if (lane.points.length < 4) + return st.lanePaths[k] = null, null; + let e = st.graph.edges[edge], path = { + route, + look: lookOf(st, e, route), + coords: lane.points, + anchors: lane.anchors, + kind: "lane", + travel: travelSign(e.direction.get(route)), + startDistance: 0, + edge + }; + return st.lanePaths[k] = path, path; +} +function guideOf(st, parts) { + let out = []; + for (let p of parts) + for (let i = 0; i < p.length; i += 2) + out.length && Math.abs(out[out.length - 2] - p[i]) < 1e-6 && Math.abs(out[out.length - 1] - p[i + 1]) < 1e-6 || out.push(p[i], p[i + 1]); + return st.smooth ? simplify(smoothCatmullRom(out, SMOOTH_SEGMENT_PX, 10, 3, CORNER_BEND_DEG, CORNER_RADIUS_LANES * st.spacing), SIMPLIFY_TOLERANCE_PX2) : out; +} +function resolveTail(st, plan) { + if (plan.mode) return; + plan.mode = 2; + let g = st.graph, t = plan.tail, p = plan.port, portLane = laneOf(st, p.edge, t.route), parts = t.edges.map((s) => orientedPx(st, s.edge, s.forward)), far = t.edges[t.side === "end" ? t.edges.length - 1 : 0], farOffset = offsetTravel(st, far.edge, t.route, far.forward), portOffset = offsetTravel(st, p.edge, t.route, t.side === "end" ? p.end === "b" : p.end === "a"), portPx = preparedPx(st, p.edge), portPart = t.side === "end" ? lastPortion(p.end === "b" ? portPx : reversed(portPx), p.end === "b" ? st.frontB[p.edge] : st.frontA[p.edge]) : firstPortion(p.end === "a" ? portPx : reversed(portPx), p.end === "a" ? st.frontA[p.edge] : st.frontB[p.edge]), guide = t.side === "end" ? guideOf(st, [portPart, ...parts]) : guideOf(st, [...parts, portPart]), slide = t.side === "end" ? offsetPolylineSlidingAnchored(guide, portOffset, farOffset) : offsetPolylineSlidingAnchored(guide, farOffset, portOffset); + if (slide.points.length < 4) return; + let lanePts = t.side === "end" ? p.end === "b" ? portLane.points : reversed(portLane.points) : p.end === "a" ? portLane.points : reversed(portLane.points); + t.side === "end" ? (slide.points[0] = lanePts[lanePts.length - 2], slide.points[1] = lanePts[lanePts.length - 1]) : (slide.points[slide.points.length - 2] = lanePts[0], slide.points[slide.points.length - 1] = lanePts[1]); + let { points: coords, anchors } = removeLoopsAnchored(slide.points, slide.anchors, SNAP_HOOK_LANES * st.spacing); + if (coords.length < 4) return; + let travel = travelSign(g.edges[p.edge].direction.get(t.route)) * (t.side === "end" ? p.end === "b" ? 1 : -1 : p.end === "a" ? 1 : -1), between = t.side === "end" ? [p.edge, far.edge] : [far.edge, p.edge]; + plan.slide = { + route: t.route, + look: lookOf(st, g.edges[p.edge], t.route), + coords, + anchors, + kind: "connector", + travel, + startDistance: 0, + edge: between[0], + node: p.node, + between + }, st.connPaths.set(plan.key, plan.slide), plan.mode = 1; +} +function islandPaths(st, isl) { + if (isl.paths) return isl.paths; + let g = st.graph, first = isl.steps[0], last = isl.steps[isl.steps.length - 1], guide = guideOf(st, isl.steps.map((s) => orientedPx(st, s.edge, s.forward))), d0 = offsetTravel(st, first.edge, isl.route, first.forward), d1 = offsetTravel(st, last.edge, isl.route, last.forward), slide = offsetPolylineSlidingAnchored(guide, d0, d1); + if (slide.points.length >= 4) { + let e = g.edges[first.edge], travel = travelSign(e.direction.get(isl.route)) * (first.forward ? 1 : -1); + return isl.paths = [{ + route: isl.route, + look: lookOf(st, e, isl.route), + coords: slide.points, + anchors: slide.anchors, + kind: "connector", + travel, + startDistance: 0, + edge: first.edge, + node: first.forward ? e.a : e.b, + between: [first.edge, last.edge] + }]; + } + let lanes = []; + for (let s of isl.steps) { + let path = lanePathOf(st, s.edge, isl.route); + path && lanes.push(path); + } + return isl.paths = lanes; +} +function assignDashPhase(st, route) { + let g = st.graph, forwardOn = /* @__PURE__ */ new Map(); + for (let chain of g.chains.get(route) ?? []) + for (let s of chain.steps) forwardOn.set(s.edge, s.forward); + let travelEnd = (edge) => forwardOn.get(edge) === !1 ? "a" : "b", pieces = [], index = /* @__PURE__ */ new Map(), along = [], add = (p, withTravel) => { + p && !index.has(p) && (index.set(p, pieces.length), pieces.push(p), along.push(withTravel)); + }, addLane = (edge) => add(lanePathOf(st, edge, route), forwardOn.get(edge) !== !1); + for (let e of g.edges) !st.short[e.id] && e.routes.includes(route) && addLane(e.id); + for (let ji = 0; ji < st.junctions.length; ji++) { + let { groups, tails } = planJunction(st, ji); + for (let plan of tails) + if (plan.tail.route === route) + if (resolveTail(st, plan), plan.mode === 1) add(plan.slide, !0); + else for (let { edge } of plan.tail.edges) addLane(edge); + for (let grp of groups) + if (grp.members.some((m) => m.route === route)) { + buildGroup(st, grp); + for (let m of grp.members) + m.route !== route || m.plan?.mode === 1 || add(st.connPaths.get(m.key), !!m.plan || m.pu.end === travelEnd(m.pu.edge)); + } + for (let isl of st.junctions[ji].islands) + if (isl.route === route) for (let p of islandPaths(st, isl)) add(p, !0); + } + if (!pieces.length) return; + let n = pieces.length, atPoint = /* @__PURE__ */ new Map(), endsAt = new Array(2 * n); + for (let i = 0; i < n; i++) { + let c = pieces[i].coords; + for (let end = 0; end < 2; end++) { + let x = end ? c[c.length - 2] : c[0], y = end ? c[c.length - 1] : c[1], row = atPoint.get(x); + row || atPoint.set(x, row = /* @__PURE__ */ new Map()); + let list = row.get(y); + list || row.set(y, list = []), list.push(i * 2 + end), endsAt[i * 2 + end] = list; + } + } + let length = new Float64Array(n).fill(-1), at = new Float64Array(2 * n), reached = new Uint8Array(n), reach = (i, end, d) => { + let l = length[i]; + l < 0 && (length[i] = l = polylineLength(pieces[i].coords)); + let d0 = along[i] ? end ? d - l : d : end ? d + l : d; + at[i * 2] = d0, at[i * 2 + 1] = along[i] ? end ? d : d + l : end ? d : d - l, reached[i] = 1; + }, root = 0; + chains: for (let chain of g.chains.get(route) ?? []) + for (let s of chain.steps) { + let lane = st.lanePaths[laneKey(st, s.edge, route)]; + if (lane) { + root = index.get(lane); + break chains; + } + } + let queue = []; + for (let next = -1; next < n; next++) { + if (next >= 0) { + if (reached[next]) continue; + root = next; + } + reach(root, 0, 0), queue.length = 0, queue.push(root); + for (let q = 0; q < queue.length; q++) { + let i = queue[q]; + for (let end = 0; end < 2; end++) { + let d = at[i * 2 + end]; + for (let code of endsAt[i * 2 + end]) { + let o = code >> 1; + reached[o] || (reach(o, code & 1, d), queue.push(o)); + } + } + } + let least = 0; + for (let i of queue) least = Math.min(least, at[i * 2], at[i * 2 + 1]); + for (let i of queue) { + let d0 = at[i * 2] - least; + pieces[i].startDistance = along[i] ? d0 : -d0; + } + } +} +function buildGroup(st, grp) { + let plan = grp.members[0].plan; + if (plan && (resolveTail(st, plan), plan.mode === 1)) + return; + let missing = !1; + for (let m of grp.members) st.connPaths.has(m.key) || (missing = !0); + if (!missing) return; + let g = st.graph, clique = [], ends = /* @__PURE__ */ new Map(), follows = !1; + for (let m of grp.members) { + if (st.connPaths.set(m.key, null), m.plan) continue; + let laneU = laneOf(st, m.pu.edge, m.route), laneV = laneOf(st, m.pv.edge, m.route), from = m.pu.end === "b" ? laneU.points : reversed(laneU.points), to = m.pv.end === "a" ? laneV.points : reversed(laneV.points), made = m.via.length ? null : connector(from, to, st.spacing); + ends.set(m, { from, to, made }), made && made.coords.length >= 4 && leavesLine(st, m, made.coords) && (follows = !0); + } + for (let m of grp.members) { + if (m.plan) { + buildTailConnector(st, m, clique); + continue; + } + let { from, to, made } = ends.get(m); + if ((m.via.length || follows) && buildViaConnector(st, m, from, to)) continue; + let curve = made ?? connector(from, to, st.spacing); + curve.coords.length < 4 || clique.push({ member: m, lateral: m.lateral, own: curve.coords, coords: curve.coords, corner: curve.corner }); + } + let arcs = clique.filter((c) => !c.corner); + arcs.length > 1 && deriveCliqueConnectors(arcs, st.spacing); + for (let c of clique) { + let m = c.member, conn = c.coords, node = g.nodes[m.pu.node], anchors = new Array(conn.length); + for (let i = 0; i < conn.length; i += 2) + anchors[i] = node.x * st.scale, anchors[i + 1] = node.y * st.scale; + st.connPaths.set(m.key, { + route: m.route, + look: lookOf(st, g.edges[grp.edgeU], m.route), + coords: conn, + anchors, + kind: "connector", + travel: m.travel, + startDistance: 0, + edge: grp.edgeU, + node: m.pu.node, + between: [grp.edgeU, grp.edgeV] + }); + } +} +function leavesLine(st, m, coords) { + let uPx = preparedPx(st, m.pu.edge), vPx = preparedPx(st, m.pv.edge), line = lastPortion(m.pu.end === "b" ? uPx : reversed(uPx), m.pu.end === "b" ? st.frontB[m.pu.edge] : st.frontA[m.pu.edge]).concat(firstPortion(m.pv.end === "a" ? vPx : reversed(vPx), m.pv.end === "a" ? st.frontA[m.pv.edge] : st.frontB[m.pv.edge])), limit = Math.max(Math.abs(m.lateral), Math.abs(m.lateralOut)) + FOLLOW_ABOVE_LANES * st.spacing; + for (let i = 0; i < coords.length; i += 2) { + let within = !1; + for (let k = 2; k < line.length && !within; k += 2) { + let x1 = line[k - 2], y1 = line[k - 1], dx = line[k] - x1, dy = line[k + 1] - y1, len = dx * dx + dy * dy, t = len ? Math.max(0, Math.min(1, ((coords[i] - x1) * dx + (coords[i + 1] - y1) * dy) / len)) : 0; + within = Math.hypot(coords[i] - x1 - t * dx, coords[i + 1] - y1 - t * dy) <= limit; + } + if (!within) return !0; + } + return !1; +} +function buildTailConnector(st, m, clique) { + let t = m.plan.tail, p = m.pu, step = t.edges[t.side === "end" ? 0 : t.edges.length - 1], portLane = laneOf(st, p.edge, t.route), lane = laneOf(st, step.edge, t.route), tailPts = step.forward ? lane.points : reversed(lane.points), from = t.side === "end" ? p.end === "b" ? portLane.points : reversed(portLane.points) : tailPts, to = t.side === "end" ? tailPts : p.end === "a" ? portLane.points : reversed(portLane.points), made = connector(from, to, st.spacing); + made.coords.length < 4 || clique.push({ member: m, lateral: 0, own: made.coords, coords: made.coords, corner: made.corner }); +} +function buildViaConnector(st, m, from, to) { + let g = st.graph, centerline = [], length = 0, append = (pts) => { + for (let i = 0; i < pts.length; i += 2) { + let n = centerline.length; + n && Math.abs(centerline[n - 2] - pts[i]) < 1e-6 && Math.abs(centerline[n - 1] - pts[i + 1]) < 1e-6 || (n && (length += Math.hypot(pts[i] - centerline[n - 2], pts[i + 1] - centerline[n - 1])), centerline.push(pts[i], pts[i + 1])); + } + }, uPx = preparedPx(st, m.pu.edge), vPx = preparedPx(st, m.pv.edge), middles = []; + append(lastPortion(m.pu.end === "b" ? uPx : reversed(uPx), m.pu.end === "b" ? st.frontB[m.pu.edge] : st.frontA[m.pu.edge])); + for (let step of m.via) { + let before = length; + append(orientedPx(st, step.edge, step.forward)), middles.push((before + length) / 2); + } + append(firstPortion(m.pv.end === "a" ? vPx : reversed(vPx), m.pv.end === "a" ? st.frontA[m.pv.edge] : st.frontB[m.pv.edge])); + let knots = []; + if (m.forked) { + let total = length; + m.via.forEach((step, i) => knots.push(middles[i] / total, offsetTravel(st, step.edge, m.route, step.forward))); + let straight = (at) => m.lateral + (m.lateralOut - m.lateral) * at, steepest = (share2) => { + let at = 0, offset = m.lateral, most = 0; + for (let k = 0; k <= knots.length; k += 2) { + let next = k < knots.length ? knots[k] : 1, nextOffset = k < knots.length ? straight(next) + (knots[k + 1] - straight(next)) * share2 : m.lateralOut; + next > at && (most = Math.max(most, Math.abs(nextOffset - offset) / ((next - at) * total))), at = next, offset = nextOffset; + } + return most; + }, share = 1; + if (steepest(1) > MAX_SLIDE_SLOPE) { + let lo = 0, hi = 1; + for (let i = 0; i < 12; i++) { + let mid = (lo + hi) / 2; + steepest(mid) > MAX_SLIDE_SLOPE ? hi = mid : lo = mid; + } + share = lo; + } + for (let k = 0; k < knots.length; k += 2) knots[k + 1] = straight(knots[k]) + (knots[k + 1] - straight(knots[k])) * share; + } + let guide = st.smooth ? simplify(smoothCatmullRom(centerline, SMOOTH_SEGMENT_PX, 10, 3, CORNER_BEND_DEG, CORNER_RADIUS_LANES * st.spacing), SIMPLIFY_TOLERANCE_PX2) : centerline, slide = offsetPolylineSlidingAnchored(guide, m.lateral, m.lateralOut, 4, knots); + if (slide.points.length < 4) return !1; + slide.points[0] = from[from.length - 2], slide.points[1] = from[from.length - 1], slide.points[slide.points.length - 2] = to[0], slide.points[slide.points.length - 1] = to[1]; + let { points: coords, anchors } = removeLoopsAnchored(slide.points, slide.anchors, SNAP_HOOK_LANES * st.spacing); + return coords.length < 4 ? !1 : (st.connPaths.set(m.key, { + route: m.route, + look: lookOf(st, g.edges[m.pu.edge], m.route), + coords, + anchors, + kind: "connector", + travel: m.travel, + startDistance: 0, + edge: m.pu.edge, + node: m.pu.node, + between: [m.pu.edge, m.pv.edge] + }), !0); +} +function buildJunctions(g, short, edgeLen, dirA, dirB, halfWidth) { + let N = g.nodes.length, parent = new Int32Array(N); + for (let n = 0; n < N; n++) parent[n] = n; + let find = (x) => { + for (; parent[x] !== x; ) x = parent[x] = parent[parent[x]]; + return x; + }; + for (let e of g.edges) short[e.id] && (parent[find(e.a)] = find(e.b)); + let byRoot = /* @__PURE__ */ new Map(), junctionOf = new Array(N), portIndex = new Int32Array(2 * g.edges.length).fill(-1); + for (let n = 0; n < N; n++) { + let root = find(n), j = byRoot.get(root); + j || byRoot.set(root, j = { ports: [], transitions: [], tails: [], islands: [], edges: [] }), junctionOf[n] = j; + for (let p of g.nodes[n].ports) + j.edges.push(p.edge), !short[p.edge] && (portIndex[p.edge * 2 + (p.end === "a" ? 0 : 1)] = j.ports.length, j.ports.push({ edge: p.edge, end: p.end, node: n, dir: p.end === "a" ? dirA[p.edge] : dirB[p.edge], hw: halfWidth(g.edges[p.edge]) })); + } + let stack = [], onStack = new Uint8Array(g.edges.length), follow = (route, edge, entryNode) => { + let found = [], visit = (cur, curNode) => { + stack.push({ edge: cur, entryNode: curNode }), onStack[cur]++; + let e = g.edges[cur], far = e.a === curNode ? e.b : e.a, at = g.nodes[far], onward = !1, path = null; + for (let t2 of at.transitions) { + if (t2.route !== route || t2.from < 0 || t2.to < 0) continue; + let pa = at.ports[t2.from], pb = at.ports[t2.to], next = pa.edge === cur && pb.edge !== cur ? pb : pb.edge === cur && pa.edge !== cur ? pa : null; + next && (short[next.edge] && onStack[next.edge] || (onward = !0, short[next.edge] ? visit(next.edge, far) : found.push({ edge: next.edge, end: next.end, node: far, walked: path ?? (path = stack.slice()) }))); + } + onward || found.push({ edge: -1, end: "a", node: far, walked: path ?? (path = stack.slice()) }), onStack[cur]--, stack.pop(); + }; + return visit(edge, entryNode), found; + }, lifted = /* @__PURE__ */ new Map(), tailsSeen = /* @__PURE__ */ new Set(); + for (let node of g.nodes) { + let j = junctionOf[node.id], root = find(node.id); + for (let t of node.transitions) { + if (t.from < 0 || t.to < 0) continue; + let from = node.ports[t.from], to = node.ports[t.to]; + if (!short[from.edge] && !short[to.edge]) { + let u = portIndex[from.edge * 2 + (from.end === "a" ? 0 : 1)], v = portIndex[to.edge * 2 + (to.end === "a" ? 0 : 1)]; + u >= 0 && v >= 0 && j.transitions.push({ route: t.route, u, v, via: [] }); + continue; + } + if (short[from.edge] && short[to.edge]) continue; + let inward = !short[from.edge], drawn = inward ? from : to, here = portIndex[drawn.edge * 2 + (drawn.end === "a" ? 0 : 1)]; + if (!(here < 0)) + for (let out of follow(t.route, (inward ? to : from).edge, node.id)) { + let steps = out.walked.map((w) => ({ edge: w.edge, forward: g.edges[w.edge].a === w.entryNode })), via = inward ? steps : steps.reverse().map((w) => ({ edge: w.edge, forward: !w.forward })); + if (out.edge < 0) { + let key2 = `${root}|${t.route}|${here}|${via.map((w) => w.edge).join(",")}`; + if (tailsSeen.has(key2)) continue; + tailsSeen.add(key2), j.tails.push({ route: t.route, port: here, side: inward ? "end" : "start", edges: via }); + continue; + } + let there = portIndex[out.edge * 2 + (out.end === "a" ? 0 : 1)]; + if (there < 0 || junctionOf[out.node] !== j) continue; + let key = `${root}|${t.route}|${Math.min(here, there)}|${Math.max(here, there)}`, length = via.reduce((sum, w) => sum + edgeLen[w.edge], 0), crossing = inward ? { route: t.route, u: here, v: there, via } : { route: t.route, u: there, v: here, via }, known = lifted.get(key); + if (known) { + length < known.length && (j.transitions[known.index] = crossing, known.length = length); + continue; + } + lifted.set(key, { index: j.transitions.length, length }), j.transitions.push(crossing); + } + } + } + let routeAt = /* @__PURE__ */ new Map(), routeIdx = (r) => { + let i = routeAt.get(r); + return i === void 0 && routeAt.set(r, i = routeAt.size), i; + }; + for (let r of g.routes.keys()) routeIdx(r); + for (let r of g.chains.keys()) routeIdx(r); + for (let j of byRoot.values()) { + for (let t of j.transitions) routeIdx(t.route); + for (let t of j.tails) routeIdx(t.route); + } + let R = routeAt.size, covered = new Uint8Array(g.edges.length * R); + for (let j of byRoot.values()) { + for (let t of j.transitions) { + let r = routeAt.get(t.route); + for (let v of t.via) covered[v.edge * R + r] = 1; + } + for (let t of j.tails) { + let r = routeAt.get(t.route); + for (let v of t.edges) covered[v.edge * R + r] = 1; + } + } + let nodeOf = (s, start) => s.forward === start ? g.edges[s.edge].a : g.edges[s.edge].b, portJoinedTo = (j, route, node, edge) => { + let stems = /* @__PURE__ */ new Set(), n = g.nodes[node]; + for (let t of n.transitions) { + if (t.route !== route || t.from < 0 || t.to < 0) continue; + let pa = n.ports[t.from], pb = n.ports[t.to]; + pa.edge === edge && short[pb.edge] && stems.add(pb.edge), pb.edge === edge && short[pa.edge] && stems.add(pa.edge); + } + if (!stems.size) return -1; + for (let t of n.transitions) { + if (t.route !== route || t.from < 0 || t.to < 0) continue; + let pa = n.ports[t.from], pb = n.ports[t.to], drawn = stems.has(pb.edge) && !short[pa.edge] ? pa : stems.has(pa.edge) && !short[pb.edge] ? pb : null; + if (drawn && drawn.edge !== edge) return portIndex[drawn.edge * 2 + (drawn.end === "a" ? 0 : 1)]; + } + return -1; + }; + for (let [route, chains] of g.chains) { + let r = routeAt.get(route); + for (let chain of chains) { + let steps = chain.steps; + for (let i = 0; i < steps.length; i++) { + if (!short[steps[i].edge] || covered[steps[i].edge * R + r]) continue; + let k = i; + for (; k < steps.length && short[steps[k].edge] && !covered[steps[k].edge * R + r]; ) k++; + let run = steps.slice(i, k).map((s) => ({ edge: s.edge, forward: s.forward })); + for (let s of run) covered[s.edge * R + r] = 1; + let first = run[0], last = run[run.length - 1], j = junctionOf[nodeOf(first, !0)], u = portJoinedTo(j, route, nodeOf(first, !0), first.edge), v = portJoinedTo(j, route, nodeOf(last, !1), last.edge); + u >= 0 && v >= 0 && u !== v ? j.transitions.push({ route, u, v, via: run }) : u >= 0 ? j.tails.push({ route, port: u, side: "end", edges: run }) : v >= 0 ? j.tails.push({ route, port: v, side: "start", edges: run }) : j.islands.push({ route, steps: run }), i = k - 1; + } + } + } + return [...byRoot.values()]; +} +function junctionFronts(g, junctions, laneOffsetTravel, spacing, routeIndex, routeCount) { + let need = new Float64Array(2 * g.edges.length), change = new Float64Array(2 * g.edges.length), continues = new Uint8Array(2 * g.edges.length * routeCount), markContinues = (p, route) => { + continues[(2 * p.edge + (p.end === "a" ? 0 : 1)) * routeCount + routeIndex.get(route)] = 1; + }; + for (let j of junctions) { + let ports = j.ports, portNeed = new Float64Array(ports.length), active = new Uint8Array(ports.length); + for (let t of j.transitions) + active[t.u] = 1, active[t.v] = 1, markContinues(ports[t.u], t.route), markContinues(ports[t.v], t.route); + for (let t of j.tails) + active[t.port] = 1, markContinues(ports[t.port], t.route); + for (let i = 0; i < ports.length; i++) + if (active[i]) + for (let k = 0; k < ports.length; k++) { + if (k === i || !active[k]) continue; + let cos = ports[i].dir[0] * ports[k].dir[0] + ports[i].dir[1] * ports[k].dir[1], sin = Math.sqrt(Math.max(0, 1 - cos * cos)), t; + sin < 1e-3 ? t = cos > 0 ? 6 * spacing : 0 : t = (ports[k].hw + ports[i].hw * cos) / sin, t = Math.max(0, t), t > 0 && (t += MIN_FRONT_MARGIN_LANES * spacing), portNeed[i] = Math.max(portNeed[i], Math.min(t, 8 * spacing)); + } + let portChange = new Float64Array(ports.length); + for (let t of j.transitions) { + let u = ports[t.u], v = ports[t.v], oIn = laneOffsetTravel(g.edges[u.edge], t.route, u.end === "b"), oOut = laneOffsetTravel(g.edges[v.edge], t.route, v.end === "a"), shift = Math.abs(oIn - oOut); + if (shift < 1e-6) continue; + let len = Math.max(2.5 * shift, 3 * spacing); + portChange[t.u] = Math.max(portChange[t.u], len / 2), portChange[t.v] = Math.max(portChange[t.v], len / 2); + } + for (let i = 0; i < ports.length; i++) portNeed[i] = Math.max(portNeed[i], portChange[i]); + ports.forEach((p, i) => change[2 * p.edge + (p.end === "a" ? 0 : 1)] = portChange[i]); + for (let t of j.tails) { + let p = ports[t.port], first = t.edges[t.side === "end" ? 0 : t.edges.length - 1], oIn = laneOffsetTravel(g.edges[p.edge], t.route, p.end === "b"), oOut = laneOffsetTravel(g.edges[first.edge], t.route, first.forward), shift = Math.abs(oIn - oOut); + shift < 1e-6 || (portNeed[t.port] = Math.max(portNeed[t.port], Math.max(2.5 * shift, 3 * spacing) / 2)); + } + ports.forEach((p, i) => need[2 * p.edge + (p.end === "a" ? 0 : 1)] = portNeed[i]); + } + return { need, change, continues }; +} +function lastPortion(p, dist) { + let n = p.length / 2; + if (dist <= 0 || n < 2) return [p[p.length - 2], p[p.length - 1]]; + let out = [], remaining = dist; + for (let i = n - 1; i > 0; i--) { + let ax = p[i * 2 - 2], ay = p[i * 2 - 1], bx = p[i * 2], by = p[i * 2 + 1], l = Math.hypot(bx - ax, by - ay); + if (out.unshift(bx, by), l >= remaining) { + let f = 1 - remaining / l; + return out.unshift(ax + (bx - ax) * f, ay + (by - ay) * f), out; + } + remaining -= l; + } + return out.unshift(p[0], p[1]), out; +} +function firstPortion(p, dist) { + return reversed(lastPortion(reversed(p), dist)); +} +function travelSign(d) { + return d === 1 ? 1 : d === -1 ? -1 : 0; +} +function deriveCliqueConnectors(clique, spacing) { + let ref = clique[0], refLen = polylineLength(ref.own); + for (let c of clique) { + let len = polylineLength(c.own); + len > refLen && (ref = c, refLen = len); + } + if (ref.own.length <= 4 || refLen < CLIQUE_MIN_REF_LANES * spacing) return; + let turning = signedTurning(ref.own); + for (let c of clique) { + if (c === ref) continue; + let d = c.lateral - ref.lateral, expected = refLen + d * turning; + if (expected < 0.25 * refLen) continue; + let off = offsetPolyline(ref.own, d); + if (off.length < 4) continue; + let ratio = polylineLength(off) / expected; + if (ratio > 1.1 || ratio < 0.9) continue; + let gapStart = Math.hypot(off[0] - c.own[0], off[1] - c.own[1]), gapEnd = Math.hypot(off[off.length - 2] - c.own[c.own.length - 2], off[off.length - 1] - c.own[c.own.length - 1]); + gapStart > spacing / 2 || gapEnd > spacing / 2 || (off[0] = c.own[0], off[1] = c.own[1], off[off.length - 2] = c.own[c.own.length - 2], off[off.length - 1] = c.own[c.own.length - 1], c.coords = off); + } +} +function signedTurning(p) { + let total = 0; + for (let i = 4; i < p.length; i += 2) { + let ax = p[i - 2] - p[i - 4], ay = p[i - 1] - p[i - 3], bx = p[i] - p[i - 2], by = p[i + 1] - p[i - 1]; + total += Math.atan2(ax * by - ay * bx, ax * bx + ay * by); + } + return total; +} +function connector(from, to, spacing) { + let P = [from[from.length - 2], from[from.length - 1]], Q = [to[0], to[1]], tp = endDirection(from), tqIn = startDirection(to), tq = [-tqIn[0], -tqIn[1]], chord = Math.hypot(Q[0] - P[0], Q[1] - P[1]); + if (chord < 1e-6) return { coords: [], corner: !1 }; + if (Math.abs((Q[0] - P[0]) * tp[1] - (Q[1] - P[1]) * tp[0]) < 0.05 && tp[0] * tqIn[0] + tp[1] * tqIn[1] > 0.9999) return { coords: [P[0], P[1], Q[0], Q[1]], corner: !1 }; + let corner = cornerTurn(P, tp, Q, tq, chord, spacing); + if (corner) return { coords: corner, corner: !0 }; + let samples = Math.max(4, Math.min(32, Math.round(chord / Math.max(2, spacing / 3)))), coords = connectorCurve(P, tp, Q, tq, samples), ahead = (Q[0] - P[0]) * tp[0] + (Q[1] - P[1]) * tp[1], aheadBack = (P[0] - Q[0]) * tq[0] + (P[1] - Q[1]) * tq[1]; + if (ahead < 0 || aheadBack < 0) + for (let scale = 0.5; scale > 0.1 && selfIntersects(coords); scale /= 2) coords = connectorCurve(P, tp, Q, tq, samples, scale); + return { coords, corner: !1 }; +} +function cornerTurn(P, tp, Q, tq, chord, spacing) { + let I = intersectLines(P[0], P[1], P[0] + tp[0], P[1] + tp[1], Q[0], Q[1], Q[0] + tq[0], Q[1] + tq[1]); + if (!I) return null; + let t = (I[0] - P[0]) * tp[0] + (I[1] - P[1]) * tp[1], u = (I[0] - Q[0]) * tq[0] + (I[1] - Q[1]) * tq[1]; + if (t <= 0.01 || u <= 0.01 || t + u > CORNER_MAX_REACH * chord || Math.acos(Math.max(-1, Math.min(1, -(tp[0] * tq[0] + tp[1] * tq[1])))) * 180 / Math.PI < CORNER_MIN_TURN_DEG) return null; + let L = Math.min(CORNER_RADIUS_LANES * spacing, 0.45 * t, 0.45 * u), A = [I[0] - tp[0] * L, I[1] - tp[1] * L], B = [I[0] - tq[0] * L, I[1] - tq[1] * L], out = [P[0], P[1], A[0], A[1]], steps = 8; + for (let i = 1; i < steps; i++) { + let s = i / steps, m = 1 - s; + out.push(m * m * A[0] + 2 * m * s * I[0] + s * s * B[0], m * m * A[1] + 2 * m * s * I[1] + s * s * B[1]); + } + return out.push(B[0], B[1], Q[0], Q[1]), out; +} + +// src/render/tessellate.ts +function addPiece(pieces, first, count, edge) { + if (count <= 0) return; + let last = pieces[pieces.length - 1]; + last && last[2] === edge && last[0] + last[1] === first ? last[1] += count : pieces.push([first, count, edge]); +} +function splitRanges(group, pieces, bright) { + if (!pieces.some((p) => bright.has(p[2]))) return { dimmed: group[1] ? [[group[0], group[1]]] : [], bright: [] }; + let out = { dimmed: [], bright: [] }; + for (let [first, count, edge] of pieces) { + let side = bright.has(edge) ? out.bright : out.dimmed, last = side[side.length - 1]; + last && last[0] + last[1] === first ? last[1] += count : side.push([first, count]); + } + return out; +} +var MITER_LIMIT = 2, DEG_PER_TRIANGLE = 15, FLOATS_PER_VERTEX = 9, MeshBuilder = class { + constructor(vertexCapacity, indexCapacity) { + __publicField(this, "v"); + __publicField(this, "c"); + __publicField(this, "i"); + __publicField(this, "nv", 0); + __publicField(this, "ni", 0); + this.v = new Float32Array(vertexCapacity * FLOATS_PER_VERTEX), this.c = new Uint8Array(vertexCapacity * 4), this.i = new Uint32Array(indexCapacity); + } + reset() { + this.nv = 0, this.ni = 0; + } + reserve(vertices, indices) { + for (; (this.nv + vertices) * FLOATS_PER_VERTEX > this.v.length; ) this.growV(); + for (; this.ni + indices > this.i.length; ) this.growI(); + } + growV() { + let v = new Float32Array(this.v.length * 2); + v.set(this.v), this.v = v; + let c = new Uint8Array(this.c.length * 2); + c.set(this.c), this.c = c; + } + growI() { + let i = new Uint32Array(this.i.length * 2); + i.set(this.i), this.i = i; + } + vertex(ax, ay, dx, dy, ex, ey, nx, ny, s, color) { + (this.nv + 1) * FLOATS_PER_VERTEX > this.v.length && this.growV(); + let o = this.nv * FLOATS_PER_VERTEX; + this.v[o] = ax, this.v[o + 1] = ay, this.v[o + 2] = dx, this.v[o + 3] = dy, this.v[o + 4] = ex, this.v[o + 5] = ey, this.v[o + 6] = nx, this.v[o + 7] = ny, this.v[o + 8] = s; + let co = this.nv * 4; + return this.c[co] = color[0], this.c[co + 1] = color[1], this.c[co + 2] = color[2], this.c[co + 3] = color[3], this.nv++; + } + tri(a, b, c) { + this.ni + 3 > this.i.length && this.growI(), this.i[this.ni++] = a, this.i[this.ni++] = b, this.i[this.ni++] = c; + } + quad(a1, a2, b1, b2) { + this.tri(a1, a2, b1), this.tri(a2, b2, b1); + } +}; +function normalizeColor(css) { + let ctx = typeof OffscreenCanvas < "u" ? new OffscreenCanvas(1, 1).getContext("2d", { willReadFrequently: !0 }) : typeof document < "u" ? document.createElement("canvas").getContext("2d", { willReadFrequently: !0 }) : null; + if (!ctx) return null; + ctx.fillStyle = "#000000", ctx.fillStyle = css; + let a = ctx.fillStyle; + if (ctx.fillStyle = "#ffffff", ctx.fillStyle = css, a !== ctx.fillStyle || typeof a != "string") return null; + if (/^(#|rgba?\()/i.test(a)) return a; + ctx.clearRect(0, 0, 1, 1), ctx.fillRect(0, 0, 1, 1); + let [r, g, bl, al] = ctx.getImageData(0, 0, 1, 1).data; + return `rgba(${r}, ${g}, ${bl}, ${al / 255})`; +} +function parseColor(css, normalized = !1) { + let out = new Uint8Array([136, 136, 136, 255]), hex = css.trim(), m = /^#([0-9a-f]{3,8})$/i.exec(hex); + if (m) { + let h = m[1]; + return h.length === 3 || h.length === 4 ? (out[0] = parseInt(h[0] + h[0], 16), out[1] = parseInt(h[1] + h[1], 16), out[2] = parseInt(h[2] + h[2], 16), out[3] = h.length === 4 ? parseInt(h[3] + h[3], 16) : 255) : (h.length === 6 || h.length === 8) && (out[0] = parseInt(h.slice(0, 2), 16), out[1] = parseInt(h.slice(2, 4), 16), out[2] = parseInt(h.slice(4, 6), 16), out[3] = h.length === 8 ? parseInt(h.slice(6, 8), 16) : 255), out; + } + let named = { + black: "#000000", + white: "#ffffff", + red: "#ff0000", + green: "#008000", + blue: "#0000ff", + yellow: "#ffff00", + orange: "#ffa500", + purple: "#800080", + gray: "#808080", + grey: "#808080", + silver: "#c0c0c0", + navy: "#000080", + magenta: "#ff00ff", + fuchsia: "#ff00ff", + lime: "#00ff00", + brown: "#a52a2a", + teal: "#008080", + maroon: "#800000" + }; + if (named[hex.toLowerCase()]) return parseColor(named[hex.toLowerCase()]); + let rgb = /^rgba?\(([^)]+)\)$/i.exec(hex); + if (rgb) { + let parts = rgb[1].split(/[\s,/]+/).filter((s) => s.length); + for (let i = 0; i < 3 && i < parts.length; i++) { + let v = parts[i].endsWith("%") ? parseFloat(parts[i]) * 255 / 100 : parseFloat(parts[i]); + out[i] = Math.round(Math.max(0, Math.min(255, v || 0))); + } + if (parts.length > 3) { + let a = parts[3].endsWith("%") ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]); + out[3] = Math.round(Math.max(0, Math.min(1, a)) * 255); + } + return out; + } + let norm = normalized ? null : normalizeColor(hex); + return norm ? parseColor(norm, !0) : out; +} +var TessellateCache = class { + constructor() { + /** null marks a path met once: its triangles are kept from the second build on. */ + __publicField(this, "chunks", /* @__PURE__ */ new WeakMap()); + __publicField(this, "out", null); + __publicField(this, "key", ""); + } + clear() { + this.chunks = /* @__PURE__ */ new WeakMap(), this.out = null, this.key = ""; + } + /** @internal */ + builderFor(opts, estimate) { + let key = `${opts.scale}|${opts.origin[0]}|${opts.origin[1]}|${opts.unitsPerMercator ?? 1}`; + return key !== this.key && (this.chunks = /* @__PURE__ */ new WeakMap(), this.key = key), this.out || (this.out = new MeshBuilder(Math.max(64, estimate * 3), Math.max(192, estimate * 9))), this.out.reset(), this.out; + } + /** @internal */ + emit(b, p, color, opts) { + let hit = this.chunks.get(p); + if (hit && hit.startDistance === p.startDistance && hit.color === p.look.color) { + appendChunk(b, hit); + return; + } + let nv = b.nv, ni = b.ni; + if (tessellatePath(b, p.coords, p.anchors, color, p.startDistance, opts), hit === void 0) { + this.chunks.set(p, null); + return; + } + let indices = new Uint32Array(b.ni - ni); + for (let k = 0; k < indices.length; k++) indices[k] = b.i[ni + k] - nv; + this.chunks.set(p, { + vertices: b.v.slice(nv * FLOATS_PER_VERTEX, b.nv * FLOATS_PER_VERTEX), + colors: b.c.slice(nv * 4, b.nv * 4), + indices, + vertexCount: b.nv - nv, + startDistance: p.startDistance, + color: p.look.color + }); + } +}; +function appendChunk(b, chunk) { + b.reserve(chunk.vertexCount, chunk.indices.length), b.v.set(chunk.vertices, b.nv * FLOATS_PER_VERTEX), b.c.set(chunk.colors, b.nv * 4); + let base = b.nv; + for (let k = 0; k < chunk.indices.length; k++) b.i[b.ni + k] = chunk.indices[k] + base; + b.nv += chunk.vertexCount, b.ni += chunk.indices.length; +} +function tessellate(paths, opts) { + let est = 0; + for (let p of paths) est += p.coords.length; + let cache2 = opts.cache ?? null, b = cache2 ? cache2.builderFor(opts, est) : new MeshBuilder(Math.max(64, est * 3), Math.max(192, est * 9)), colorCache = /* @__PURE__ */ new Map(), groups = [], groupRoutes = [], groupLooks = [], groupDots = [], groupPieces = [], groupDotPieces = [], byRoute = /* @__PURE__ */ new Map(); + for (let p of paths) { + let byLook = byRoute.get(p.route); + byLook || byRoute.set(p.route, byLook = /* @__PURE__ */ new Map()); + let l = byLook.get(p.look); + l || byLook.set(p.look, l = []), l.push(p); + } + let order = opts.drawOrder ?? [...byRoute.keys()]; + for (let rid of order) { + let byLook = byRoute.get(rid); + if (byLook) + for (let [look, list] of byLook) { + let start = b.ni, c = colorCache.get(look.color); + c || colorCache.set(look.color, c = parseColor(look.color)); + let pieces = []; + for (let p of list) { + let from = b.ni; + cache2 ? cache2.emit(b, p, c, opts) : tessellatePath(b, p.coords, p.anchors, c, p.startDistance, opts), addPiece(pieces, from, b.ni - from, p.edge); + } + if (b.ni > start) { + groups.push([start, b.ni - start]), groupRoutes.push(rid), groupLooks.push(look), groupPieces.push(pieces); + let dots = b.ni, dotPieces = []; + if (opts.width && look.dash && look.dash[0] === 0 && look.dash[1] > 0) + for (let p of list) { + let from = b.ni; + dotsOnPath(b, p.coords, p.anchors, c, p.startDistance, look.dash[1] * opts.width, opts), addPiece(dotPieces, from, b.ni - from, p.edge); + } + groupDots.push([dots, b.ni - dots]), groupDotPieces.push(dotPieces); + } + } + } + return { + vertices: b.v.subarray(0, b.nv * FLOATS_PER_VERTEX), + colors: b.c.subarray(0, b.nv * 4), + indices: b.i.subarray(0, b.ni), + vertexCount: b.nv, + indexCount: b.ni, + groups, + groupRoutes, + groupLooks, + groupDots, + groupPieces, + groupDotPieces + }; +} +function dotsOnPath(b, px, anchorsPx, color, startDistance, period, opts) { + let n = px.length / 2; + if (n < 2) return; + let total = 0; + for (let i = 1; i < n; i++) total += Math.hypot(px[i * 2] - px[i * 2 - 2], px[i * 2 + 1] - px[i * 2 - 1]); + if (total <= 0) return; + let back = startDistance < 0, s0 = Math.abs(startDistance), lo = back ? s0 - total : s0, { scale, origin } = opts, upm = opts.unitsPerMercator ?? 1, seg = 1, before = 0, len = Math.hypot(px[2] - px[0], px[3] - px[1]), first = Math.ceil((lo - period / 2) / period), count = Math.max(0, Math.ceil((lo + total - period / 2) / period) - first); + for (let k = 0; k < count; k++) { + let center = period / 2 + (back ? first + count - 1 - k : first + k) * period, at = back ? s0 - center : center - s0; + for (; seg < n - 1 && at > before + len; ) + before += len, seg++, len = Math.hypot(px[seg * 2] - px[seg * 2 - 2], px[seg * 2 + 1] - px[seg * 2 - 1]); + let t = len > 0 ? Math.max(0, Math.min(1, (at - before) / len)) : 0, i = (seg - 1) * 2, j = seg * 2, x = px[i] + (px[j] - px[i]) * t, y = px[i + 1] + (px[j + 1] - px[i + 1]) * t, gx = anchorsPx[i] + (anchorsPx[j] - anchorsPx[i]) * t, gy = anchorsPx[i + 1] + (anchorsPx[j + 1] - anchorsPx[i + 1]) * t, ax = (gx / scale - origin[0]) * upm, ay = (gy / scale - origin[1]) * upm, v0 = b.vertex(ax, ay, x - gx, y - gy, -1, -1, -1, -1, center, color), v1 = b.vertex(ax, ay, x - gx, y - gy, 1, -1, 1, -1, center, color), v2 = b.vertex(ax, ay, x - gx, y - gy, 1, 1, 1, 1, center, color), v3 = b.vertex(ax, ay, x - gx, y - gy, -1, 1, -1, 1, center, color); + b.tri(v0, v1, v2), b.tri(v0, v2, v3); + } +} +function tessellatePath(b, px, anchorsPx, color, startDistance, opts) { + let pts = [], anc = []; + for (let i = 0; i < px.length; i += 2) { + let n2 = pts.length; + n2 >= 2 && Math.abs(pts[n2 - 2] - px[i]) < 1e-6 && Math.abs(pts[n2 - 1] - px[i + 1]) < 1e-6 || (pts.push(px[i], px[i + 1]), anc.push(anchorsPx[i], anchorsPx[i + 1])); + } + let n = pts.length / 2; + if (n < 2) return; + let { scale, origin } = opts, upm = opts.unitsPerMercator ?? 1, AX = (i) => (anc[i * 2] / scale - origin[0]) * upm, AY = (i) => (anc[i * 2 + 1] / scale - origin[1]) * upm, DX = (i) => pts[i * 2] - anc[i * 2], DY = (i) => pts[i * 2 + 1] - anc[i * 2 + 1], along = new Float64Array(n); + for (let i = 1; i < n; i++) along[i] = along[i - 1] + Math.hypot(pts[i * 2] - pts[i * 2 - 2], pts[i * 2 + 1] - pts[i * 2 - 1]); + let S = (i) => startDistance < 0 ? -startDistance - along[i] : startDistance + along[i], lastL = -1, lastR = -1, pair = (i, ex, ey, nx, ny, connect) => { + let ax = AX(i), ay = AY(i), dx = DX(i), dy = DY(i), s = S(i), l = b.vertex(ax, ay, dx, dy, ex, ey, nx, ny, s, color), r = b.vertex(ax, ay, dx, dy, -ex, -ey, -nx, -ny, s, color); + connect && lastL >= 0 && b.quad(lastL, lastR, l, r), lastL = l, lastR = r; + }, fan = (i, fromX, fromY, toX, toY, ccwOnScreen) => { + let ax = AX(i), ay = AY(i), dx = DX(i), dy = DY(i), s = S(i), c = b.vertex(ax, ay, dx, dy, 0, 0, 0, 0, s, color), a0 = Math.atan2(fromY, fromX), da = Math.atan2(toY, toX) - a0; + for (; da > Math.PI; ) da -= 2 * Math.PI; + for (; da < -Math.PI; ) da += 2 * Math.PI; + Math.abs(Math.abs(da) - Math.PI) < 1e-6 && (da = ccwOnScreen ? -Math.PI : Math.PI); + let steps = Math.max(1, Math.ceil(Math.abs(da) * 180 / Math.PI / DEG_PER_TRIANGLE)), prev = b.vertex(ax, ay, dx, dy, fromX, fromY, fromX, fromY, s, color); + for (let k = 1; k <= steps; k++) { + let a = a0 + da * k / steps, vx = Math.cos(a), vy = Math.sin(a), cur = b.vertex(ax, ay, dx, dy, vx, vy, vx, vy, s, color); + b.tri(c, prev, cur), prev = cur; + } + }, sdx = new Float64Array(n - 1), sdy = new Float64Array(n - 1); + for (let i = 0; i < n - 1; i++) { + let dx = pts[i * 2 + 2] - pts[i * 2], dy = pts[i * 2 + 3] - pts[i * 2 + 1], l = Math.hypot(dx, dy) || 1; + sdx[i] = dx / l, sdy[i] = dy / l; + } + for (let i = 0; i < n; i++) { + let hasPrev = i > 0, hasNext = i < n - 1, dPrevX = hasPrev ? sdx[i - 1] : 0, dPrevY = hasPrev ? sdy[i - 1] : 0, dNextX = hasNext ? sdx[i] : 0, dNextY = hasNext ? sdy[i] : 0, nPrevX = dPrevY, nPrevY = -dPrevX, nNextX = dNextY, nNextY = -dNextX; + if (!hasPrev) { + fan(i, nNextX, nNextY, -nNextX, -nNextY, !0), pair(i, nNextX, nNextY, nNextX, nNextY, !1); + continue; + } + if (!hasNext) { + pair(i, nPrevX, nPrevY, nPrevX, nPrevY, !0), fan(i, nPrevX, nPrevY, -nPrevX, -nPrevY, !1); + continue; + } + let jx = nPrevX + nNextX, jy = nPrevY + nNextY, jl = Math.hypot(jx, jy); + if (jl < 1e-9) { + pair(i, nPrevX, nPrevY, nPrevX, nPrevY, !0), fan(i, nPrevX, nPrevY, -nPrevX, -nPrevY, !1), pair(i, nNextX, nNextY, nNextX, nNextY, !1); + continue; + } + jx /= jl, jy /= jl; + let cosHalf = jx * nNextX + jy * nNextY, miterLen = cosHalf > 1e-6 ? 1 / cosHalf : 1 / 0; + if (miterLen <= MITER_LIMIT) + pair(i, jx * miterLen, jy * miterLen, jx, jy, !0); + else { + let outsideLeft = dPrevX * dNextY - dPrevY * dNextX > 0; + pair(i, nPrevX, nPrevY, nPrevX, nPrevY, !0), outsideLeft ? fan(i, nPrevX, nPrevY, nNextX, nNextY, !1) : fan(i, -nPrevX, -nPrevY, -nNextX, -nNextY, !0), pair(i, nNextX, nNextY, nNextX, nNextY, !1); + } + } +} + +// src/render/layer.ts +var VERTEX_SHADER_BODY = ` +in vec2 a_anchor; +in vec2 a_delta; +in vec2 a_extrude; +in vec2 a_normal; +in float a_along; +in vec4 a_color; +uniform float u_units_per_px; +uniform float u_outset; +out vec2 v_normal; +out vec4 v_color; +out float v_along; +out float v_depth; +void main() { + v_along = a_along; + float thickness = projectLineThickness(a_anchor.y); + vec2 p = a_anchor + (a_delta + a_extrude * u_outset) * (u_units_per_px * thickness); + gl_Position = projectTile(p); + v_depth = gl_Position.z / gl_Position.w; + v_normal = a_normal; + v_color = a_color; +}`, FRAGMENT_SHADER_BODY = ` +in vec2 v_normal; +in vec4 v_color; +in float v_along; +in float v_depth; +uniform float u_outset; +uniform float u_blur; +uniform vec4 u_override; +uniform float u_opacity; +uniform vec2 u_dash; +uniform float u_cap; +uniform float u_along_scale; +uniform float u_cover; +out vec4 fragColor; +void main() { +#ifdef GLOBE + if (v_depth > 1.0) discard; +#endif + float dist = length(v_normal) * u_outset; + float alpha = clamp((u_outset - dist) / u_blur, 0.0, 1.0); + if (u_dash.y > 0.0) { + if (u_dash.x > 0.0) { + float period = u_dash.x + u_dash.y; + float t = mod(v_along, period); + if (u_cap < 0.5) { + float edge = min(t, u_dash.x - t) * u_along_scale; + alpha *= clamp(edge / u_blur + 0.5, 0.0, 1.0); + } else { + float r = u_outset - 0.5 * u_blur; + float best = 1e9; + for (int k = -1; k <= 1; k++) { + float tk = t - float(k) * period; + float c = clamp(tk, 0.0, u_dash.x); + best = min(best, length(vec2((tk - c) * u_along_scale, dist)) - r); + } + alpha *= clamp(-best / u_blur + 0.5, 0.0, 1.0); + } + } else { + float period = u_dash.y; + float t = mod(v_along, period) - period * 0.5; + float r = u_outset - 0.5 * u_blur; + float d = length(vec2(t * u_along_scale, dist)); + alpha = clamp((r - d) / u_blur + 0.5, 0.0, 1.0); + } + } + if (u_cover > 0.5) { + if (u_cover < 1.5) { + if (alpha < 1.0) discard; + } else if (u_cover < 2.5) { + if (alpha <= 0.0 || alpha >= 1.0) discard; + } else if (alpha < 0.5) discard; + } + vec4 c = u_override.a < 0.0 ? v_color : u_override; + fragColor = vec4(c.rgb, 1.0) * (c.a * alpha * u_opacity); +}`, DEPTH_GAP = 1 / 65536, DEPTH_CLEAR = 4 / 8, DEPTH_MARK = 3 / 8, DEPTH_FILL = 2 / 8, DEPTH_FILL_OVER = 1 / 8, offscreens = /* @__PURE__ */ new WeakMap(), COMPOSITE_VERTEX_SHADER = `#version 300 es +void main() { + vec2 p = vec2(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0); + gl_Position = vec4(p, 0.0, 1.0); +}`, COMPOSITE_FRAGMENT_SHADER = `#version 300 es +precision highp float; +uniform sampler2D u_color; +out vec4 fragColor; +void main() { + vec4 c = texelFetch(u_color, ivec2(gl_FragCoord.xy), 0); + if (c.a == 0.0) discard; + fragColor = c; +}`, COVER_ALL = 0, COVER_FULL = 1, COVER_PARTIAL = 2, COVER_HALF = 3; +function widthAt(w, zoom) { + return typeof w == "function" ? w(zoom) : w; +} +var LaneLayer = class { + constructor(opts) { + __publicField(this, "id"); + __publicField(this, "type", "custom"); + __publicField(this, "renderingMode", "2d"); + __publicField(this, "map", null); + __publicField(this, "gl", null); + /** Per projection variant; null for one that failed to build, so it is not tried every frame. */ + __publicField(this, "programs", /* @__PURE__ */ new Map()); + __publicField(this, "vbo", null); + __publicField(this, "cbo", null); + __publicField(this, "ibo", null); + __publicField(this, "mesh", null); + /** Materialized from `packed` on demand. */ + __publicField(this, "layout", null); + /** A worker's answer, kept as typed arrays until something asks for paths. */ + __publicField(this, "packed", null); + __publicField(this, "built", null); + __publicField(this, "inFlight", null); + __publicField(this, "session", nextSession++); + __publicField(this, "sentGraph", !1); + __publicField(this, "looksDirty", !0); + __publicField(this, "layoutCache", new LayoutCache()); + __publicField(this, "meshCache", new TessellateCache()); + /** + * A tile the size of the graph, at its northwest corner. Anchors are in + * its local units, so float32 keeps sub-pixel precision at high zoom. + */ + __publicField(this, "tile", { z: 0, x: 0, y: 0 }); + __publicField(this, "origin", [0, 0]); + __publicField(this, "unitsPerMercator", EXTENT); + /** + * What the uploaded mesh and the last build were made from. After `setGraph` the old + * mesh keeps drawing until the new build lands, so it needs its own tile, and queries + * on the old layout need the old graph's edges. + */ + __publicField(this, "builtWith", null); + __publicField(this, "dirty", !0); + __publicField(this, "graph"); + __publicField(this, "sizes"); + __publicField(this, "casing"); + __publicField(this, "opacity"); + __publicField(this, "zoomEpsilon"); + __publicField(this, "smooth"); + __publicField(this, "openFolds"); + __publicField(this, "laneStyle"); + __publicField(this, "cull"); + __publicField(this, "cullMargin"); + __publicField(this, "useWorker"); + __publicField(this, "onBuild"); + __publicField(this, "builds", 0); + __publicField(this, "fullFeatures", null); + /** By cache key, so repeat calls share one request. */ + __publicField(this, "fullPending", /* @__PURE__ */ new Map()); + /** Bumped by every setter a full-extent answer depends on, so one asked before it is not kept. */ + __publicField(this, "generation", 0); + __publicField(this, "highlight", []); + __publicField(this, "highlightStyle", null); + /** Each group's bright split, kept until the mesh or the highlight changes. */ + __publicField(this, "brightSplit", null); + __publicField(this, "timings", { layoutMs: 0, meshMs: 0, renderThreadMs: 0 }); + /** The depth MapLibre gave this layer for the frame being drawn. */ + __publicField(this, "depthBase", 1 - DEPTH_GAP); + /** The layer's index in the style when `depthBase` was read, or -1. */ + __publicField(this, "depthPlace", -1); + if (this.id = opts.id, this.graph = opts.graph, !opts.graph || !Array.isArray(opts.graph.edges)) throw new TypeError("maplibre-gl-lanes: `graph` must be a line graph from `buildLineGraph`"); + requireSizesFunction(opts.sizes), this.sizes = opts.sizes, this.opacity = Math.max(0, Math.min(1, opts.opacity ?? 1)), this.zoomEpsilon = opts.zoomEpsilon ?? 0.25, this.smooth = opts.smooth ?? !0, this.openFolds = opts.openFolds ?? !0, this.laneStyle = opts.laneStyle ?? null, this.cull = opts.cull ?? !0, this.cullMargin = opts.cullMargin ?? 0.5, this.useWorker = opts.worker ?? !0, this.onBuild = opts.onBuild ?? null; + let casingCss = opts.casingColor === void 0 ? "#333333" : opts.casingColor; + this.casing = casingCss === null ? null : rgba(parseColor(casingCss)), this.computeOrigin(); + } + /** Replace the graph (e.g. after re-ordering or filtering routes). */ + setGraph(graph) { + this.graph = graph, this.generation++, this.layoutCache.clear(), this.meshCache.clear(), this.fullFeatures = null, this.fullPending.clear(), this.closeSession(), this.session = nextSession++, this.sentGraph = !1, this.looksDirty = !0, this.computeOrigin(), this.dirty = !0, this.map?.triggerRepaint(); + } + /** Change the sizes per zoom (see `LaneLayerOptions.sizes`). */ + setSizes(sizes) { + requireSizesFunction(sizes), this.sizes = sizes, this.generation++, this.fullFeatures = null, this.fullPending.clear(), this.dirty = !0, this.map?.triggerRepaint(); + } + /** Change or remove the per-edge styling (see `LaneLayerOptions.laneStyle`). */ + setLaneStyle(laneStyle) { + this.laneStyle = laneStyle, this.generation++, this.layoutCache.clear(), this.meshCache.clear(), this.fullFeatures = null, this.fullPending.clear(), this.looksDirty = !0, this.dirty = !0, this.map?.triggerRepaint(); + } + /** Change the opacity of the whole layer, 0 to 1, without a rebuild. */ + setOpacity(opacity) { + this.opacity = Math.max(0, Math.min(1, opacity)), this.map?.triggerRepaint(); + } + /** Change the casing color (see `LaneLayerOptions.casingColor`); null removes the casing. */ + setCasingColor(css) { + this.casing = css === null ? null : rgba(parseColor(css)), this.map?.triggerRepaint(); + } + /** + * Lift one route, or several: their lanes are drawn last, with a halo + * and an outline, and the rest can be dimmed. It draws inside this + * layer, so layers above it cover it. Pass null, or an empty list, to + * clear. + */ + setHighlight(route, style = {}) { + this.highlight = route === null ? [] : typeof route == "string" ? [route] : [...route], this.highlightStyle = this.highlight.length === 0 ? null : { + halo: style.halo === null ? null : rgba(parseColor(style.halo ?? "#ffb700")), + haloWidth: style.haloWidth ?? 4, + haloBlur: style.haloBlur ?? 3, + outline: style.outline === null ? null : rgba(parseColor(style.outline ?? "#000000")), + outlineWidth: style.outlineWidth ?? 1.5, + dim: style.dim ?? 1, + bright: new Set(style.bright ?? []) + }, this.map?.triggerRepaint(); + } + /** The highlighted routes, in the order they were given; empty when none. */ + getHighlight() { + return [...this.highlight]; + } + /** + * What the last build covers, without its geometry. Cheap enough to call + * every frame, unlike `getLayout`. + */ + getBuildInfo() { + let l = this.layout ?? this.packed; + return l ? { build: this.builds, zoom: l.zoom, bounds: l.bounds, stats: l.stats, timings: this.timings } : null; + } + /** + * Listen for finished builds; refresh sources derived from + * `laneFeatures` here, not on `idle`, which can arrive before a worker + * build. One listener, which this replaces; null clears it. + */ + setOnBuild(listener) { + this.onBuild = listener; + } + /** + * The lane geometry of the last build, in world pixels at `layout.zoom`. + * The first call after a worker build unpacks it, so do not call it + * every frame. + */ + getLayout() { + if (this.layout) return this.layout; + let p = this.packed; + return p ? (this.layout = { + zoom: p.zoom, + scale: p.scale, + paths: unpackPaths(p.paths), + drawOrder: p.drawOrder, + bounds: p.bounds, + mergedEdges: p.mergedEdges, + stats: p.stats + }, this.layout) : null; + } + /** + * Lane geometry as GeoJSON LineStrings, one per lane piece, with + * properties `route`, `name`, `color`, `direction`, `kind`, `edge`, + * `lanes`, `routes` and the graph's `uniformProperties` (see + * docs/api.md). Lane positions depend on the zoom and on the build, + * so refresh on `moveend` and from `setOnBuild`. + */ + laneFeatures(opts = {}) { + if (opts.extent === "full") return this.fullLaneFeatures(opts); + let layout = this.getLayout(), features = []; + if (!layout) return { type: "FeatureCollection", features }; + let z = opts.zoom ?? this.map?.getZoom() ?? layout.zoom, only = opts.routes ? new Set(opts.routes) : null, { scale } = layout, nowScale = 512 * Math.pow(2, z); + for (let p of layout.paths) { + if (only && !only.has(p.route)) continue; + let coords = []; + for (let i = 0; i < p.coords.length; i += 2) { + let x = (p.anchors[i] + (p.coords[i] - p.anchors[i]) * (scale / nowScale)) / scale, y = (p.anchors[i + 1] + (p.coords[i + 1] - p.anchors[i + 1]) * (scale / nowScale)) / scale; + coords.push([x * 360 - 180, Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180 / Math.PI]); + } + coords.length < 2 || features.push(this.laneFeature(p, coords, this.builtWith?.graph ?? this.graph)); + } + return { type: "FeatureCollection", features }; + } + /** + * `laneFeatures` with `extent: 'full'` laid out in the worker. Without a + * worker, or for the built extent, it resolves with `laneFeatures`. + */ + laneFeaturesAsync(opts = {}) { + if (opts.extent !== "full" || !this.useWorker) return Promise.resolve(this.laneFeatures(opts)); + let z = opts.zoom ?? this.map?.getZoom() ?? this.built?.zoom ?? 0, routes = opts.routes ? [...opts.routes].sort() : null, key = `${z}|${routes ? routes.join("\0") : "*"}`, cached = this.fullFeatures; + if (cached && cached.graph === this.graph && cached.key === key) return Promise.resolve(cached.fc); + let waiting = this.fullPending.get(key); + if (waiting) return waiting; + let wanted = this.requestFullFeatures(z, routes, key); + this.fullPending.set(key, wanted); + let settle2 = () => { + this.fullPending.get(key) === wanted && this.fullPending.delete(key); + }; + return wanted.then(settle2, settle2), wanted; + } + async requestFullFeatures(z, routes, key) { + let graph = this.graph, generation = this.generation; + for (let attempt = 0; attempt < 2; attempt++) { + let req = this.layoutRequest(z, null, "full", routes), sent = sendToWorker(req); + if (!sent) { + this.unsent(req); + break; + } + let res = await sent; + if (res.needGraph) { + this.sentGraph = !1; + continue; + } + if (res.error) { + console.warn("maplibre-gl-lanes: lane features in the worker failed, building on this thread:", res.error); + break; + } + if (res.session !== this.session || graph !== this.graph || generation !== this.generation) { + graph = this.graph, generation = this.generation; + continue; + } + let fc = this.featureCollection(unpackPaths(res.paths), res.scale, graph); + return this.fullFeatures = { graph, key, fc }, fc; + } + return this.fullLaneFeatures({ zoom: z, routes: routes ?? void 0, extent: "full" }); + } + laneFeature(p, coords, graph) { + let meta = graph.routes.get(p.route), edge = p.edge, e = graph.edges[edge]; + p.travel === -1 && coords.reverse(); + let properties = { + ...e?.properties, + route: p.route, + name: meta?.name ?? p.route, + color: p.look.color, + direction: p.travel === 0 ? 0 : 1, + kind: p.kind, + edge, + lanes: e?.routes.length ?? 1, + routes: e ? (e.order.length === e.routes.length ? e.order : e.routes).slice() : [p.route] + }; + return { type: "Feature", geometry: { type: "LineString", coordinates: coords }, properties }; + } + fullLaneFeatures(opts) { + let z = opts.zoom ?? this.map?.getZoom() ?? this.built?.zoom ?? 0, routes = opts.routes ? [...opts.routes].sort() : null, key = `${z}|${routes ? routes.join("\0") : "*"}`, cached = this.fullFeatures; + if (cached && cached.graph === this.graph && cached.key === key) return cached.fc; + let layout = layoutAtZoom(this.graph, z, this.sizes, { smooth: this.smooth, openFolds: this.openFolds, routes: routes ?? void 0, laneStyle: this.laneStyle ?? void 0 }), fc = this.featureCollection(layout.paths, layout.scale, this.graph); + return this.fullFeatures = { graph: this.graph, key, fc }, fc; + } + featureCollection(paths, scale, graph) { + let features = []; + for (let p of paths) { + let coords = []; + for (let i = 0; i < p.coords.length; i += 2) { + let x = p.coords[i] / scale, y = p.coords[i + 1] / scale; + coords.push([x * 360 - 180, Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180 / Math.PI]); + } + coords.length < 2 || features.push(this.laneFeature(p, coords, graph)); + } + return { type: "FeatureCollection", features }; + } + /** + * The lane nearest to a screen point, within `tolerancePx` (default 6), + * or null. Only the last build is searched. + */ + queryLane(point, tolerancePx = 6) { + if (!this.map) return null; + let lngLat = this.map.unproject([point.x, point.y]); + return this.queryLaneAt([lngLat.lng, lngLat.lat], this.map.getZoom(), tolerancePx); + } + /** `queryLane` for a geographic point, with the zoom that sets the pixel tolerance. */ + queryLaneAt(lngLat, zoom, tolerancePx = 6) { + let layout = this.getLayout(); + if (!layout) return null; + let merc = lngLatToMercator(lngLat[0], lngLat[1]), nowScale = 512 * Math.pow(2, zoom), px = [merc[0] * nowScale, merc[1] * nowScale], ratio = layout.scale / nowScale, best = null, bestD2 = tolerancePx * tolerancePx, bestX = 0, bestY = 0; + for (let p of layout.paths) { + let c = p.coords, a = p.anchors, prevX = 0, prevY = 0; + for (let i = 0; i < c.length; i += 2) { + let x = a[i] / ratio + (c[i] - a[i]), y = a[i + 1] / ratio + (c[i + 1] - a[i + 1]); + if (i > 0) { + let [d2, cx, cy] = segNearest(px[0], px[1], prevX, prevY, x, y); + d2 < bestD2 && (bestD2 = d2, best = p, bestX = cx, bestY = cy); + } + prevX = x, prevY = y; + } + } + if (!best) return null; + let edgeId = best.edge, graph = this.builtWith?.graph ?? this.graph, e = graph.edges[edgeId]; + return { + route: best.route, + name: graph.routes.get(best.route)?.name, + distancePx: Math.sqrt(bestD2), + lngLat: mercatorToLngLat(bestX / nowScale, bestY / nowScale), + kind: best.kind, + edge: edgeId, + routes: e ? (e.order.length === e.routes.length ? e.order : e.routes).slice() : [best.route], + properties: e?.properties ?? {} + }; + } + dashColor(css) { + let c = dashColorCache.get(css); + return c || dashColorCache.set(css, c = rgba(parseColor(css))), c; + } + computeOrigin() { + let minX = 1 / 0, minY = 1 / 0, maxX = -1 / 0, maxY = -1 / 0; + for (let node of this.graph.nodes) + node.x < minX && (minX = node.x), node.x > maxX && (maxX = node.x), node.y < minY && (minY = node.y), node.y > maxY && (maxY = node.y); + if (!Number.isFinite(minX)) { + this.tile = { z: 0, x: 0, y: 0 }, this.origin = [0, 0], this.unitsPerMercator = EXTENT; + return; + } + let span = Math.max(maxX - minX, maxY - minY), z = span > 0 ? Math.max(0, Math.min(22, Math.floor(-Math.log2(span)))) : 22, n = Math.pow(2, z); + this.tile = { z, x: Math.min(n - 1, Math.floor(minX * n)), y: Math.min(n - 1, Math.floor(minY * n)) }, this.origin = [this.tile.x / n, this.tile.y / n], this.unitsPerMercator = EXTENT * n; + } + onAdd(map, anyGl) { + if (!isWebGL2(anyGl)) { + console.error("maplibre-gl-lanes: this map has a WebGL1 context; the lane layer needs WebGL2 and will not draw."), this.map = map; + return; + } + let gl = anyGl; + this.map = map, this.gl = gl; + let share = offscreens.get(gl); + if (share) share.users++; + else { + let made = { users: 1, target: null, composite: void 0, onLost: () => { + made.target = null, made.composite = void 0; + } }; + offscreens.set(gl, made), gl.canvas.addEventListener?.("webglcontextlost", made.onLost); + } + map.getTerrain() && console.warn("maplibre-gl-lanes: 3D terrain is enabled; lanes are drawn at sea level and will be hidden under raised terrain."), this.vbo = gl.createBuffer(), this.cbo = gl.createBuffer(), this.ibo = gl.createBuffer(), this.dirty = !0; + } + onRemove(_map, anyGl) { + let gl = this.gl; + if (!gl) { + this.map = null; + return; + } + for (let p of this.programs.values()) + p && (gl.deleteProgram(p.program), gl.deleteVertexArray(p.vao)); + this.programs.clear(); + for (let b of [this.vbo, this.cbo, this.ibo]) b && gl.deleteBuffer(b); + this.vbo = this.cbo = this.ibo = null; + let share = offscreens.get(gl); + share && --share.users <= 0 && (share.target && releaseOffscreen(gl, share.target), share.composite && (gl.deleteProgram(share.composite.program), gl.deleteVertexArray(share.composite.vao)), gl.canvas.removeEventListener?.("webglcontextlost", share.onLost), offscreens.delete(gl)), this.layoutCache.clear(), this.meshCache.clear(), this.closeSession(), this.mesh = null, this.map = null, this.gl = null; + } + /** A failure is logged once and draws nothing, rather than throwing every frame. */ + programFor(gl, shaderData) { + let name = shaderData.variantName; + if (this.programs.has(name)) return this.programs.get(name); + let entry = null; + try { + entry = this.buildProgram(gl, shaderData); + } catch (err) { + console.error(`maplibre-gl-lanes: lanes will not draw in the ${name} projection:`, err); + } + return this.programs.set(name, entry), entry; + } + buildProgram(gl, shaderData) { + let vsSource = `#version 300 es +precision highp float; +${shaderData.vertexShaderPrelude} +${shaderData.define} +${VERTEX_SHADER_BODY}`, fsSource = `#version 300 es +precision highp float; +${shaderData.define} +${FRAGMENT_SHADER_BODY}`, vs = compile(gl, gl.VERTEX_SHADER, vsSource), fs = compile(gl, gl.FRAGMENT_SHADER, fsSource), program = gl.createProgram(); + if (gl.attachShader(program, vs), gl.attachShader(program, fs), gl.linkProgram(program), gl.deleteShader(vs), gl.deleteShader(fs), !gl.getProgramParameter(program, gl.LINK_STATUS)) { + let log = gl.getProgramInfoLog(program); + throw gl.deleteProgram(program), new Error(`program link failed: ${log}`); + } + let uniforms = {}; + for (let u of [ + "u_units_per_px", + "u_outset", + "u_blur", + "u_override", + "u_opacity", + "u_dash", + "u_cap", + "u_along_scale", + "u_cover", + "u_projection_matrix", + "u_projection_tile_mercator_coords", + "u_projection_clipping_plane", + "u_projection_transition", + "u_projection_fallback_matrix" + ]) + uniforms[u] = gl.getUniformLocation(program, u); + let vao = gl.createVertexArray(); + gl.bindVertexArray(vao), gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); + let stride = FLOATS_PER_VERTEX * 4, attribs = [["a_anchor", 0, 2], ["a_delta", 8, 2], ["a_extrude", 16, 2], ["a_normal", 24, 2], ["a_along", 32, 1]]; + for (let [name, offset, size] of attribs) { + let loc = gl.getAttribLocation(program, name); + loc < 0 || (gl.enableVertexAttribArray(loc), gl.vertexAttribPointer(loc, size, gl.FLOAT, !1, stride, offset)); + } + let aCol = gl.getAttribLocation(program, "a_color"); + return gl.bindBuffer(gl.ARRAY_BUFFER, this.cbo), gl.enableVertexAttribArray(aCol), gl.vertexAttribPointer(aCol, 4, gl.UNSIGNED_BYTE, !0, 4, 0), gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo), gl.bindVertexArray(null), { program, vao, uniforms }; + } + viewBounds() { + if (!this.cull || !this.map) return null; + let b = this.map.getBounds(), sw = lngLatToMercator(b.getWest(), b.getSouth()), ne = lngLatToMercator(b.getEast(), b.getNorth()), minX = Math.min(sw[0], ne[0]), maxX = Math.max(sw[0], ne[0]), minY = Math.min(sw[1], ne[1]), maxY = Math.max(sw[1], ne[1]), mx = (maxX - minX) * this.cullMargin, my = (maxY - minY) * this.cullMargin; + return { minX: minX - mx, minY: minY - my, maxX: maxX + mx, maxY: maxY + my }; + } + viewInside(built) { + if (!built) return !0; + let b = this.map.getBounds(), sw = lngLatToMercator(b.getWest(), b.getSouth()), ne = lngLatToMercator(b.getEast(), b.getNorth()); + return Math.min(sw[0], ne[0]) >= built.minX && Math.max(sw[0], ne[0]) <= built.maxX && Math.min(sw[1], ne[1]) >= built.minY && Math.max(sw[1], ne[1]) <= built.maxY; + } + serves(build, zoom) { + let dz = zoom - build.zoom; + return dz >= -this.zoomEpsilon && dz <= 2 * this.zoomEpsilon && this.viewInside(build.bounds); + } + /** + * Only one build is ever in flight: a pan that outruns the worker asks + * again once its answer lands, rather than queueing a request per frame. + */ + rebuild(zoom) { + if (this.inFlight) return; + let bounds = this.viewBounds(); + if (this.useWorker && this.sendLayout(zoom, bounds)) { + this.dirty = !1; + return; + } + let t0 = performance.now(), layout = layoutAtZoom(this.graph, zoom, this.sizes, { bounds, smooth: this.smooth, openFolds: this.openFolds, laneStyle: this.laneStyle ?? void 0, cache: this.layoutCache }), t1 = performance.now(), mesh = tessellate(layout.paths, { scale: layout.scale, origin: this.origin, unitsPerMercator: this.unitsPerMercator, drawOrder: layout.drawOrder, width: checkedSizes(this.sizes, zoom).width, cache: this.meshCache }), t2 = performance.now(); + this.upload(mesh), this.layout = layout, this.packed = null, this.built = { zoom, bounds }, this.dirty = !1, this.timings = { layoutMs: t1 - t0, meshMs: t2 - t1, renderThreadMs: performance.now() - t0 }, this.finished(); + } + layoutRequest(zoom, bounds, extent, routes) { + let req = { + kind: "layout", + id: nextRequestId(), + session: this.session, + zoom, + style: checkedSizes(this.sizes, zoom), + smooth: this.smooth, + openFolds: this.openFolds, + bounds, + extent + }; + return routes && (req.routes = routes), this.sentGraph || (req.graph = toTransfer(this.graph), req.origin = this.origin, req.unitsPerMercator = this.unitsPerMercator, this.looksDirty = !0, this.sentGraph = !0), this.looksDirty && (req.looks = this.lookTable(), this.looksDirty = !1), req; + } + unsent(req) { + req.graph && (this.sentGraph = !1), req.looks !== void 0 && (this.looksDirty = !0); + } + sendLayout(zoom, bounds) { + let req = this.layoutRequest(zoom, bounds, "built"), sent = sendToWorker(req); + return sent ? (this.inFlight = { id: req.id }, sent.then((res) => this.onLayout(res)), !0) : (this.unsent(req), this.useWorker = !1, !1); + } + onLayout(res) { + if (!this.inFlight || res.id !== this.inFlight.id) return; + if (this.inFlight = null, res.session !== this.session) { + this.dirty = !0, this.map?.triggerRepaint(); + return; + } + if (res.needGraph) { + this.sentGraph = !1, this.dirty = !0, this.map?.triggerRepaint(); + return; + } + if (res.error) { + console.warn("maplibre-gl-lanes: layout in the worker failed, building on the main thread:", res.error), this.useWorker = !1, this.dirty = !0, this.map?.triggerRepaint(); + return; + } + if (this.dirty) { + this.map?.triggerRepaint(); + return; + } + if (!this.gl) return; + let t0 = performance.now(); + this.upload(res), this.packed = res, this.layout = null, this.built = { zoom: res.zoom, bounds: res.bounds }, this.timings = { layoutMs: res.layoutMs, meshMs: res.meshMs, renderThreadMs: performance.now() - t0 }, this.finished(), this.map?.triggerRepaint(); + } + /** The listener runs in a microtask, never inside the render call. */ + finished() { + this.builds++; + let listener = this.onBuild; + if (!listener) return; + let info = this.getBuildInfo(); + queueMicrotask(() => { + this.map && listener(info); + }); + } + /** In the worker's reading order: edges, then each edge's routes. */ + lookTable() { + let laneStyle = this.laneStyle; + if (!laneStyle) return null; + let out = []; + for (let e of this.graph.edges) for (let r of e.routes) out.push(laneStyle(e, r) ?? null); + return out; + } + closeSession() { + this.fullPending.clear(), this.inFlight = null, this.sentGraph && (this.sentGraph = !1, sendToWorker({ kind: "dispose", id: nextRequestId(), session: this.session })); + } + upload(mesh) { + let gl = this.gl; + gl.bindVertexArray(null), gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo), gl.bufferData(gl.ARRAY_BUFFER, mesh.vertices, gl.DYNAMIC_DRAW), gl.bindBuffer(gl.ARRAY_BUFFER, this.cbo), gl.bufferData(gl.ARRAY_BUFFER, mesh.colors, gl.DYNAMIC_DRAW), gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo), gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, mesh.indices, gl.DYNAMIC_DRAW), this.mesh = mesh, this.builtWith = { graph: this.graph, tile: this.tile, unitsPerMercator: this.unitsPerMercator }; + } + render(anyGl, args) { + let gl = this.gl; + if (!this.map || !this.vbo || !gl) return; + let zoom = this.map.getZoom(); + (this.dirty || !this.built || !this.serves(this.built, zoom)) && this.rebuild(zoom); + let mesh = this.mesh, frame = this.builtWith; + if (!mesh || !mesh.indexCount || !frame) return; + let entry = this.programFor(gl, args.shaderData); + if (!entry) return; + let { program, vao, uniforms } = entry, tileID = { canonical: frame.tile, wrap: 0 }, pd = args.getProjectionData ? args.getProjectionData({ tileID, applyGlobeMatrix: !0 }) : this.map.transform.getProjectionData({ overscaledTileID: tileID, applyGlobeMatrix: !0 }), dpr = this.map.getPixelRatio(), style = checkedSizes(this.sizes, zoom), built = this.built ? this.built.zoom : zoom, dashStyle = built === zoom ? style : checkedSizes(this.sizes, built), alongScale = Math.pow(2, zoom - built), unitsPerPx = frame.unitsPerMercator / (512 * Math.pow(2, zoom)); + gl.useProgram(program), gl.bindVertexArray(vao), gl.uniformMatrix4fv(uniforms.u_projection_matrix, !1, pd.mainMatrix), gl.uniformMatrix4fv(uniforms.u_projection_fallback_matrix, !1, pd.fallbackMatrix), gl.uniform4f(uniforms.u_projection_tile_mercator_coords, ...pd.tileMercatorCoords), gl.uniform4f(uniforms.u_projection_clipping_plane, ...pd.clippingPlane), gl.uniform1f(uniforms.u_projection_transition, pd.projectionTransition), gl.uniform1f(uniforms.u_units_per_px, unitsPerPx), gl.uniform1f(uniforms.u_along_scale, alongScale), gl.uniform1f(uniforms.u_blur, 1 / dpr), gl.uniform1f(uniforms.u_opacity, this.opacity), gl.uniform1f(uniforms.u_cover, COVER_ALL); + let place = this.map.getLayersOrder ? this.map.getLayersOrder().indexOf(this.id) : -1; + if (place < 0 || place !== this.depthPlace) { + let range = gl.getParameter(gl.DEPTH_RANGE); + this.depthBase = range ? range[0] : 1 - DEPTH_GAP, this.depthPlace = place; + } + this.depthAt(gl, 0), gl.enable(gl.DEPTH_TEST), gl.depthFunc(gl.LEQUAL), gl.depthMask(!1), gl.disable(gl.STENCIL_TEST), gl.disable(gl.CULL_FACE), gl.enable(gl.BLEND), gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + let aa = 0.5 / dpr; + gl.uniform2f(uniforms.u_dash, 0, 0), gl.uniform1f(uniforms.u_cap, 0); + let runs = []; + for (let gi = 0; gi < mesh.groupRoutes.length; ) { + let route = mesh.groupRoutes[gi], run = []; + for (; gi < mesh.groupRoutes.length && mesh.groupRoutes[gi] === route; ) + mesh.groups[gi][1] && run.push(gi), gi++; + run.length && runs.push(run); + } + let hl = this.highlightStyle, liftedRuns = [], lifted = /* @__PURE__ */ new Set(); + if (hl) + for (let route of this.highlight) { + let run = runs.find((r) => mesh.groupRoutes[r[0]] === route); + if (run) { + liftedRuns.push(run); + for (let gi of run) lifted.add(gi); + } + } + let dimmed = !!hl && hl.dim < 1, translucent = this.opacity < 1 || dimmed, once = !!this.casing && (this.casing[3] < 1 || translucent), marks = once || translucent, offscreen = marks ? acquireOffscreen(gl) : null; + offscreen || (once = marks = !1); + let mapFramebuffer = null; + offscreen && (mapFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING), gl.bindFramebuffer(gl.FRAMEBUFFER, offscreen.target.framebuffer), gl.clearColor(0, 0, 0, 0), gl.clearDepth(1), gl.depthMask(!0), gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT), gl.depthMask(!1)); + let split = dimmed && hl.bright.size > 0 ? this.splitFor(mesh, hl) : null, restOpacity = dimmed ? this.opacity * hl.dim : this.opacity, opacities = split ? { dimmed: restOpacity, bright: this.opacity, split } : null; + dimmed && gl.uniform1f(uniforms.u_opacity, restOpacity); + for (let run of runs) + lifted.has(run[0]) || this.drawRoute(gl, uniforms, run, style, dashStyle, aa, once, marks, restOpacity, opacities); + if (hl && liftedRuns.length) { + gl.uniform1f(uniforms.u_opacity, this.opacity); + let outlineEdge = style.width / 2 + style.casingWidth + widthAt(hl.outlineWidth, zoom), lift = () => { + for (let gi of lifted) gl.drawElements(gl.TRIANGLES, mesh.groups[gi][1], gl.UNSIGNED_INT, mesh.groups[gi][0] * 4); + if (once) { + gl.depthFunc(gl.ALWAYS), gl.depthMask(!0), this.depthAt(gl, DEPTH_CLEAR), gl.colorMask(!1, !1, !1, !1); + for (let gi of lifted) gl.drawElements(gl.TRIANGLES, mesh.groups[gi][1], gl.UNSIGNED_INT, mesh.groups[gi][0] * 4); + gl.colorMask(!0, !0, !0, !0), gl.depthMask(!1), gl.depthFunc(gl.LEQUAL), this.depthAt(gl, 0); + } + }; + hl.halo && (gl.uniform1f(uniforms.u_blur, Math.max(1 / dpr, widthAt(hl.haloBlur, zoom))), gl.uniform1f(uniforms.u_outset, outlineEdge + widthAt(hl.haloWidth, zoom) + aa), gl.uniform4fv(uniforms.u_override, hl.halo), lift(), gl.uniform1f(uniforms.u_blur, 1 / dpr)), hl.outline && (gl.uniform1f(uniforms.u_outset, outlineEdge + aa), gl.uniform4fv(uniforms.u_override, hl.outline), lift()); + for (let run of liftedRuns) this.drawRoute(gl, uniforms, run, style, dashStyle, aa, once, marks, this.opacity); + } + offscreen && (gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.DEPTH_ATTACHMENT]), gl.bindFramebuffer(gl.FRAMEBUFFER, mapFramebuffer), gl.useProgram(offscreen.composite.program), gl.bindVertexArray(offscreen.composite.vao), gl.activeTexture(gl.TEXTURE0), gl.bindTexture(gl.TEXTURE_2D, offscreen.target.color), gl.uniform1i(offscreen.composite.color, 0), gl.depthFunc(gl.LEQUAL), gl.depthMask(!1), this.depthAt(gl, 0), gl.drawArrays(gl.TRIANGLES, 0, 3)), gl.depthFunc(gl.LEQUAL), gl.depthMask(!1), gl.depthRange(this.depthBase, this.depthBase), gl.bindVertexArray(null); + } + /** Draw at an offset behind this layer's depth, in units of the gap (see `DEPTH_GAP`). */ + depthAt(gl, offset) { + let d = this.depthBase + offset * DEPTH_GAP; + gl.depthRange(d, d); + } + /** The bright split of every group, worked out once per mesh and highlight. */ + splitFor(mesh, hl) { + let cached = this.brightSplit; + if (cached && cached.mesh === mesh && cached.style === hl) return cached.groups; + let groups = []; + for (let gi = 0; gi < mesh.groups.length; gi++) { + let ribbon = splitRanges(mesh.groups[gi], mesh.groupPieces[gi], hl.bright), dots = splitRanges(mesh.groupDots[gi], mesh.groupDotPieces[gi], hl.bright); + groups.push(ribbon.bright.length || dots.bright.length ? { dimmed: ribbon.dimmed, bright: ribbon.bright, dotsDimmed: dots.dimmed, dotsBright: dots.bright } : null); + } + return this.brightSplit = { mesh, style: hl, groups }, groups; + } + /** + * Every casing before any fill, or a group's casing would seam over its + * neighbor's fill. `dashStyle` is in the mesh's build pixels. + * + * With `opacities`, the pieces on bright edges draw at full opacity in + * the same passes as the rest of the route, so they keep its place in + * the drawing order and its once-only casing. Their casings go after + * the dimmed ones: where a bright cap reaches into a dimmed neighbor, + * the neighbor's own casing already holds the mark, so a full-strength + * casing never shows through the neighbor's translucent fill. Their + * fills go last, so they cover the dimmed caps that reach into them. + * + * A translucent fill paints each pixel once (`DEPTH_FILL`), over all of + * one side's groups, and the mark is cleared only after them, so a + * connector meeting a lane of another look counts as one too. The groups + * go last first, since the first full fragment keeps its pixel, so the + * one on top stays the one on top. + */ + drawRoute(gl, uniforms, run, style, dashStyle, aa, once, marks, opacity, opacities = null) { + let mesh = this.mesh, meta = (this.builtWith?.graph ?? this.graph).routes.get(mesh.groupRoutes[run[0]]), periodOf = (look) => look.dash ? [look.dash[0] * dashStyle.width, look.dash[1] * dashStyle.width] : [0, 0], split = opacities ? opacities.split : null, ribbon = (gi, pass) => { + let sp = split && split[gi]; + return sp ? pass === "bright" ? sp.bright : sp.dimmed : pass === "bright" ? [] : [mesh.groups[gi]]; + }, dotRanges = (gi, pass) => { + let sp = split && split[gi]; + return sp ? pass === "bright" ? sp.dotsBright : sp.dotsDimmed : pass === "bright" ? [] : [mesh.groupDots[gi]]; + }, drawAll = (ranges) => { + for (let [first, count] of ranges) count && gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_INT, first * 4); + }, draw = (gi, pass) => drawAll(ribbon(gi, pass)), drawDashes = (gi, look, pass) => { + let dots = mesh.groupDots[gi]; + if (look.dash[0] === 0 && dots && dots[1]) { + drawAll(dotRanges(gi, pass)); + return; + } + let period = periodOf(look); + gl.uniform2f(uniforms.u_dash, period[0], period[1]), draw(gi, pass), gl.uniform2f(uniforms.u_dash, 0, 0); + }, lit = !!split && run.some((gi) => split[gi]), passes = lit ? ["dimmed", "bright"] : ["all"], setOpacity = (pass) => { + lit && gl.uniform1f(uniforms.u_opacity, pass === "bright" ? opacities.bright : opacities.dimmed); + }; + if (this.casing && meta?.casing !== !1) { + gl.uniform1f(uniforms.u_outset, style.width / 2 + style.casingWidth + aa), gl.uniform4fv(uniforms.u_override, this.casing); + for (let pass of passes) for (let gi of run) { + if (pass === "bright" && !split[gi]) continue; + setOpacity(pass); + let look = mesh.groupLooks[gi], drawCasing = !!look.dash && look.dash[0] === 0 && !look.dashColor ? () => drawDashes(gi, look, pass) : () => draw(gi, pass); + once ? (gl.depthFunc(gl.LESS), this.depthAt(gl, DEPTH_MARK), gl.depthMask(!0), gl.uniform1f(uniforms.u_cover, COVER_FULL), drawCasing(), gl.depthMask(!1), gl.uniform1f(uniforms.u_cover, COVER_PARTIAL), drawCasing(), gl.uniform1f(uniforms.u_cover, COVER_ALL)) : drawCasing(); + } + } + once && (gl.depthFunc(gl.LEQUAL), this.depthAt(gl, 0)), gl.uniform1f(uniforms.u_outset, style.width / 2 + aa); + let fillPasses = lit ? ["dimmed", "bright"] : ["all"]; + for (let pass of fillPasses) { + let groups = pass === "bright" ? run.filter((gi) => split[gi]) : run, paintOnce = marks && (pass === "all" ? opacity : pass === "bright" ? opacities.bright : opacities.dimmed) < 1, fill = (paint, depth) => { + if (!paintOnce) { + paint(); + return; + } + gl.depthFunc(gl.LESS), this.depthAt(gl, depth), gl.depthMask(!0), gl.uniform1f(uniforms.u_cover, COVER_FULL), paint(), gl.depthMask(!1), gl.uniform1f(uniforms.u_cover, COVER_PARTIAL), paint(), gl.uniform1f(uniforms.u_cover, COVER_ALL); + }; + setOpacity(pass); + for (let gi of paintOnce ? [...groups].reverse() : groups) { + let look = mesh.groupLooks[gi], dash = look.dash; + gl.uniform1f(uniforms.u_cap, look.dashCap === "round" ? 1 : 0), dash && look.dashColor && paintOnce ? (gl.depthFunc(gl.LESS), this.depthAt(gl, DEPTH_FILL), gl.depthMask(!0), gl.uniform1f(uniforms.u_cover, COVER_FULL), gl.uniform4f(uniforms.u_override, 0, 0, 0, -1), drawDashes(gi, look, pass), gl.uniform4fv(uniforms.u_override, this.dashColor(look.dashColor)), fill(() => draw(gi, pass), DEPTH_FILL), this.depthAt(gl, DEPTH_FILL_OVER), gl.uniform1f(uniforms.u_cover, COVER_PARTIAL), gl.uniform4f(uniforms.u_override, 0, 0, 0, -1), drawDashes(gi, look, pass), gl.uniform1f(uniforms.u_cover, COVER_ALL)) : dash && look.dashColor ? (gl.uniform4fv(uniforms.u_override, this.dashColor(look.dashColor)), draw(gi, pass), gl.uniform4f(uniforms.u_override, 0, 0, 0, -1), drawDashes(gi, look, pass)) : dash ? (gl.uniform4f(uniforms.u_override, 0, 0, 0, -1), fill(() => drawDashes(gi, look, pass), DEPTH_FILL)) : (gl.uniform4f(uniforms.u_override, 0, 0, 0, -1), fill(() => draw(gi, pass), DEPTH_FILL)); + } + if (once || paintOnce) { + gl.depthFunc(gl.ALWAYS), gl.colorMask(!1, !1, !1, !1), gl.depthMask(!0), this.depthAt(gl, DEPTH_CLEAR), gl.uniform1f(uniforms.u_cover, COVER_HALF); + for (let gi of groups) { + let look = mesh.groupLooks[gi]; + look.dash && !look.dashColor ? drawDashes(gi, look, pass) : draw(gi, pass); + } + gl.uniform1f(uniforms.u_cover, COVER_ALL), gl.depthMask(!1), gl.colorMask(!0, !0, !0, !0), gl.depthFunc(gl.LEQUAL), this.depthAt(gl, 0); + } + } + lit && gl.uniform1f(uniforms.u_opacity, opacities.dimmed); + } +}, EXTENT = 8192, nextSession = 1, dashColorCache = /* @__PURE__ */ new Map(); +function segNearest(px, py, ax, ay, bx, by) { + let dx = bx - ax, dy = by - ay, l2 = dx * dx + dy * dy, t = l2 > 0 ? Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / l2)) : 0, x = ax + dx * t, y = ay + dy * t; + return [(px - x) * (px - x) + (py - y) * (py - y), x, y]; +} +function isWebGL2(gl) { + return typeof gl.createVertexArray == "function"; +} +function acquireOffscreen(gl) { + let share = offscreens.get(gl); + if (!share) return null; + let width = gl.drawingBufferWidth, height = gl.drawingBufferHeight, target = share.target, stale = target && (target.width !== width || target.height !== height); + return target && stale && (releaseOffscreen(gl, target), target = share.target = null), target || (target = share.target = createOffscreen(gl, width, height)), share.composite === void 0 && (share.composite = buildComposite(gl)), target && share.composite ? { target, composite: share.composite } : null; +} +function createOffscreen(gl, width, height) { + let color = gl.createTexture(), depth = gl.createRenderbuffer(), framebuffer = gl.createFramebuffer(); + if (!color || !depth || !framebuffer) return null; + gl.bindTexture(gl.TEXTURE_2D, color), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null), gl.bindRenderbuffer(gl.RENDERBUFFER, depth), gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, width, height); + let bound = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer), gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color, 0), gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depth); + let complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE; + gl.bindFramebuffer(gl.FRAMEBUFFER, bound); + let target = { framebuffer, color, depth, width, height }; + return complete ? target : (releaseOffscreen(gl, target), null); +} +function releaseOffscreen(gl, target) { + gl.deleteFramebuffer(target.framebuffer), gl.deleteTexture(target.color), gl.deleteRenderbuffer(target.depth); +} +function buildComposite(gl) { + try { + let program = gl.createProgram(); + if (gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, COMPOSITE_VERTEX_SHADER)), gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, COMPOSITE_FRAGMENT_SHADER)), gl.linkProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) + return gl.deleteProgram(program), null; + let vao = gl.createVertexArray(); + return { program, vao, color: gl.getUniformLocation(program, "u_color") }; + } catch { + return null; + } +} +function rgba(c) { + return new Float32Array([c[0] / 255, c[1] / 255, c[2] / 255, c[3] / 255]); +} +function compile(gl, type, src) { + let s = gl.createShader(type); + if (gl.shaderSource(s, src), gl.compileShader(s), !gl.getShaderParameter(s, gl.COMPILE_STATUS)) { + let log = gl.getShaderInfoLog(s); + throw gl.deleteShader(s), new Error(`shader compile failed: ${log}`); + } + return s; +} +export { + LaneLayer, + LayoutCache, + applyLaneOrders, + buildLineGraph, + disposeWorkers, + filterGraph, + layoutAtZoom, + lngLatToMercator, + mercatorToLngLat, + orderLanes, + orderLanesAsync, + seedFromSnapshot, + snapshotLaneOrders, + stabilizeLanes +};