@tileflow/core 0.1.0-alpha.35 → 0.1.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -933,9 +933,9 @@ var init_sun = __esm({
933
933
  azimuth: 210,
934
934
  altitude: 60,
935
935
  color: "#ffffff",
936
- intensity: 0.5,
936
+ intensity: 0.2,
937
937
  shadowIntensity: 1,
938
- ambient: Object.freeze({ color: "#ffffff", intensity: 0.5 })
938
+ ambient: Object.freeze({ color: "#ffffff", intensity: 0.8 })
939
939
  });
940
940
  degrees = Math.PI / 180;
941
941
  }
@@ -4466,6 +4466,159 @@ function withAlpha(color, alpha) {
4466
4466
  return `rgba(${value >> 16}, ${value >> 8 & 255}, ${value & 255}, ${alpha})`;
4467
4467
  }
4468
4468
 
4469
+ // src/boundary-areas/schema.ts
4470
+ var tileflowBoundaryClasses = [
4471
+ "country",
4472
+ "dependency",
4473
+ "region",
4474
+ "province_or_county",
4475
+ "municipality",
4476
+ "district",
4477
+ "neighbourhood"
4478
+ ];
4479
+ var tileflowBoundaryClassTiers = ["primary", "secondary"];
4480
+ var tileflowBoundaryAreasV1Schema = Object.freeze({
4481
+ classes: tileflowBoundaryClasses,
4482
+ /** First zoom at which each class appears, by tier. */
4483
+ classMinzoom: Object.freeze({
4484
+ country: Object.freeze({ primary: 0, secondary: 0 }),
4485
+ dependency: Object.freeze({ primary: 0, secondary: 0 }),
4486
+ region: Object.freeze({ primary: 3, secondary: 3 }),
4487
+ province_or_county: Object.freeze({ primary: 6, secondary: 7 }),
4488
+ municipality: Object.freeze({ primary: 8, secondary: 8 }),
4489
+ district: Object.freeze({ primary: 10, secondary: 10 }),
4490
+ neighbourhood: Object.freeze({ primary: 11, secondary: 11 })
4491
+ }),
4492
+ fields: Object.freeze({
4493
+ adminDepth: "admin_depth",
4494
+ adminLevel: "admin_level",
4495
+ class: "class",
4496
+ classTier: "class_tier",
4497
+ iso31661: "iso_3166_1",
4498
+ iso31662: "iso_3166_2",
4499
+ name: "name",
4500
+ nuts: "nuts",
4501
+ ref: "ref",
4502
+ wikidata: "wikidata"
4503
+ }),
4504
+ layers: Object.freeze({ areas: "boundary_area", labels: "boundary_label" }),
4505
+ /** Properties present on `boundary_area` polygons. */
4506
+ areaProperties: Object.freeze([
4507
+ "class",
4508
+ "class_tier",
4509
+ "admin_level",
4510
+ "admin_depth",
4511
+ "ref",
4512
+ "iso_3166_1"
4513
+ ]),
4514
+ maxzoom: 12,
4515
+ minzoom: 0,
4516
+ product: "boundaries-v1",
4517
+ schemaVersion: 1,
4518
+ tiers: tileflowBoundaryClassTiers
4519
+ });
4520
+ var tileflowBoundaryFamilies = Object.freeze({
4521
+ boundaries: Object.freeze({
4522
+ attribution: "\xA9 OpenStreetMap contributors",
4523
+ license: "ODbL-1.0",
4524
+ path: "/tiles/boundaries/tiles.json",
4525
+ sourceId: "tileflow-boundaries"
4526
+ })
4527
+ });
4528
+ function isTileflowBoundaryFamily(value) {
4529
+ return typeof value === "string" && Object.hasOwn(tileflowBoundaryFamilies, value);
4530
+ }
4531
+
4532
+ // src/boundary-areas/feature-state.ts
4533
+ function createBoundaryFeatureState(map, options = {}) {
4534
+ const family = options.family ?? "boundaries";
4535
+ if (!isTileflowBoundaryFamily(family)) {
4536
+ throw new Error(`Unknown Tileflow Boundaries family: ${String(family)}.`);
4537
+ }
4538
+ const source = tileflowBoundaryFamilies[family].sourceId;
4539
+ const stateKey = options.state?.trim() || "value";
4540
+ const sourceLayers = [
4541
+ tileflowBoundaryAreasV1Schema.layers.areas,
4542
+ ...options.labels ? [tileflowBoundaryAreasV1Schema.layers.labels] : []
4543
+ ];
4544
+ let applied = /* @__PURE__ */ new Map();
4545
+ let disposed = false;
4546
+ const write = (id, value) => {
4547
+ for (const sourceLayer of sourceLayers) {
4548
+ map.setFeatureState({ id, source, sourceLayer }, { [stateKey]: value });
4549
+ }
4550
+ };
4551
+ const erase = (id) => {
4552
+ for (const sourceLayer of sourceLayers) {
4553
+ map.removeFeatureState({ id, source, sourceLayer }, stateKey);
4554
+ }
4555
+ };
4556
+ const sourceReady = () => map.getSource(source) !== void 0;
4557
+ let pendingReload = false;
4558
+ const onStyleData = () => {
4559
+ if (!sourceReady()) {
4560
+ pendingReload = true;
4561
+ return;
4562
+ }
4563
+ if (!pendingReload) return;
4564
+ pendingReload = false;
4565
+ for (const [id, value] of applied) write(id, value);
4566
+ };
4567
+ map.on("styledata", onStyleData);
4568
+ const assertOpen = () => {
4569
+ if (disposed) throw new Error("This Boundaries feature-state controller was disposed.");
4570
+ };
4571
+ return Object.freeze({
4572
+ clear() {
4573
+ assertOpen();
4574
+ if (sourceReady()) for (const id of applied.keys()) erase(id);
4575
+ applied = /* @__PURE__ */ new Map();
4576
+ },
4577
+ dispose() {
4578
+ if (disposed) return;
4579
+ if (sourceReady()) for (const id of applied.keys()) erase(id);
4580
+ applied = /* @__PURE__ */ new Map();
4581
+ map.off("styledata", onStyleData);
4582
+ disposed = true;
4583
+ },
4584
+ set(values) {
4585
+ assertOpen();
4586
+ const featureKey = promotedKey(map, source) ?? "id";
4587
+ if (values.joinBy !== featureKey) {
4588
+ throw new Error(
4589
+ `Boundaries feature-state is keyed by ${featureKey}; join the rows to ${featureKey} first with joinBoundaryRows.`
4590
+ );
4591
+ }
4592
+ const next = /* @__PURE__ */ new Map();
4593
+ for (const row of values.rows) {
4594
+ if (row.value === null) continue;
4595
+ if (featureKey === "id") {
4596
+ if (typeof row.key !== "number" || !Number.isSafeInteger(row.key) || row.key < 0) {
4597
+ throw new Error("Boundaries feature-state rows keyed by id need integer keys.");
4598
+ }
4599
+ } else if (typeof row.key !== "string" || !row.key) {
4600
+ throw new Error("Boundaries feature-state rows keyed by ref need string keys.");
4601
+ }
4602
+ next.set(row.key, row.value);
4603
+ }
4604
+ if (sourceReady()) {
4605
+ for (const id of applied.keys()) if (!next.has(id)) erase(id);
4606
+ for (const [id, value] of next) if (applied.get(id) !== value) write(id, value);
4607
+ } else {
4608
+ pendingReload = true;
4609
+ }
4610
+ applied = next;
4611
+ }
4612
+ });
4613
+ }
4614
+ function promotedKey(map, source) {
4615
+ const definition = map.getStyle?.()?.sources?.[source];
4616
+ const promoteId = definition?.promoteId;
4617
+ if (promoteId === void 0) return void 0;
4618
+ const value = typeof promoteId === "string" ? promoteId : promoteId[tileflowBoundaryAreasV1Schema.layers.areas];
4619
+ return value === "ref" ? "ref" : void 0;
4620
+ }
4621
+
4469
4622
  // src/line-progress.ts
4470
4623
  var epsilon = 5e-4;
4471
4624
  var transparent = "rgba(0, 0, 0, 0)";
@@ -4808,13 +4961,14 @@ var tileflowDefaultBuildings3DDesign = Object.freeze({
4808
4961
  layers: Object.freeze({ building: "building", places: "poi" }),
4809
4962
  color: "#d8d0c0",
4810
4963
  edges: 0,
4811
- lighting: "style",
4964
+ edgeRadius: 0.8,
4965
+ lighting: "sun",
4812
4966
  colorVariation: 0,
4813
4967
  verticalGradient: true,
4814
4968
  nightLight: 0,
4815
4969
  windows: false,
4816
- shadows: false,
4817
- occlusion: false
4970
+ shadows: true,
4971
+ occlusion: true
4818
4972
  });
4819
4973
  function readTileflowBuildings3D(metadata) {
4820
4974
  if (!isRecord6(metadata)) return void 0;
@@ -4825,6 +4979,7 @@ function readTileflowBuildings3D(metadata) {
4825
4979
  return void 0;
4826
4980
  if (typeof color !== "string" && !Array.isArray(color)) return void 0;
4827
4981
  const within = (number, high, fallback) => typeof number === "number" && number >= 0 && number <= high ? number : fallback;
4982
+ const defaults = tileflowDefaultBuildings3DDesign;
4828
4983
  return {
4829
4984
  source,
4830
4985
  layers: {
@@ -4833,14 +4988,15 @@ function readTileflowBuildings3D(metadata) {
4833
4988
  },
4834
4989
  color,
4835
4990
  edges: within(value.edges, 1, 0),
4836
- lighting: value.lighting === "sun" ? "sun" : "style",
4991
+ edgeRadius: within(value.edgeRadius, 1, defaults.edgeRadius),
4992
+ lighting: value.lighting === "style" ? "style" : defaults.lighting,
4837
4993
  colorVariation: within(value.colorVariation, 1, 0),
4838
4994
  verticalGradient: value.verticalGradient !== false,
4839
4995
  ...typeof value.edgeColor === "string" ? { edgeColor: value.edgeColor } : {},
4840
4996
  nightLight: within(value.nightLight, 2, 0),
4841
4997
  windows: value.windows === true,
4842
- shadows: value.shadows === true,
4843
- occlusion: value.occlusion === true,
4998
+ shadows: typeof value.shadows === "boolean" ? value.shadows : defaults.shadows,
4999
+ occlusion: typeof value.occlusion === "boolean" ? value.occlusion : defaults.occlusion,
4844
5000
  ...modelsOf(value.models),
4845
5001
  ...modelColorsOf(value.modelColors),
4846
5002
  ...landmarksOf(value.landmarks)
@@ -5829,10 +5985,12 @@ function meshTileflowBuildings(layer, options) {
5829
5985
  const scale = tileflowBuildingExtent / layer.extent;
5830
5986
  const metresPerUnit = options.tileMetres / tileflowBuildingExtent;
5831
5987
  const fallback = options.defaultHeight ?? 5;
5988
+ const edgeRadius = Number.isFinite(options.edgeRadius) ? Math.max(0, Math.min(1, options.edgeRadius)) / metresPerUnit : 0;
5832
5989
  const vertices = new VertexWriter();
5833
5990
  const roofs = new IndexWriter();
5834
5991
  const walls = new IndexWriter();
5835
5992
  const features = [];
5993
+ const parts = [];
5836
5994
  let tallest = 0;
5837
5995
  const uses = nightUsesOf(layer, options);
5838
5996
  for (const [featureIndex, feature] of layer.features.entries()) {
@@ -5848,16 +6006,35 @@ function meshTileflowBuildings(layer, options) {
5848
6006
  const index = features.length;
5849
6007
  let used = false;
5850
6008
  for (const polygon of polygonsOf(feature.geometry, scale)) {
5851
- const rings = polygon.map(clip).filter((ring) => ring.length >= 6);
6009
+ let rings = polygon.map(clip).filter((ring) => ring.length >= 6);
5852
6010
  if (rings.length === 0 || area2(rings[0]) === 0) continue;
5853
6011
  const [cx, cy] = centroid(rings[0]);
5854
6012
  if (options.masks?.length && (tileflowInsideRings(options.masks, cx, cy) || tileflowRingsOverlap(rings, options.masks, maskMargin / metresPerUnit)))
5855
6013
  continue;
6014
+ const part = {
6015
+ cell: Math.min(7, Math.max(0, Math.floor(cy / 1024))) * 8 + Math.min(7, Math.max(0, Math.floor(cx / 1024))),
6016
+ bounds: [Infinity, Infinity, -Infinity, -Infinity, height + 0.1],
6017
+ roofsFrom: roofs.length,
6018
+ roofsCount: 0,
6019
+ wallsFrom: walls.length,
6020
+ wallsCount: 0
6021
+ };
6022
+ for (const ring of rings)
6023
+ for (let at = 0; at < ring.length; at += 2) {
6024
+ part.bounds[0] = Math.min(part.bounds[0], ring[at] - 1);
6025
+ part.bounds[1] = Math.min(part.bounds[1], ring[at + 1] - 1);
6026
+ part.bounds[2] = Math.max(part.bounds[2], ring[at] + 1);
6027
+ part.bounds[3] = Math.max(part.bounds[3], ring[at + 1] + 1);
6028
+ }
5856
6029
  used = true;
5857
6030
  tallest = Math.max(tallest, height);
5858
6031
  const seed = hash(cx * 0.0731 + cy * 0.19137 + index * 1.618);
5859
6032
  const jitter = rings[0].some((value) => value <= 0 || value >= tileflowBuildingExtent) ? hash(height * 7.31 + base * 3.17 + tone) : seed;
5860
6033
  const building = { base, top: height, jitter, seed, tone, group, use, cx, cy, index };
6034
+ const rounded = rings.map(
6035
+ (ring, at) => roundCorners(ring, at, edgeRadius, options.cornerAngle)
6036
+ );
6037
+ rings = rounded.map(({ ring }) => ring);
5861
6038
  const roof = polygon.length === 1 && rings.length === 1 ? tileflowRoofOf(
5862
6039
  polygon[0],
5863
6040
  rings[0],
@@ -5869,6 +6046,7 @@ function meshTileflowBuildings(layer, options) {
5869
6046
  ) : void 0;
5870
6047
  if (roof) {
5871
6048
  tallest = Math.max(tallest, roof.ridge);
6049
+ part.bounds[4] = Math.max(part.bounds[4], roof.ridge + 0.1);
5872
6050
  writeRoof(roof, building, vertices, roofs);
5873
6051
  } else {
5874
6052
  writeFlatRoof(rings, building, vertices, roofs);
@@ -5902,6 +6080,14 @@ function meshTileflowBuildings(layer, options) {
5902
6080
  if (length === 0 || onBorder(ax, ay, bx, by)) continue;
5903
6081
  const normalX = -(by - ay) / length * outward;
5904
6082
  const normalY = (bx - ax) / length * outward;
6083
+ const startNormal = rounded[ringIndex].normals[at] ?? [normalX, normalY];
6084
+ const endNormal = rounded[ringIndex].normals[(at + 1) % points] ?? [normalX, normalY];
6085
+ const normalAt = (share3) => {
6086
+ const x3 = startNormal[0] + (endNormal[0] - startNormal[0]) * share3;
6087
+ const y2 = startNormal[1] + (endNormal[1] - startNormal[1]) * share3;
6088
+ const length2 = Math.hypot(x3, y2) || 1;
6089
+ return [x3 / length2, y2 / length2];
6090
+ };
5905
6091
  const shares = [0, ...roof ? roof.breaks(ax, ay, bx, by) : [], 1];
5906
6092
  for (let piece = 0; piece + 1 < shares.length; piece += 1) {
5907
6093
  const first = piece === 0;
@@ -5914,6 +6100,8 @@ function meshTileflowBuildings(layer, options) {
5914
6100
  const corners = (first && tight(at) ? 1 : 0) | (last && tight(at + 1) ? 2 : 0) | (ringIndex > 0 ? 4 : 0);
5915
6101
  const cut = (first && onEdge(ax, ay) ? 1 : 0) | (last && onEdge(bx, by) ? 2 : 0);
5916
6102
  const end = along + metres;
6103
+ const sn = normalAt(shares[piece]);
6104
+ const en = normalAt(shares[piece + 1]);
5917
6105
  const startTop = topAt(sx, sy);
5918
6106
  const endTop = topAt(ex, ey);
5919
6107
  const v2 = vertices.write(
@@ -5921,8 +6109,8 @@ function meshTileflowBuildings(layer, options) {
5921
6109
  sx,
5922
6110
  sy,
5923
6111
  0,
5924
- normalX,
5925
- normalY,
6112
+ sn[0],
6113
+ sn[1],
5926
6114
  1,
5927
6115
  along,
5928
6116
  corners,
@@ -5936,8 +6124,8 @@ function meshTileflowBuildings(layer, options) {
5936
6124
  ex,
5937
6125
  ey,
5938
6126
  0,
5939
- normalX,
5940
- normalY,
6127
+ en[0],
6128
+ en[1],
5941
6129
  1,
5942
6130
  end,
5943
6131
  corners,
@@ -5951,8 +6139,8 @@ function meshTileflowBuildings(layer, options) {
5951
6139
  sx,
5952
6140
  sy,
5953
6141
  1,
5954
- normalX,
5955
- normalY,
6142
+ sn[0],
6143
+ sn[1],
5956
6144
  1,
5957
6145
  along,
5958
6146
  corners,
@@ -5966,8 +6154,8 @@ function meshTileflowBuildings(layer, options) {
5966
6154
  ex,
5967
6155
  ey,
5968
6156
  1,
5969
- normalX,
5970
- normalY,
6157
+ en[0],
6158
+ en[1],
5971
6159
  1,
5972
6160
  end,
5973
6161
  corners,
@@ -5981,14 +6169,44 @@ function meshTileflowBuildings(layer, options) {
5981
6169
  }
5982
6170
  }
5983
6171
  }
6172
+ part.roofsCount = roofs.length - part.roofsFrom;
6173
+ part.wallsCount = walls.length - part.wallsFrom;
6174
+ parts.push(part);
5984
6175
  }
5985
6176
  if (used) features.push(properties);
5986
6177
  }
5987
6178
  if (vertices.count === 0) return void 0;
5988
6179
  const indices = new Uint32Array(roofs.length + walls.length);
5989
- indices.set(roofs.values());
5990
- indices.set(walls.values(), roofs.length);
6180
+ const groups = [];
6181
+ let roofAt = 0;
6182
+ let wallAt = roofs.length;
6183
+ const roofIndices = roofs.values();
6184
+ const wallIndices = walls.values();
6185
+ parts.sort((a, b2) => a.cell - b2.cell);
6186
+ let cell = -1;
6187
+ for (const part of parts) {
6188
+ if (part.cell !== cell) {
6189
+ cell = part.cell;
6190
+ groups.push({
6191
+ bounds: [...part.bounds],
6192
+ roofsFrom: roofAt,
6193
+ roofsCount: 0,
6194
+ wallsFrom: wallAt,
6195
+ wallsCount: 0
6196
+ });
6197
+ }
6198
+ const group = groups.at(-1);
6199
+ for (let axis = 0; axis < 5; axis += 1)
6200
+ group.bounds[axis] = axis < 2 ? Math.min(group.bounds[axis], part.bounds[axis]) : Math.max(group.bounds[axis], part.bounds[axis]);
6201
+ indices.set(roofIndices.subarray(part.roofsFrom, part.roofsFrom + part.roofsCount), roofAt);
6202
+ indices.set(wallIndices.subarray(part.wallsFrom, part.wallsFrom + part.wallsCount), wallAt);
6203
+ roofAt += part.roofsCount;
6204
+ wallAt += part.wallsCount;
6205
+ group.roofsCount += part.roofsCount;
6206
+ group.wallsCount += part.wallsCount;
6207
+ }
5991
6208
  return {
6209
+ groups,
5992
6210
  vertices: vertices.bytes(),
5993
6211
  indices,
5994
6212
  wallsFrom: roofs.length,
@@ -6075,6 +6293,68 @@ function writeRoof(roof, building, vertices, roofs) {
6075
6293
  }
6076
6294
  }
6077
6295
  }
6296
+ function roundCorners(ring, ringIndex, radius, cornerAngle = Math.PI / 6) {
6297
+ if (radius < 1) return { ring, normals: [] };
6298
+ const points = ring.length / 2;
6299
+ const left = area2(ring) > 0;
6300
+ const outward = ringIndex === 0 ? left ? -1 : 1 : left ? 1 : -1;
6301
+ const result = [];
6302
+ const normals = [];
6303
+ const append = (x3, y2, normal) => {
6304
+ x3 = clampUnit(x3);
6305
+ y2 = clampUnit(y2);
6306
+ if (result.at(-2) === x3 && result.at(-1) === y2) return;
6307
+ result.push(x3, y2);
6308
+ normals.push(normal);
6309
+ };
6310
+ for (let at = 0; at < points; at += 1) {
6311
+ const before = (at + points - 1) % points;
6312
+ const after = (at + 1) % points;
6313
+ const [px, py] = [ring[before * 2], ring[before * 2 + 1]];
6314
+ const [x3, y2] = [ring[at * 2], ring[at * 2 + 1]];
6315
+ const [nx, ny] = [ring[after * 2], ring[after * 2 + 1]];
6316
+ const incoming = Math.hypot(x3 - px, y2 - py);
6317
+ const outgoing = Math.hypot(nx - x3, ny - y2);
6318
+ if (!incoming || !outgoing || onEdge(x3, y2) || onBorder(px, py, x3, y2) || onBorder(x3, y2, nx, ny)) {
6319
+ append(x3, y2);
6320
+ continue;
6321
+ }
6322
+ const [ix, iy] = [(x3 - px) / incoming, (y2 - py) / incoming];
6323
+ const [ox, oy] = [(nx - x3) / outgoing, (ny - y2) / outgoing];
6324
+ const turn = Math.atan2(ix * oy - iy * ox, ix * ox + iy * oy);
6325
+ if (turn * outward >= -0.08 || Math.abs(turn) > Math.PI * 0.85) {
6326
+ append(x3, y2);
6327
+ continue;
6328
+ }
6329
+ const tangent = Math.tan(Math.abs(turn) / 2);
6330
+ const reach = Math.min(radius * tangent, incoming * 0.2, outgoing * 0.2);
6331
+ const actualRadius = reach / tangent;
6332
+ if (actualRadius < 1) {
6333
+ append(x3, y2);
6334
+ continue;
6335
+ }
6336
+ const [normalX, normalY] = [-iy * outward, ix * outward];
6337
+ const [cx, cy] = [
6338
+ x3 - ix * reach - normalX * actualRadius,
6339
+ y2 - iy * reach - normalY * actualRadius
6340
+ ];
6341
+ const start = Math.atan2(normalY, normalX);
6342
+ const steps = Math.max(
6343
+ cornerAngle <= Math.PI / 6 ? 2 : 1,
6344
+ Math.ceil(Math.abs(turn) / Math.max(Math.PI / 6, Math.min(Math.PI / 3, cornerAngle)))
6345
+ );
6346
+ for (let step = 0; step <= steps; step += 1) {
6347
+ const angle = start + turn * step / steps;
6348
+ const normal = [Math.cos(angle), Math.sin(angle)];
6349
+ append(cx + normal[0] * actualRadius, cy + normal[1] * actualRadius, normal);
6350
+ }
6351
+ }
6352
+ if (result[0] === result.at(-2) && result[1] === result.at(-1)) {
6353
+ result.splice(-2);
6354
+ normals.pop();
6355
+ }
6356
+ return result.length >= 6 ? { ring: result, normals } : { ring, normals: [] };
6357
+ }
6078
6358
  var VertexWriter = class {
6079
6359
  #buffer = new ArrayBuffer(4096 * tileflowBuildingVertexBytes);
6080
6360
  #view = new DataView(this.#buffer);
@@ -6293,6 +6573,330 @@ function hash(value) {
6293
6573
  return s - Math.floor(s);
6294
6574
  }
6295
6575
 
6576
+ // src/buildings-3d-mesher.ts
6577
+ function browserWorker() {
6578
+ if (typeof Worker === "undefined" || false)
6579
+ return;
6580
+ const url = URL.createObjectURL(
6581
+ new Blob(['"use strict";(()=>{var An=new Set,Dn=!1;function kn(n,e,t=2){let s=e&&e.length,o=s?e[0]*t:n.length;An.size&&An.clear();let r=he(n,0,o,t,!0),i=[];if(!r||r.next===r.prev)return i;let u=0,l=0,c=0;if(s&&(r=_e(n,e,r,t)),n.length>80*t){u=n[0],l=n[1];let a=u,f=l;for(let h=t;h<o;h+=t){let m=n[h],b=n[h+1];m<u&&(u=m),b<l&&(l=b),m>a&&(a=m),b>f&&(f=b)}c=Math.max(a-u,f-l),c=c!==0?32767/c:0}return Wn(r,i,u,l,c),i}function he(n,e,t,s,o){let r=null;if(o===Ye(n,e,t,s)>0)for(let i=e;i<t;i+=s)r=fe(i/s|0,n[i],n[i+1],r);else for(let i=t-s;i>=e;i-=s)r=fe(i/s|0,n[i],n[i+1],r);return r&&In(r,r.next)&&(Tn(r),r=r.next),r}function fn(n,e=n){let t=e===n,s=n,o;do o=!1,s!==s.next&&(An.size===0||!An.has(s))&&(In(s,s.next)||O(s.prev,s,s.next)===0)?((t||s===e)&&(e=s.prev),Dn=!0,Tn(s),s=s.prev,o=!0):(t||s!==e)&&(s=s.next,o=!t);while(o||s!==e);return e}function Wn(n,e,t,s,o){o&&Se(n,t,s,o);let r=n,i=!1;for(;n.prev!==n.next;){let u=n.prev,l=n.next;if(O(u,n,l)<0&&(o?ke(n,t,s,o):Te(n))){e.push(u.i,n.i,l.i),Tn(n),n=l,r=l;continue}if(n=l,n===r){if(Dn=!1,n=fn(n),Dn){r=n;continue}if(!i){n=Be(n,e),r=n,i=!0;continue}Fe(n,e,t,s,o);break}}}function Te(n){let e=n.prev,t=n,s=n.next,o=e.x,r=t.x,i=s.x,u=e.y,l=t.y,c=s.y,a=Math.min(o,r,i),f=Math.min(u,l,c),h=Math.max(o,r,i),m=Math.max(u,l,c),b=s.next;for(;b!==e;){if(b.x>=a&&b.x<=h&&b.y>=f&&b.y<=m&&!(o===b.x&&u===b.y)&&Vn(o,u,r,l,i,c,b.x,b.y)&&O(b.prev,b,b.next)>=0)return!1;b=b.next}return!0}function ke(n,e,t,s){let o=n.prev,r=n,i=n.next,u=o.x,l=r.x,c=i.x,a=o.y,f=r.y,h=i.y,m=Math.min(u,l,c),b=Math.min(a,f,h),d=Math.max(u,l,c),x=Math.max(a,f,h),I=$n(m,b,e,t,s),B=$n(d,x,e,t,s),M=n.prevZ;for(;M&&M.z>=I;){if(M.x>=m&&M.x<=d&&M.y>=b&&M.y<=x&&M!==i&&!(u===M.x&&a===M.y)&&Vn(u,a,l,f,c,h,M.x,M.y)&&O(M.prev,M,M.next)>=0)return!1;M=M.prevZ}let y=n.nextZ;for(;y&&y.z<=B;){if(y.x>=m&&y.x<=d&&y.y>=b&&y.y<=x&&y!==i&&!(u===y.x&&a===y.y)&&Vn(u,a,l,f,c,h,y.x,y.y)&&O(y.prev,y,y.next)>=0)return!1;y=y.nextZ}return!0}function Be(n,e){let t=n,s=!1;do{let o=t.prev,r=t.next.next;xe(o,t,t.next,r,!1)&&Un(o,r)&&Un(r,o)&&(e.push(o.i,t.i,r.i),Tn(t),Tn(t.next),t=n=r,s=!0),t=t.next}while(t!==n);return s?fn(t):t}function Fe(n,e,t,s,o){let r=n;do{let i=r.next.next;for(;i!==r.prev;){if(r.i!==i.i&&He(r,i)){let u=de(r,i);r=fn(r,r.next),u=fn(u,u.next),Wn(r,e,t,s,o),Wn(u,e,t,s,o);return}i=i.next}r=r.next}while(r!==n)}var Kn=!1;function _e(n,e,t,s){let o=[];for(let r=0,i=e.length;r<i;r++){let u=e[r]*s,l=r<i-1?e[r+1]*s:n.length,c=he(n,u,l,s,!1);c===c.next&&An.add(c),o.push(Ee(c))}o.sort(Pe),Le(n.length/s,e.length),be(t,t),Kn=!0;for(let r=0;r<o.length;r++)t=Oe(o[r],t);return Kn=!1,fn(t)}function Pe(n,e){return n.x-e.x||n.y-e.y||(n.next.y-n.y)/(n.next.x-n.x)-(e.next.y-e.y)/(e.next.x-e.x)}function Oe(n,e){let t=ze(n,e);if(!t)return e;let s=de(t,n),o=s.next;return be(t,o.next),fn(s,s.next),fn(t,t.next)}var me=16,_=new Float64Array(0),Nn=0,Jn=[],Qn=[];function Le(n,e){let t=Math.ceil((n+2*e)/me)+e+2;_.length<t*4&&(_=new Float64Array(t*4)),Nn=0}function be(n,e){let t=n;do{let s=Nn++;Jn[s]=t;let o=t.x,r=t.y,i=t.x,u=t.y,l=0;do{let a=t.next;t.z=s,a.x<o&&(o=a.x),a.x>i&&(i=a.x),a.y<r&&(r=a.y),a.y>u&&(u=a.y),t=a}while(++l<me&&t!==e);Qn[s]=t;let c=s*4;_[c]=o,_[c+1]=r,_[c+2]=i,_[c+3]=u}while(t!==e)}function Ce(n,e){let t=n.z*4;e.x<_[t]&&(_[t]=e.x),e.y<_[t+1]&&(_[t+1]=e.y),e.x>_[t+2]&&(_[t+2]=e.x),e.y>_[t+3]&&(_[t+3]=e.y)}function ce(n){let e=Qn[n];for(;e.prev.next!==e;)e=e.next;return Qn[n]=e,e}function ae(n){let e=Jn[n];for(;e.prev.next!==e;)e=e.next;return Jn[n]=e,e}function ze(n,e){let t=e,s=n.x,o=n.y,r=-1/0,i;if(In(n,t))return t;for(let h=0,m=0;h<Nn;h++,m+=4){if(o<_[m+1]||o>_[m+3]||_[m]>s||_[m+2]<=r)continue;let b=ce(h);t=ae(h);do{if(t.prev.next===t){if(In(n,t.next))return t.next;if(o<=t.y&&o>=t.next.y&&t.next.y!==t.y){let d=t.x+(o-t.y)*(t.next.x-t.x)/(t.next.y-t.y);if(d<=s&&d>r&&(r=d,i=t.x<t.next.x?t:t.next,d===s))return i}}t=t.next}while(t!==b)}if(!i)return null;let u=i.x,l=i.y,c=Math.min(o,l),a=Math.max(o,l),f=1/0;for(let h=0,m=0;h<Nn;h++,m+=4){if(_[m+2]<u||_[m]>s||_[m+3]<c||_[m+1]>a)continue;let b=ce(h);t=ae(h);do{if(t.prev.next===t&&s>=t.x&&t.x>=u&&s!==t.x&&Vn(o<l?s:r,o,u,l,o<l?r:s,o,t.x,t.y)){let d=Math.abs(o-t.y)/(s-t.x);(Un(t,n)||t.y===o&&t.next.y===o&&t.next.x>s)&&(d<f||d===f&&(t.x>i.x||t.x===i.x&&Ze(i,t)))&&(i=t,f=d)}t=t.next}while(t!==b)}return i}function Ze(n,e){return O(n.prev,n,e.prev)<0&&O(e.next,n,n.next)<0}var K=[],dn=[],an=new Uint32Array(0),vn=new Uint32Array(0),Rn=new Uint32Array(256);function Se(n,e,t,s){let o=n,r=0;do o.z=$n(o.x,o.y,e,t,s),K[r++]=o,o=o.next;while(o!==n);je(r);let i=null;for(let u=0;u<r;u++){let l=K[u];l.prevZ=i,i&&(i.nextZ=l),i=l}i.nextZ=null,K.fill(null,0,r),dn.fill(null,0,r)}function je(n){if(n<=32){for(let e=1;e<n;e++){let t=K[e],s=t.z,o=e-1;for(;o>=0&&K[o].z>s;)K[o+1]=K[o],o--;K[o+1]=t}return}an.length<n&&(an=new Uint32Array(n),vn=new Uint32Array(n),dn=new Array(n));for(let e=0;e<n;e++)an[e]=K[e].z;En(n,K,an,dn,vn,0),En(n,dn,vn,K,an,8),En(n,K,an,dn,vn,16),En(n,dn,vn,K,an,24)}function En(n,e,t,s,o,r){Rn.fill(0);for(let u=0;u<n;u++)Rn[t[u]>>>r&255]++;let i=0;for(let u=0;u<256;u++){let l=Rn[u];Rn[u]=i,i+=l}for(let u=0;u<n;u++){let l=t[u],c=Rn[l>>>r&255]++;s[c]=e[u],o[c]=l}}function $n(n,e,t,s,o){return n=(n-t)*o|0,e=(e-s)*o|0,n=(n|n<<8)&16711935,n=(n|n<<4)&252645135,n=(n|n<<2)&858993459,n=(n|n<<1)&1431655765,e=(e|e<<8)&16711935,e=(e|e<<4)&252645135,e=(e|e<<2)&858993459,e=(e|e<<1)&1431655765,n|e<<1}function Ee(n){let e=n,t=n;do(e.x<t.x||e.x===t.x&&e.y<t.y)&&(t=e),e=e.next;while(e!==n);return t}function Vn(n,e,t,s,o,r,i,u){return(o-i)*(e-u)>=(n-i)*(r-u)&&(n-i)*(s-u)>=(t-i)*(e-u)&&(t-i)*(r-u)>=(o-i)*(s-u)}function He(n,e){let t=In(n,e)&&O(n.prev,n,n.next)>0&&O(e.prev,e,e.next)>0;return n.next.i!==e.i&&(t||Un(n,e)&&Un(e,n)&&(O(n.prev,n,e.prev)!==0||O(n,e.prev,e)!==0))&&!Ne(n,e)&&(t||Ve(n,e))}function O(n,e,t){return(e.y-n.y)*(t.x-e.x)-(e.x-n.x)*(t.y-e.y)}function In(n,e){return n.x===e.x&&n.y===e.y}function xe(n,e,t,s,o=!0){let r=O(n,e,t),i=O(n,e,s),u=O(t,s,n),l=O(t,s,e);return(r>0&&i<0||r<0&&i>0)&&(u>0&&l<0||u<0&&l>0)?!0:o?!!(r===0&&Hn(n,t,e)||i===0&&Hn(n,s,e)||u===0&&Hn(t,n,s)||l===0&&Hn(t,e,s)):!1}function Hn(n,e,t){return e.x<=Math.max(n.x,t.x)&&e.x>=Math.min(n.x,t.x)&&e.y<=Math.max(n.y,t.y)&&e.y>=Math.min(n.y,t.y)}function Ne(n,e){let t=Math.min(n.x,e.x),s=Math.max(n.x,e.x),o=Math.min(n.y,e.y),r=Math.max(n.y,e.y),i=n;do{let u=i.next;if(i.x>s&&u.x>s||i.x<t&&u.x<t||i.y>r&&u.y>r||i.y<o&&u.y<o){i=u;continue}if(i.i!==n.i&&u.i!==n.i&&i.i!==e.i&&u.i!==e.i&&xe(i,u,n,e))return!0;i=u}while(i!==n);return!1}function Un(n,e){return O(n.prev,n,n.next)<0?O(n,e,n.next)>=0&&O(n,n.prev,e)>=0:O(n,e,n.prev)<0||O(n,n.next,e)<0}function Ve(n,e){let t=n,s=!1,o=(n.x+e.x)/2,r=(n.y+e.y)/2;do{let i=t.next;t.y>r!=i.y>r&&o<(i.x-t.x)*(r-t.y)/(i.y-t.y)+t.x&&(s=!s),t=i}while(t!==n);return s}function de(n,e){let t=qn(n.i,n.x,n.y),s=qn(e.i,e.x,e.y),o=n.next,r=e.prev;return n.next=e,e.prev=n,t.next=o,o.prev=t,s.next=t,t.prev=s,r.next=s,s.prev=r,s}function fe(n,e,t,s){let o=qn(n,e,t);return s?(o.next=s.next,o.prev=s,s.next.prev=o,s.next=o):(o.prev=o,o.next=o),o}function Tn(n){n.next.prev=n.prev,n.prev.next=n.next,n.prevZ&&(n.prevZ.nextZ=n.nextZ),n.nextZ&&(n.nextZ.prevZ=n.prevZ),Kn&&Ce(n.prev,n.next)}function qn(n,e,t){return{i:n,x:e,y:t,prev:null,next:null,z:0,prevZ:null,nextZ:null}}function Ye(n,e,t,s){let o=0;for(let r=e,i=t-s;r<t;r+=s)o+=(n[i]-n[r])*(n[r+1]+n[i+1]),i=r;return o}var Xe=.7,Ge=Math.tan(Math.PI/6),De=Math.tan(35*Math.PI/180),We=Math.tan(Math.PI/18),Bn=.65,Fn=.4;function Me(n,e,t,s,o,r,i){let u=String(t.roof_shape??"");if(!u||u==="flat"||u==="sawtooth"||qe(n,i))return;let l=Qe(n);if(!l||Math.abs(ne(n))<Xe*4*l.halfU*l.halfV)return;let c=l.u,a=l.v,f=l.halfU*r,h=l.halfV*r;f<h&&([c,a,f,h]=[a,c,h,f]);let m=ge(t.roof_direction),b=m===void 0?void 0:[Math.sin(m*Math.PI/180),-Math.cos(m*Math.PI/180)];(b!==void 0?Math.abs(b[0]*c[0]+b[1]*c[1])>Math.abs(b[0]*a[0]+b[1]*a[1]):t.roof_orientation==="across")&&u!=="skillion"&&([c,a,f,h]=[a,c,h,f]);let x=Je(u,f,h,b&&[pe(b,c),pe(b,a)]);if(!x)return;let I=Math.max(0,o-s),B=ge(t.roof_height),M,y;if(B!==void 0&&B>0)y=o,M=Math.max(s+Math.min(2,I/2),o-B);else{let p=Math.min(Ke(u,h),I*.6,30);t.height_source==="height"?(y=o,M=o-p):(M=o,y=o+p)}let k=y-M;if(!(k>=.3))return;let[R,N]=l.centre,F=(p,A)=>{let v=(p-R)*r,g=(A-N)*r;return[v*c[0]+g*c[1],v*a[0]+g*a[1]]},S=(p,A)=>[R+(p*c[0]+A*a[0])/r,N+(p*c[1]+A*a[1])/r],J=(p,A)=>{let v=1/0;for(let[g,E,H]of x)v=Math.min(v,g*p+E*A+H);return Math.max(0,v)},Q=(p,A)=>M+k*J(...F(p,A)),L=[];for(let p=0;p+1<e.length;p+=2)L.push(...F(e[p],e[p+1]));ne(L)<0&&nt(L);let C=1.01,$=[-f*C,-h*C,f*C,-h*C,f*C,h*C,-f*C,h*C],V=[];for(let[p,A]of x.entries()){let v=$;for(let[T,j]of x.entries()){if(T===p)continue;let z=A[0]-j[0],nn=A[1]-j[1],en=A[2]-j[2];if(Math.abs(z)+Math.abs(nn)+Math.abs(en)<1e-9){T<p&&(v=[]);continue}if(v=ye(v,z,nn,en),v.length<6)break}if(v.length<6)continue;let g=L,E=v.length/2;for(let T=0;T<E&&g.length>=6;T+=1){let[j,z]=[v[T*2],v[T*2+1]],[nn,en]=[v[(T+1)%E*2],v[(T+1)%E*2+1]];g=ye(g,en-z,j-nn,-(en-z)*j-(j-nn)*z)}if(g.length<6||Math.abs(ne(g))<1e-6)continue;let H=[];for(let T=0;T<g.length;T+=2)H.push(...S(g[T],g[T+1]));let q=kn(H);if(q.length===0)continue;let on=k*A[0],Y=k*A[1],w=on*c[0]+Y*a[0],U=on*c[1]+Y*a[1],rn=Math.hypot(w,U,1);V.push({ring:H,triangles:q,normal:[-w/rn,-U/rn]})}if(V.length!==0)return{eaves:M,ridge:y,faces:V,heightAt:Q,breaks(p,A,v,g){let[E,H]=F(p,A),[q,on]=F(v,g),Y=[];for(let w=0;w<x.length;w+=1)for(let U=w+1;U<x.length;U+=1){let rn=(Z,P)=>(x[w][0]-x[U][0])*Z+(x[w][1]-x[U][1])*P+(x[w][2]-x[U][2]),T=rn(E,H),j=rn(q,on);if(T===j||T*j>0)continue;let z=T/(T-j);if(z<=1e-4||z>=1-1e-4)continue;let nn=E+(q-E)*z,en=H+(on-H)*z;x[w][0]*nn+x[w][1]*en+x[w][2]<=J(nn,en)+1e-6&&Y.push(z)}return[...new Set(Y.map(w=>Math.round(w*1e6)/1e6))].sort((w,U)=>w-U)}}}function Ke(n,e){switch(n){case"skillion":return 2*e*We;case"pyramidal":return e*De;case"gambrel":case"mansard":return e*.8;case"dome":case"onion":case"round":return e;default:return e*Ge}}function Je(n,e,t,s){let o=e,r=t,i=[[0,-1/r,1],[0,1/r,1]],u=c=>c.flatMap(a=>[1,-1].flatMap(f=>{let h=a==="v"?r:o,m=Bn/(Fn*r)*f,b=(1-Bn)/((1-Fn)*r)*f,d=x=>a==="v"?[0,-x]:[-x,0];return[[...d(m),Bn/(Fn*r)*h],[...d(b),Bn+(1-Bn)/((1-Fn)*r)*(h-Fn*r)]]})),l=[.99,.8,.5,.2];switch(n){case"gabled":return i;case"saltbox":{let c=r/3;return[[0,-1/(r-c),1+c/(r-c)],[0,1/(r+c),1-c/(r+c)]]}case"hipped":return[...i,[-1/r,0,o/r],[1/r,0,o/r]];case"half_hipped":return[...i,[-1/r,0,.5+o/r],[1/r,0,.5+o/r]];case"pyramidal":return[...i,[-1/o,0,1],[1/o,0,1]];case"gambrel":return u(["v"]);case"mansard":return u(["v","u"]);case"skillion":{let[c,a]=s??[0,1],f=Math.abs(c)*o+Math.abs(a)*r;return f>0?[[-c/(2*f),-a/(2*f),.5]]:void 0}case"round":return[[0,0,1],...l.flatMap(c=>{let a=Math.sqrt(1-c*c);return[1,-1].map(f=>[0,-f*c/(r*a),1/a])})];case"dome":case"onion":return[[0,0,1],...l.flatMap(c=>{let a=Math.sqrt(1-c*c);return Array.from({length:12},(f,h)=>{let m=h*Math.PI/6;return[-c*Math.cos(m)/(o*a),-c*Math.sin(m)/(r*a),1/a]})})];default:return}}function ye(n,e,t,s){let o=n.length/2,r=[];for(let i=0;i<o;i+=1){let u=n[i*2],l=n[i*2+1],c=n[(i+1)%o*2],a=n[(i+1)%o*2+1],f=e*u+t*l+s,h=e*c+t*a+s;if(f<=0&&r.push(u,l),f<0&&h>0||f>0&&h<0){let m=f/(f-h);r.push(u+(c-u)*m,l+(a-l)*m)}}return r}function Qe(n){let e=$e(n);if(e.length<3)return;let t,s=1/0;for(let o=0;o<e.length;o+=1){let[r,i]=e[o],[u,l]=e[(o+1)%e.length],c=Math.hypot(u-r,l-i);if(c===0)continue;let a=[(u-r)/c,(l-i)/c],f=[-a[1],a[0]],[h,m,b,d]=[1/0,-1/0,1/0,-1/0];for(let[I,B]of e){let M=I*a[0]+B*a[1],y=I*f[0]+B*f[1];h=Math.min(h,M),m=Math.max(m,M),b=Math.min(b,y),d=Math.max(d,y)}let x=(m-h)*(d-b);if(x<s){s=x;let I=(h+m)/2,B=(b+d)/2;t={centre:[I*a[0]+B*f[0],I*a[1]+B*f[1]],u:a,v:f,halfU:(m-h)/2,halfV:(d-b)/2}}}return t&&t.halfU>0&&t.halfV>0?t:void 0}function $e(n){let e=[];for(let r=0;r+1<n.length;r+=2)e.push([n[r],n[r+1]]);e.sort((r,i)=>r[0]-i[0]||r[1]-i[1]);let t=(r,i,u)=>(i[0]-r[0])*(u[1]-r[1])-(i[1]-r[1])*(u[0]-r[0]),s=[];for(let r of e){for(;s.length>=2&&t(s.at(-2),s.at(-1),r)<=0;)s.pop();s.push(r)}let o=[];for(let r of e.reverse()){for(;o.length>=2&&t(o.at(-2),o.at(-1),r)<=0;)o.pop();o.push(r)}return[...s.slice(0,-1),...o.slice(0,-1)]}function qe(n,e){let[t,s,o,r]=[1/0,-1/0,1/0,-1/0];for(let u=0;u+1<n.length;u+=2)t=Math.min(t,n[u]),s=Math.max(s,n[u]),o=Math.min(o,n[u+1]),r=Math.max(r,n[u+1]);let i=n.length/2;for(let u=0;u<i;u+=1){let l=n[u*2],c=n[u*2+1],a=n[(u+1)%i*2],f=n[(u+1)%i*2+1];if(t<0&&l===t&&a===t||s>e&&l===s&&a===s||o<0&&c===o&&f===o||r>e&&c===r&&f===r)return!0}return!1}function ne(n){let e=0,t=n.length/2;for(let s=0;s<t;s+=1){let o=(s+1)%t;e+=n[s*2]*n[o*2+1]-n[o*2]*n[s*2+1]}return e/2}function nt(n){let e=[];for(let t=0;t<n.length;t+=2)e.push([n[t],n[t+1]]);e.reverse(),n.splice(0,n.length,...e.flat())}function pe(n,e){return n[0]*e[0]+n[1]*e[1]}function ge(n){let e=typeof n=="string"?Number(n):n;return typeof e=="number"&&Number.isFinite(e)?e:void 0}var gt=Object.freeze({surface:0,secondary:1,glass:2,accent:3,roof:4}),Mt=Object.freeze({levels:3,vertices:2e5,triangles:2e5,footprintPoints:4096,radius:2e3,height:1e3,models:512,bytes:16*1024*1024});function ee(n,e,t){for(let s of n){let o=!1;for(let r=0,i=s.length-2;r<s.length;i=r,r+=2){let u=s[r],l=s[r+1],c=s[i],a=s[i+1];l>t!=a>t&&e<(c-u)*(t-l)/(a-l)+u&&(o=!o)}if(o)return!0}return!1}function Re(n,e,t){for(let s of e){let[o,r,i,u]=ve(s);for(let l of n){let[c,a,f,h]=ve(l);if(!(c>i-t||f<o+t||a>u-t||h<r+t)&&(we(l,s,t)||we(s,l,t)))return!0}}return!1}function we(n,e,t){let s=(o,r)=>ee([e],o,r)&&tt(e,o,r)>t;for(let o=0;o+1<n.length;o+=2)if(s(n[o],n[o+1]))return!0;for(let o=0,r=n.length-2;o<n.length;r=o,o+=2){let[i,u,l,c]=[n[r],n[r+1],n[o],n[o+1]],a=[0,1];for(let f=0,h=e.length-2;f<e.length;h=f,f+=2){let m=et(i,u,l,c,e[h],e[h+1],e[f],e[f+1]);m!==void 0&&a.push(m)}if(a.length!==2){a.sort((f,h)=>f-h);for(let f=1;f<a.length;f+=1){let h=(a[f-1]+a[f])/2;if(s(i+(l-i)*h,u+(c-u)*h))return!0}}}return!1}function et(n,e,t,s,o,r,i,u){let l=t-n,c=s-e,a=i-o,f=u-r,h=l*f-c*a;if(h===0)return;let m=((o-n)*f-(r-e)*a)/h,b=((o-n)*c-(r-e)*l)/h;return m>0&&m<1&&b>=0&&b<=1?m:void 0}function tt(n,e,t){let s=1/0;for(let o=0,r=n.length-2;o<n.length;r=o,o+=2){let[i,u,l,c]=[n[r],n[r+1],n[o],n[o+1]],a=(l-i)**2+(c-u)**2,f=a>0?Math.max(0,Math.min(1,((e-i)*(l-i)+(t-u)*(c-u))/a)):0;s=Math.min(s,Math.hypot(e-(i+(l-i)*f),t-(u+(c-u)*f)))}return s}function ve(n){let[e,t,s,o]=[1/0,1/0,-1/0,-1/0];for(let r=0;r+1<n.length;r+=2)e=Math.min(e,n[r]),s=Math.max(s,n[r]),t=Math.min(t,n[r+1]),o=Math.max(o,n[r+1]);return[e,t,s,o]}var tn=8192,te=28,ot={commercial:1,active:2,destination:3},rt={residential:1,...Object.fromEntries(["civic","commercial","education","entertainment","medical","transportation"].map(n=>[n,2])),...Object.fromEntries(["agricultural","industrial","military","outbuilding","religious","service"].map(n=>[n,3]))},st={dining:1,entertainment:2,nightlife:3},oe={...Object.fromEntries(["alcoholic_beverage_venue","bar","beer","biergarten","brewery","casino","cocktail_bar","dance_club","karaoke","lounge","music_venue","night_club","nightclub","pub","wine_bar"].map(n=>[n,3])),...Object.fromEntries(["arts_centre","bowling_alley","cinema","comedy_club","concert_hall","movie_theater","opera","performing_arts_venue","theater","theatre","theatre_venue"].map(n=>[n,2])),...Object.fromEntries(["casual_eatery","fast_food","fast_food_restaurant","food_court","restaurant"].map(n=>[n,1]))},ut=6,it=.5,lt=.5;function Ue(n,e){let t=tn/n.extent,s=e.tileMetres/tn,o=e.defaultHeight??5,r=Number.isFinite(e.edgeRadius)?Math.max(0,Math.min(1,e.edgeRadius))/s:0,i=new re,u=new Yn,l=new Yn,c=[],a=[],f=0,h=ht(n,e);for(let[y,k]of n.features.entries()){if(k.type!=="Polygon"||c.length>=65535)continue;let R=k.properties,N=R.hide_3d;if(N===!0||N===1||N==="1"||N==="true")continue;let F=Math.min(6e3,Ae(R.render_height)??o),S=Math.min(F,Ae(R.render_min_height)??0),J=ot[String(R.building_tone??"")]??0,Q=rt[String(R.class??"")]??0,L=h.get(y)??0,C=c.length,$=!1;for(let V of mt(k.geometry,t)){let p=V.map(xt).filter(w=>w.length>=6);if(p.length===0||Xn(p[0])===0)continue;let[A,v]=bt(p[0]);if(e.masks?.length&&(ee(e.masks,A,v)||Re(p,e.masks,lt/s)))continue;let g={cell:Math.min(7,Math.max(0,Math.floor(v/1024)))*8+Math.min(7,Math.max(0,Math.floor(A/1024))),bounds:[1/0,1/0,-1/0,-1/0,F+.1],roofsFrom:u.length,roofsCount:0,wallsFrom:l.length,wallsCount:0};for(let w of p)for(let U=0;U<w.length;U+=2)g.bounds[0]=Math.min(g.bounds[0],w[U]-1),g.bounds[1]=Math.min(g.bounds[1],w[U+1]-1),g.bounds[2]=Math.max(g.bounds[2],w[U]+1),g.bounds[3]=Math.max(g.bounds[3],w[U+1]+1);$=!0,f=Math.max(f,F);let E=Ie(A*.0731+v*.19137+C*1.618),H=p[0].some(w=>w<=0||w>=tn)?Ie(F*7.31+S*3.17+J):E,q={base:S,top:F,jitter:H,seed:E,tone:J,group:Q,use:L,cx:A,cy:v,index:C},on=p.map((w,U)=>ft(w,U,r,e.cornerAngle));p=on.map(({ring:w})=>w);let Y=V.length===1&&p.length===1?Me(V[0],p[0],R,S,F,s,tn):void 0;Y?(f=Math.max(f,Y.ridge),g.bounds[4]=Math.max(g.bounds[4],Y.ridge+.1),at(Y,q,i,u)):ct(p,q,i,u);for(let[w,U]of p.entries()){let rn=Xn(U)>0,T=w===0?rn?-1:1:rn?1:-1,j=U.length/2,z=Z=>{let P=(Z%j+j)%j;return[U[P*2],U[P*2+1]]},nn=Z=>{let[P,D]=z(Z-1),[X,G]=z(Z),[un,mn]=z(Z+1);if(hn(P,D,X,G)||hn(X,G,un,mn))return!1;let bn=Math.hypot(X-P,G-D),ln=Math.hypot(un-X,mn-G);if(!bn||!ln)return!1;let _n=-(G-D)/bn*T,Pn=(X-P)/bn*T;return(_n*(un-X)+Pn*(mn-G))/ln>it},en=(Z,P)=>Y?Y.heightAt(Z,P):F,pn=0;for(let Z=0;Z<j;Z+=1){let[P,D]=z(Z),[X,G]=z(Z+1),un=Math.hypot(X-P,G-D);if(un===0||hn(P,D,X,G))continue;let mn=-(G-D)/un*T,bn=(X-P)/un*T,ln=on[w].normals[Z]??[mn,bn],_n=on[w].normals[(Z+1)%j]??[mn,bn],Pn=W=>{let gn=ln[0]+(_n[0]-ln[0])*W,Mn=ln[1]+(_n[1]-ln[1])*W,cn=Math.hypot(gn,Mn)||1;return[gn/cn,Mn/cn]},sn=[0,...Y?Y.breaks(P,D,X,G):[],1];for(let W=0;W+1<sn.length;W+=1){let gn=W===0,Mn=W+2===sn.length,cn=P+(X-P)*sn[W],On=D+(G-D)*sn[W],Ln=P+(X-P)*sn[W+1],Cn=D+(G-D)*sn[W+1],wn=Math.hypot(Ln-cn,Cn-On)/tn*e.tileMetres,zn=(gn&&nn(Z)?1:0)|(Mn&&nn(Z+1)?2:0)|(w>0?4:0),Zn=(gn&&se(P,D)?1:0)|(Mn&&se(X,G)?2:0),Gn=pn+wn,Sn=Pn(sn[W]),jn=Pn(sn[W+1]),ie=en(cn,On),le=en(Ln,Cn),xn=i.write(q,cn,On,0,Sn[0],Sn[1],1,pn,zn,wn,0,Zn,ie);i.write(q,Ln,Cn,0,jn[0],jn[1],1,Gn,zn,wn,1,Zn,le),i.write(q,cn,On,1,Sn[0],Sn[1],1,pn,zn,wn,0,Zn,ie),i.write(q,Ln,Cn,1,jn[0],jn[1],1,Gn,zn,wn,1,Zn,le),l.push(xn,xn+1,xn+2,xn+1,xn+3,xn+2),pn=Gn}}}g.roofsCount=u.length-g.roofsFrom,g.wallsCount=l.length-g.wallsFrom,a.push(g)}$&&c.push(R)}if(i.count===0)return;let m=new Uint32Array(u.length+l.length),b=[],d=0,x=u.length,I=u.values(),B=l.values();a.sort((y,k)=>y.cell-k.cell);let M=-1;for(let y of a){y.cell!==M&&(M=y.cell,b.push({bounds:[...y.bounds],roofsFrom:d,roofsCount:0,wallsFrom:x,wallsCount:0}));let k=b.at(-1);for(let R=0;R<5;R+=1)k.bounds[R]=R<2?Math.min(k.bounds[R],y.bounds[R]):Math.max(k.bounds[R],y.bounds[R]);m.set(I.subarray(y.roofsFrom,y.roofsFrom+y.roofsCount),d),m.set(B.subarray(y.wallsFrom,y.wallsFrom+y.wallsCount),x),d+=y.roofsCount,x+=y.wallsCount,k.roofsCount+=y.roofsCount,k.wallsCount+=y.wallsCount}return{groups:b,vertices:i.bytes(),indices:m,wallsFrom:u.length,top:f,features:c}}function ct(n,e,t,s){let o=[],r=[];for(let l of n)r.push(o.length/2),o.push(...l);r.push(o.length/2);let i=(l,c)=>{let a=r.findIndex((d,x)=>l>=d&&l<r[x+1]),[f,h]=[r[a],r[a+1]-1];if(c<f||c>h)return!1;let[m,b]=l<c?[l,c]:[c,l];return b-m!==1&&!(m===f&&b===h)?!1:!hn(o[l*2],o[l*2+1],o[c*2],o[c*2+1])},u=kn(o,r.slice(1,-1),2);for(let l=0;l+2<u.length;l+=3){let c=[u[l],u[l+1],u[l+2]],a=0;for(let f=0;f<3;f+=1)i(c[(f+1)%3],c[(f+2)%3])&&(a|=1<<f);for(let[f,h]of c.entries())s.push(t.write(e,o[h*2],o[h*2+1],1,0,0,0,0,0,0,f,a))}}function at(n,e,t,s){for(let o of n.faces){let r=o.ring,i=r.length/2,u=(l,c)=>{let[a,f]=l<c?[l,c]:[c,l];return f-a!==1&&!(a===0&&f===i-1)?!1:!hn(r[l*2],r[l*2+1],r[c*2],r[c*2+1])};for(let l=0;l+2<o.triangles.length;l+=3){let c=[o.triangles[l],o.triangles[l+1],o.triangles[l+2]],a=0;for(let f=0;f<3;f+=1)u(c[(f+1)%3],c[(f+2)%3])&&(a|=1<<f);for(let[f,h]of c.entries()){let m=r[h*2],b=r[h*2+1];s.push(t.write(e,m,b,1,o.normal[0],o.normal[1],0,0,0,0,f,a,n.heightAt(m,b)))}}}}function ft(n,e,t,s=Math.PI/6){if(t<1)return{ring:n,normals:[]};let o=n.length/2,r=Xn(n)>0,i=e===0?r?-1:1:r?1:-1,u=[],l=[],c=(a,f,h)=>{a=yn(a),f=yn(f),!(u.at(-2)===a&&u.at(-1)===f)&&(u.push(a,f),l.push(h))};for(let a=0;a<o;a+=1){let f=(a+o-1)%o,h=(a+1)%o,[m,b]=[n[f*2],n[f*2+1]],[d,x]=[n[a*2],n[a*2+1]],[I,B]=[n[h*2],n[h*2+1]],M=Math.hypot(d-m,x-b),y=Math.hypot(I-d,B-x);if(!M||!y||se(d,x)||hn(m,b,d,x)||hn(d,x,I,B)){c(d,x);continue}let[k,R]=[(d-m)/M,(x-b)/M],[N,F]=[(I-d)/y,(B-x)/y],S=Math.atan2(k*F-R*N,k*N+R*F);if(S*i>=-.08||Math.abs(S)>Math.PI*.85){c(d,x);continue}let J=Math.tan(Math.abs(S)/2),Q=Math.min(t*J,M*.2,y*.2),L=Q/J;if(L<1){c(d,x);continue}let[C,$]=[-R*i,k*i],[V,p]=[d-k*Q-C*L,x-R*Q-$*L],A=Math.atan2($,C),v=Math.max(s<=Math.PI/6?2:1,Math.ceil(Math.abs(S)/Math.max(Math.PI/6,Math.min(Math.PI/3,s))));for(let g=0;g<=v;g+=1){let E=A+S*g/v,H=[Math.cos(E),Math.sin(E)];c(V+H[0]*L,p+H[1]*L,H)}}return u[0]===u.at(-2)&&u[1]===u.at(-1)&&(u.splice(-2),l.pop()),u.length>=6?{ring:u,normals:l}:{ring:n,normals:[]}}var re=class{#n=new ArrayBuffer(4096*te);#e=new DataView(this.#n);count=0;write(e,t,s,o,r,i,u,l,c,a,f=0,h=0,m=e.top){let b=te;if((this.count+1)*b>this.#n.byteLength){let I=new ArrayBuffer(this.#n.byteLength*2);new Uint8Array(I).set(new Uint8Array(this.#n)),this.#n=I,this.#e=new DataView(I)}let d=this.#e,x=this.count*b;return d.setUint16(x,yn(t),!0),d.setUint16(x+2,yn(s),!0),d.setUint16(x+4,Math.min(65535,Math.round(e.base*10)),!0),d.setUint16(x+6,Math.min(65535,Math.max(0,Math.round(m*10))),!0),d.setInt8(x+8,Math.round(r*127)),d.setInt8(x+9,Math.round(i*127)),d.setUint8(x+10,o),d.setUint8(x+11,u),d.setUint16(x+12,Math.round(l*10)%65536,!0),d.setUint8(x+14,Math.round(e.jitter*255)),d.setUint8(x+15,Math.round(e.seed*255)),d.setUint16(x+16,yn(e.cx),!0),d.setUint16(x+18,yn(e.cy),!0),d.setUint8(x+20,e.tone+4*e.group),d.setUint8(x+21,e.use),d.setUint8(x+22,c),d.setUint8(x+23,Math.min(255,Math.round(a))),d.setUint16(x+24,e.index,!0),d.setUint8(x+26,f),d.setUint8(x+27,h),this.count+=1,this.count-1}bytes(){return new Uint8Array(this.#n.slice(0,this.count*te))}},Yn=class{#n=new Uint32Array(8192);length=0;push(...e){if(this.length+e.length>this.#n.length){let t=new Uint32Array(Math.max(this.length+e.length,this.#n.length*2));t.set(this.#n),this.#n=t}for(let t of e)this.#n[this.length++]=t}values(){return this.#n.subarray(0,this.length)}};function ht(n,e){let t=new Map;if(n.features.some(u=>u.properties.night_use!==void 0)){for(let[u,l]of n.features.entries()){let c=st[String(l.properties.night_use??"")];c&&t.set(u,c)}return t}let s=e.places;if(!s||s.features.length===0)return t;let o=ut/e.tileMetres*n.extent,r=[];for(let[u,l]of n.features.entries()){if(l.type!=="Polygon")continue;let c=l.properties.hide_3d;if(c===!0||c===1||c==="1"||c==="true")continue;let a=[1/0,1/0,-1/0,-1/0];for(let f of l.geometry)for(let h=0;h+1<f.length;h+=2)a[0]=Math.min(a[0],f[h]),a[1]=Math.min(a[1],f[h+1]),a[2]=Math.max(a[2],f[h]),a[3]=Math.max(a[3],f[h+1]);r.push({index:u,rings:l.geometry,box:a})}let i=n.extent/s.extent;for(let u of s.features){if(u.type!=="Point")continue;let l=u.properties,c=oe[String(l.type??"")]??oe[String(l.subclass??"")]??oe[String(l.class??"")];if(c)for(let a of u.geometry)for(let f=0;f+1<a.length;f+=2){let h=a[f]*i,m=a[f+1]*i,b,d,x=o;for(let B of r){let[M,y,k,R]=B.box;if(h<M-o||h>k+o||m<y-o||m>R+o)continue;let N=!1;for(let F of B.rings)for(let S=0,J=F.length-2;S+1<F.length;J=S,S+=2){let Q=F[J],L=F[J+1],C=F[S],$=F[S+1];$>m!=L>m&&h<(Q-C)*(m-$)/(L-$)+C&&(N=!N);let V=C-Q,p=$-L,A=V*V+p*p,v=A?Math.min(1,Math.max(0,((h-Q)*V+(m-L)*p)/A)):0,g=Math.hypot(h-Q-v*V,m-L-v*p);g<x&&(x=g,d=B.index)}if(N){b=B.index;break}}let I=b??d;I!==void 0&&t.set(I,Math.max(t.get(I)??0,c))}}return t}function yn(n){return Math.min(tn,Math.max(0,Math.round(n)))}function Ae(n){let e=Number(n);return Number.isFinite(e)&&e>0?e:void 0}function mt(n,e){let t=[],s;for(let o of n){let r=[];for(let u=0;u+1<o.length;u+=2){let l=o[u]*e,c=o[u+1]*e;r.length&&l===r.at(-2)&&c===r.at(-1)||r.push(l,c)}if(r.length>=6&&r[0]===r.at(-2)&&r[1]===r.at(-1)&&(r.length-=2),r.length<6)continue;let i=Math.sign(Xn(r));i&&(s??=i,i===s||t.length===0?t.push([r]):t.at(-1).push(r))}return t}function Xn(n){let e=0,t=n.length/2;for(let s=0;s<t;s+=1){let o=(s+1)%t;e+=n[s*2]*n[o*2+1]-n[o*2]*n[s*2+1]}return e}function bt(n){let e=0,t=0,s=n.length/2;for(let o=0;o<s;o+=1)e+=n[o*2],t+=n[o*2+1];return[e/s,t/s]}function se(n,e){return n<=0||e<=0||n>=tn||e>=tn}function hn(n,e,t,s){let o=tn;return n<=0&&t<=0||n>=o&&t>=o||e<=0&&s<=0||e>=o&&s>=o}function xt(n){let e=tn,t=[...n],s=[[0,0,o=>o>=0],[0,e,o=>o<=e],[1,0,o=>o>=0],[1,e,o=>o<=e]];for(let[o,r,i]of s){let u=[],l=t.length/2;for(let c=0;c<l;c+=1){let a=[t[c*2],t[c*2+1]],f=[t[(c+1)%l*2],t[(c+1)%l*2+1]],h=i(a[o]),m=i(f[o]);if(h&&u.push(a[0],a[1]),h!==m){let b=(r-a[o])/(f[o]-a[o]);u.push(a[0]+(f[0]-a[0])*b,a[1]+(f[1]-a[1])*b)}}if(t=u,t.length<6)return[]}return t}function Ie(n){let e=Math.sin(n*12.9898+78.233)*43758.5453;return e-Math.floor(e)}var ue=globalThis;ue.onmessage=({data:n})=>{try{let e=Ue(n.layer,n.options);ue.postMessage({mesh:e},e?[e.vertices.buffer,e.indices.buffer]:[])}catch(e){ue.postMessage({error:String(e)})}};})();\n'], { type: "text/javascript" })
6582
+ );
6583
+ try {
6584
+ return new Worker(url, { name: "tileflow-buildings" });
6585
+ } finally {
6586
+ URL.revokeObjectURL(url);
6587
+ }
6588
+ }
6589
+ function createBuildingMesher(createWorker = browserWorker) {
6590
+ let worker2;
6591
+ let unavailable = false;
6592
+ let disposed = false;
6593
+ let finish;
6594
+ let tail = Promise.resolve();
6595
+ return {
6596
+ build(layer, options, signal) {
6597
+ const result = tail.then(async () => {
6598
+ if (disposed || signal.aborted) throw new Error("Building mesh cancelled");
6599
+ if (!worker2 && !unavailable) {
6600
+ try {
6601
+ worker2 = createWorker();
6602
+ } catch {
6603
+ }
6604
+ if (!worker2) unavailable = true;
6605
+ }
6606
+ if (!worker2) return meshTileflowBuildings(layer, options);
6607
+ const active = worker2;
6608
+ return new Promise((resolve, reject) => {
6609
+ const cleanup = () => {
6610
+ clearTimeout(timeout);
6611
+ finish = void 0;
6612
+ active.onmessage = null;
6613
+ active.onerror = null;
6614
+ };
6615
+ const fallback = () => {
6616
+ cleanup();
6617
+ active.terminate();
6618
+ worker2 = void 0;
6619
+ unavailable = true;
6620
+ if (disposed || signal.aborted) {
6621
+ reject(new Error("Building mesh cancelled"));
6622
+ return;
6623
+ }
6624
+ try {
6625
+ resolve(meshTileflowBuildings(layer, options));
6626
+ } catch (error) {
6627
+ reject(error);
6628
+ }
6629
+ };
6630
+ const timeout = setTimeout(fallback, 15e3);
6631
+ finish = () => {
6632
+ cleanup();
6633
+ reject(new Error("Building mesher disposed"));
6634
+ };
6635
+ active.onerror = (event) => {
6636
+ event.preventDefault();
6637
+ fallback();
6638
+ };
6639
+ active.onmessage = ({
6640
+ data
6641
+ }) => {
6642
+ cleanup();
6643
+ if (disposed || signal.aborted) reject(new Error("Building mesh cancelled"));
6644
+ else if (data.error) reject(new Error(data.error));
6645
+ else resolve(data.mesh);
6646
+ };
6647
+ try {
6648
+ active.postMessage({ layer, options });
6649
+ } catch {
6650
+ fallback();
6651
+ }
6652
+ });
6653
+ });
6654
+ tail = result.then(
6655
+ () => {
6656
+ },
6657
+ () => {
6658
+ }
6659
+ );
6660
+ return result;
6661
+ },
6662
+ dispose() {
6663
+ disposed = true;
6664
+ finish?.();
6665
+ worker2?.terminate();
6666
+ worker2 = void 0;
6667
+ }
6668
+ };
6669
+ }
6670
+
6671
+ // src/buildings-3d-occlusion.ts
6672
+ var tileflowBuildingOcclusion = {
6673
+ ground: 3,
6674
+ groundStrength: 0.16,
6675
+ wall: 0.14,
6676
+ wallRadius: 3,
6677
+ corner: 3
6678
+ };
6679
+ function buildingContactSegments(mesh) {
6680
+ const count = (mesh.indices.length - mesh.wallsFrom) / 6;
6681
+ const bytes = new Uint8Array(count * 16);
6682
+ for (let wall = 0; wall < count; wall += 1) {
6683
+ const index = mesh.wallsFrom + wall * 6;
6684
+ const start = mesh.indices[index] * tileflowBuildingVertexBytes;
6685
+ const end = mesh.indices[index + 1] * tileflowBuildingVertexBytes;
6686
+ bytes.set(mesh.vertices.subarray(start, start + 4), wall * 16);
6687
+ bytes.set(mesh.vertices.subarray(end, end + 4), wall * 16 + 4);
6688
+ bytes.set(mesh.vertices.subarray(start + 4, start + 8), wall * 16 + 8);
6689
+ bytes[wall * 16 + 12] = mesh.vertices[start + 14];
6690
+ bytes[wall * 16 + 13] = mesh.vertices[start + 27];
6691
+ }
6692
+ return bytes;
6693
+ }
6694
+ function createBuildingContactVao(gl, mesh) {
6695
+ const vao = gl.createVertexArray();
6696
+ const vbo = gl.createBuffer();
6697
+ gl.bindVertexArray(vao);
6698
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
6699
+ const segments = buildingContactSegments(mesh);
6700
+ gl.bufferData(gl.ARRAY_BUFFER, segments, gl.STATIC_DRAW);
6701
+ const offset = (first) => {
6702
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
6703
+ for (const [location, size, type, normalised, byte2] of [
6704
+ [0, 2, gl.UNSIGNED_SHORT, false, 0],
6705
+ [1, 2, gl.UNSIGNED_SHORT, false, 4],
6706
+ [2, 2, gl.UNSIGNED_SHORT, false, 8],
6707
+ [3, 1, gl.UNSIGNED_BYTE, true, 12],
6708
+ [4, 1, gl.UNSIGNED_BYTE, false, 13]
6709
+ ]) {
6710
+ gl.enableVertexAttribArray(location);
6711
+ gl.vertexAttribPointer(location, size, type, normalised, 16, first * 16 + byte2);
6712
+ gl.vertexAttribDivisor(location, 1);
6713
+ }
6714
+ };
6715
+ offset(0);
6716
+ gl.bindVertexArray(null);
6717
+ return { vao, vbo, count: segments.length / 16, offset };
6718
+ }
6719
+ var buildingContactVertex = (projected) => `#version 300 es
6720
+ ${projected}
6721
+ layout(location=0) in vec2 a_start;
6722
+ layout(location=1) in vec2 a_end;
6723
+ layout(location=2) in vec2 a_heights;
6724
+ layout(location=3) in float a_seed;
6725
+ layout(location=4) in float a_cuts;
6726
+ uniform float u_scale;
6727
+ uniform float u_metres;
6728
+ uniform float u_band;
6729
+ out vec2 v_point;
6730
+ flat out float v_length;
6731
+ flat out float v_strength;
6732
+ void main() {
6733
+ float p = clamp(u_scale * 1.25 - a_seed * 0.25, 0.0, 1.0);
6734
+ if (p <= 0.0 || a_heights.x > 5.0 || a_heights.y <= a_heights.x) {
6735
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
6736
+ return;
6737
+ }
6738
+ vec2 edge = (a_end - a_start) * u_metres;
6739
+ v_length = length(edge);
6740
+ vec2 tangent = edge / max(v_length, 0.0001);
6741
+ // A tile cut continues in its neighbour; do not round that artificial endpoint.
6742
+ float startCap = mod(a_cuts, 2.0) < 0.5 ? u_band : 0.0;
6743
+ float endCap = a_cuts < 2.0 ? u_band : 0.0;
6744
+ v_point = vec2((gl_VertexID & 1) == 0 ? -startCap : v_length + endCap,
6745
+ gl_VertexID < 2 ? -u_band : u_band);
6746
+ vec2 point = a_start + (tangent * v_point.x + vec2(-tangent.y, tangent.x) * v_point.y) / u_metres;
6747
+ gl_Position = projectTileFor3D(point, 0.0);
6748
+ v_strength = p * p * (3.0 - 2.0 * p);
6749
+ }`;
6750
+ var buildingContactFragment = `#version 300 es
6751
+ precision highp float;
6752
+ in vec2 v_point;
6753
+ flat in float v_length;
6754
+ flat in float v_strength;
6755
+ uniform float u_band;
6756
+ uniform float u_band_strength;
6757
+ out vec4 fragColor;
6758
+ void main() {
6759
+ float distance = length(v_point - vec2(clamp(v_point.x, 0.0, v_length), 0.0));
6760
+ float near = 1.0 - smoothstep(0.0, u_band, distance);
6761
+ float shade = 1.0 - u_band_strength * near * near * v_strength;
6762
+ fragColor = vec4(vec3(shade), 1.0);
6763
+ }`;
6764
+ var buildingContactCompositeFragment = `#version 300 es
6765
+ precision highp float;
6766
+ uniform highp sampler2D u_contact;
6767
+ out vec4 fragColor;
6768
+ void main() {
6769
+ float shade = texelFetch(u_contact, ivec2(gl_FragCoord.xy), 0).r;
6770
+ fragColor = vec4(vec3(shade), 1.0);
6771
+ }`;
6772
+ function createBuildingContactMask() {
6773
+ let target;
6774
+ function release(gl) {
6775
+ if (!target) return;
6776
+ gl.deleteTexture(target.texture);
6777
+ gl.deleteFramebuffer(target.framebuffer);
6778
+ target = void 0;
6779
+ }
6780
+ return {
6781
+ release,
6782
+ render(gl, draw) {
6783
+ const framebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
6784
+ const depth = gl.isEnabled(gl.DEPTH_TEST);
6785
+ const stencil = gl.isEnabled(gl.STENCIL_TEST);
6786
+ const cull = gl.isEnabled(gl.CULL_FACE);
6787
+ const scissor = gl.isEnabled(gl.SCISSOR_TEST);
6788
+ const equation = gl.getParameter(gl.BLEND_EQUATION_RGB);
6789
+ const alphaEquation = gl.getParameter(gl.BLEND_EQUATION_ALPHA);
6790
+ const clear = gl.getParameter(gl.COLOR_CLEAR_VALUE);
6791
+ try {
6792
+ const width = gl.drawingBufferWidth;
6793
+ const height = gl.drawingBufferHeight;
6794
+ if (!target || target.width !== width || target.height !== height) {
6795
+ release(gl);
6796
+ const texture = gl.createTexture();
6797
+ gl.activeTexture(gl.TEXTURE2);
6798
+ gl.bindTexture(gl.TEXTURE_2D, texture);
6799
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R8, width, height);
6800
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
6801
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
6802
+ const next = gl.createFramebuffer();
6803
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, next);
6804
+ gl.framebufferTexture2D(
6805
+ gl.DRAW_FRAMEBUFFER,
6806
+ gl.COLOR_ATTACHMENT0,
6807
+ gl.TEXTURE_2D,
6808
+ texture,
6809
+ 0
6810
+ );
6811
+ target = { texture, framebuffer: next, width, height };
6812
+ }
6813
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, target.framebuffer);
6814
+ gl.disable(gl.DEPTH_TEST);
6815
+ gl.disable(gl.STENCIL_TEST);
6816
+ gl.disable(gl.CULL_FACE);
6817
+ gl.disable(gl.SCISSOR_TEST);
6818
+ gl.clearColor(1, 1, 1, 1);
6819
+ gl.clear(gl.COLOR_BUFFER_BIT);
6820
+ gl.blendEquation(gl.MIN);
6821
+ draw();
6822
+ return target.texture;
6823
+ } finally {
6824
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffer);
6825
+ gl.blendEquationSeparate(equation, alphaEquation);
6826
+ gl.clearColor(clear[0], clear[1], clear[2], clear[3]);
6827
+ for (const [capability, enabled] of [
6828
+ [gl.DEPTH_TEST, depth],
6829
+ [gl.STENCIL_TEST, stencil],
6830
+ [gl.CULL_FACE, cull],
6831
+ [gl.SCISSOR_TEST, scissor]
6832
+ ]) {
6833
+ if (enabled) gl.enable(capability);
6834
+ else gl.disable(capability);
6835
+ }
6836
+ }
6837
+ }
6838
+ };
6839
+ }
6840
+
6841
+ // src/buildings-3d-opacity.ts
6842
+ function drawBuildingOpacity(gl, opacity, draw) {
6843
+ if (opacity <= 0) return;
6844
+ if (opacity >= 1) {
6845
+ draw();
6846
+ return;
6847
+ }
6848
+ const colourMask = gl.getParameter(gl.COLOR_WRITEMASK);
6849
+ const depthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
6850
+ const depthFunc = gl.getParameter(gl.DEPTH_FUNC);
6851
+ const blending = gl.isEnabled(gl.BLEND);
6852
+ const blendColour = gl.getParameter(gl.BLEND_COLOR);
6853
+ const blend = [gl.BLEND_SRC_RGB, gl.BLEND_DST_RGB, gl.BLEND_SRC_ALPHA, gl.BLEND_DST_ALPHA].map(
6854
+ (name) => gl.getParameter(name)
6855
+ );
6856
+ const equations = [gl.BLEND_EQUATION_RGB, gl.BLEND_EQUATION_ALPHA].map(
6857
+ (name) => gl.getParameter(name)
6858
+ );
6859
+ try {
6860
+ gl.colorMask(false, false, false, false);
6861
+ gl.depthMask(true);
6862
+ gl.depthFunc(gl.LEQUAL);
6863
+ gl.disable(gl.BLEND);
6864
+ draw();
6865
+ gl.colorMask(...colourMask);
6866
+ gl.depthMask(false);
6867
+ gl.depthFunc(gl.EQUAL);
6868
+ gl.enable(gl.BLEND);
6869
+ gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
6870
+ gl.blendColor(0, 0, 0, opacity);
6871
+ gl.blendFuncSeparate(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA, gl.ZERO, gl.ONE);
6872
+ draw();
6873
+ } finally {
6874
+ gl.colorMask(...colourMask);
6875
+ gl.depthMask(depthMask);
6876
+ gl.depthFunc(depthFunc);
6877
+ gl.blendColor(...Array.from(blendColour));
6878
+ gl.blendFuncSeparate(blend[0], blend[1], blend[2], blend[3]);
6879
+ gl.blendEquationSeparate(equations[0], equations[1]);
6880
+ if (blending) gl.enable(gl.BLEND);
6881
+ else gl.disable(gl.BLEND);
6882
+ }
6883
+ }
6884
+
6885
+ // src/scene-3d-shaders.ts
6886
+ var tileflowShadowLookup = `
6887
+ uniform highp sampler2DShadow u_shadow_map;
6888
+ uniform float u_shadow;
6889
+ uniform float u_depth_unit;
6890
+
6891
+ float visibility(vec3 c, float bias) {
6892
+ if (u_shadow <= 0.0 || c.x <= 0.0 || c.y <= 0.0 || c.x >= 1.0 || c.y >= 1.0 || c.z >= 1.0)
6893
+ return 1.0;
6894
+ float z = c.z - bias * u_depth_unit;
6895
+ // The depth texture uses LINEAR comparison; an extra multi-tap kernel would
6896
+ // spread distant texels into metres of soft shadow.
6897
+ return texture(u_shadow_map, vec3(c.xy, z));
6898
+ }`;
6899
+
6296
6900
  // src/buildings-3d-shaders.ts
6297
6901
  var tileflowBuildingAttributes = [
6298
6902
  ["a_pos", 2, "UNSIGNED_SHORT", false, 0],
@@ -6365,24 +6969,6 @@ vec3 shopOf(float use, float seed) {
6365
6969
  return vec3(0.0);
6366
6970
  }
6367
6971
  `;
6368
- var shadowLookup = `
6369
- uniform highp sampler2DShadow u_shadow_map;
6370
- uniform vec2 u_shadow_texel;
6371
- uniform float u_shadow; // how dark shadows fall, 0 for none
6372
- uniform float u_depth_unit; // the depth of a metre
6373
-
6374
- float visibility(vec3 c, float bias) {
6375
- if (u_shadow <= 0.0 || c.x <= 0.0 || c.y <= 0.0 || c.x >= 1.0 || c.y >= 1.0 || c.z >= 1.0)
6376
- return 1.0;
6377
- float z = c.z - bias * u_depth_unit;
6378
- vec2 o = u_shadow_texel * 0.75;
6379
- return 0.25 * (
6380
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, -o.y), z)) +
6381
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, -o.y), z)) +
6382
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, o.y), z)) +
6383
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, o.y), z)));
6384
- }
6385
- `;
6386
6972
  var tileflowBuildingDepthVertex = `#version 300 es
6387
6973
  ${attributes}
6388
6974
  uniform mat4 u_light_matrix;
@@ -6415,12 +7001,8 @@ uniform vec2 u_palette_themes; // the themes either side of the blend position
6415
7001
  uniform float u_palette_mix;
6416
7002
  uniform float u_variation; // how far buildings' brightness strays from their colour, either way
6417
7003
  ${growth}
6418
- uniform vec3 u_style_light_position;
6419
- uniform vec3 u_style_light_colour;
6420
- uniform float u_style_light_intensity;
6421
- uniform float u_style_gradient;
6422
- out vec3 v_style_colour;
6423
7004
  out vec3 v_colour;
7005
+ out float v_base;
6424
7006
  out vec3 v_facade; // metres along the wall, metres up as built, the building's top
6425
7007
  out vec4 v_building; // seed, tone and class group, wall, night use
6426
7008
  out vec3 v_normal;
@@ -6435,22 +7017,6 @@ vec3 paletteColour(float theme) {
6435
7017
  return texelFetch(u_palette, ivec2(int(column), int(row)), 0).rgb;
6436
7018
  }
6437
7019
 
6438
- // MapLibre GL JS 6.13 fill-extrusion lighting, adapted to this mesh's normals and heights.
6439
- // Copyright MapLibre contributors and Mapbox; BSD-3-Clause, see THIRD_PARTY_NOTICES.md.
6440
- vec3 styleColour(vec3 colour, vec3 normal, float base, float top) {
6441
- float luminance = dot(colour, vec3(0.2126, 0.7152, 0.0722));
6442
- float facing = clamp(dot(normal * vec3(1.0, -1.0, 1.0), u_style_light_position), 0.0, 1.0);
6443
- float brightness = mix(1.0 - u_style_light_intensity,
6444
- max(1.0 - luminance + u_style_light_intensity, 1.0), facing);
6445
- if (a_flags.y > 0.5) {
6446
- float gradient = clamp((a_flags.x + base) * sqrt(top / 150.0),
6447
- mix(0.7, 0.98, 1.0 - u_style_light_intensity), 1.0);
6448
- brightness *= mix(1.0, gradient, u_style_gradient);
6449
- }
6450
- return clamp((colour + 0.03) * brightness * u_style_light_colour,
6451
- 0.3 * (1.0 - u_style_light_colour), vec3(1.0));
6452
- }
6453
-
6454
7020
  void main() {
6455
7021
  float base = a_height.x * 0.1;
6456
7022
  float top = a_height.y * 0.1;
@@ -6476,7 +7042,7 @@ void main() {
6476
7042
  : u_colour;
6477
7043
  // Neighbours of one colour stay apart: each building a little lighter or darker than its colour.
6478
7044
  v_colour *= 1.0 + (a_wave.x - 0.5) * 2.0 * u_variation;
6479
- v_style_colour = styleColour(v_colour, normal, base, top);
7045
+ v_base = base;
6480
7046
  v_edge = wall > 0.5 ? vec3(a_edge.x, 0.0, 0.0) : vec3(equal(vec3(a_edge.x), vec3(0.0, 1.0, 2.0)));
6481
7047
  v_outline = a_edge.y;
6482
7048
  v_facade = vec3(a_along * 0.1, own, top);
@@ -6486,7 +7052,12 @@ void main() {
6486
7052
  var tileflowBuildingFragment = `#version 300 es
6487
7053
  precision highp float;
6488
7054
  in vec3 v_colour;
6489
- in vec3 v_style_colour;
7055
+ in float v_base;
7056
+ uniform vec3 u_style_light_position;
7057
+ uniform vec3 u_style_light_colour;
7058
+ uniform float u_style_light_intensity;
7059
+ uniform float u_style_gradient;
7060
+ uniform float u_edge_radius;
6490
7061
  uniform float u_sun_lighting;
6491
7062
  in vec3 v_facade;
6492
7063
  in vec4 v_building;
@@ -6499,6 +7070,7 @@ uniform vec3 u_sun_dir;
6499
7070
  uniform vec3 u_sun_light;
6500
7071
  uniform vec3 u_sky_light;
6501
7072
  uniform float u_occlusion;
7073
+ uniform float u_occlusion_radius;
6502
7074
  uniform float u_corner;
6503
7075
  uniform float u_night; // 0 by day, 1 at night
6504
7076
  uniform float u_windows; // whether to draw night windows
@@ -6508,13 +7080,48 @@ uniform float u_edge_width; // their width, in the drawing buffer's pixels
6508
7080
  uniform vec3 u_edge_colour; // the application's colour for them
6509
7081
  uniform float u_edge_own; // how much of them takes the application's colour, from 0 to 1
6510
7082
  ${nightLight}
6511
- ${shadowLookup}
7083
+ ${tileflowShadowLookup}
6512
7084
  out vec4 fragColor;
6513
7085
 
6514
7086
  float hash(vec2 p) {
6515
7087
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
6516
7088
  }
6517
7089
 
7090
+ // The contact shade rises gradually up the facade, in world metres. Use one bounded shade
7091
+ // for the authored gradient, ground contact and inner corners, rather than multiplying them.
7092
+ float facadeShade() {
7093
+ if (v_building.z < 0.5) return 1.0;
7094
+ float reach = min(3.0, max((v_facade.z - v_base) * 0.5, 0.1));
7095
+ float gradient = 0.1 * u_style_gradient * (1.0 - smoothstep(0.0, reach, v_facade.y - v_base));
7096
+ // Exponential attenuation leaves a soft tail above the first floor, without a band ending
7097
+ // at its radius. Height above ground also keeps raised parts from acquiring a dark lower rim.
7098
+ float up = max(v_facade.y, 0.0);
7099
+ float contact = u_occlusion * exp(-up / max(u_occlusion_radius, 0.001));
7100
+ float along = v_facade.x - v_wall.x;
7101
+ float corner = 0.0;
7102
+ if (mod(v_wall.y, 2.0) >= 1.0) corner = 1.0 - smoothstep(0.0, u_corner, along);
7103
+ if (mod(floor(v_wall.y / 2.0), 2.0) >= 1.0)
7104
+ corner = max(corner, 1.0 - smoothstep(0.0, u_corner, v_wall.z - along));
7105
+ float inner = u_occlusion * 0.7 * corner * mix(1.0, 0.5, clamp(up / 30.0, 0.0, 1.0));
7106
+ return 1.0 - max(gradient, max(contact, inner));
7107
+ }
7108
+
7109
+ // MapLibre GL JS 6.13 fill-extrusion light, evaluated per fragment for curved corner normals.
7110
+ // Copyright MapLibre contributors and Mapbox; BSD-3-Clause, see THIRD_PARTY_NOTICES.md.
7111
+ vec3 styleColour(vec3 colour, vec3 normal) {
7112
+ float luminance = dot(colour, vec3(0.2126, 0.7152, 0.0722));
7113
+ float facing = clamp(dot(normal * vec3(1.0, -1.0, 1.0), u_style_light_position), 0.0, 1.0);
7114
+ float brightness = mix(1.0 - u_style_light_intensity,
7115
+ max(1.0 - luminance + u_style_light_intensity, 1.0), facing);
7116
+ if (v_building.z > 0.5) {
7117
+ float wallLight = clamp((1.0 + v_base) * sqrt(v_facade.z / 150.0),
7118
+ mix(0.7, 0.98, 1.0 - u_style_light_intensity), 1.0);
7119
+ brightness *= mix(1.0, mix(wallLight, 1.0, normal.z), u_style_gradient);
7120
+ }
7121
+ return clamp((colour + 0.03) * brightness * u_style_light_colour,
7122
+ 0.3 * (1.0 - u_style_light_colour), vec3(1.0));
7123
+ }
7124
+
6518
7125
  // A surface's light over the light a sunlit roof takes, in the map's colours: a sunlit roof keeps
6519
7126
  // its colour, a wall turned to a low sun glows, and shade takes the sky's colour. A wall sees half
6520
7127
  // the sky; by day the ground's light makes up some of the rest, at night little does.
@@ -6548,9 +7155,17 @@ float edgeOf() {
6548
7155
 
6549
7156
  void main() {
6550
7157
  vec3 normal = normalize(v_normal);
7158
+ // A narrow lighting transition softens the roof/wall junction without moving the roof
7159
+ // or adding a bright outline. Exterior vertical corners are rounded in the mesh itself.
7160
+ if (v_building.z > 0.5 && u_edge_radius > 0.0) {
7161
+ float radius = min(u_edge_radius, max((v_facade.z - v_base) * 0.25, 0.01));
7162
+ float rim = 1.0 - smoothstep(0.0, radius, v_facade.z - v_facade.y);
7163
+ normal = normalize(mix(normal, vec3(0.0, 0.0, 1.0), rim));
7164
+ }
6551
7165
  float lit = dot(normal, u_sun_dir) > 0.0 ? visibility(v_shadow, 0.3) : 0.0;
6552
- vec3 colour = mix(v_style_colour * (1.0 - u_shadow * (1.0 - lit)),
7166
+ vec3 colour = mix(styleColour(v_colour, normal) * (1.0 - u_shadow * (1.0 - lit)),
6553
7167
  v_colour * shade(normal, lit), u_sun_lighting);
7168
+ colour *= facadeShade();
6554
7169
  // Roof outlines and outer corners catch the light, so where one building ends and the next starts
6555
7170
  // stays legible, at night most of all.
6556
7171
  float edge = u_edge > 0.0 ? edgeOf() * u_edge : 0.0;
@@ -6560,13 +7175,6 @@ void main() {
6560
7175
  float up = v_facade.y;
6561
7176
  float top = v_facade.z;
6562
7177
  float seed = v_building.x;
6563
- float foot = 1.0 - u_occlusion * pow(1.0 - clamp(up / 9.0, 0.0, 1.0), 2.0);
6564
- float along = v_facade.x - v_wall.x;
6565
- float corner = 0.0;
6566
- if (mod(v_wall.y, 2.0) >= 1.0) corner = 1.0 - smoothstep(0.0, u_corner, along);
6567
- if (mod(floor(v_wall.y / 2.0), 2.0) >= 1.0)
6568
- corner = max(corner, 1.0 - smoothstep(0.0, u_corner, v_wall.z - along));
6569
- colour *= foot * (1.0 - u_occlusion * 0.7 * corner * mix(1.0, 0.5, clamp(up / 30.0, 0.0, 1.0)));
6570
7178
  // Sheds, barns, factories and churches have no rows of windows; homes light more of theirs at
6571
7179
  // night than offices, in a warm light, and offices more of theirs in a cool one.
6572
7180
  float group = groupOf(v_building.y);
@@ -6649,7 +7257,7 @@ var tileflowBuildingGroundFragment = `#version 300 es
6649
7257
  precision highp float;
6650
7258
  in vec3 v_shadow;
6651
7259
  uniform vec3 u_ground_shadow;
6652
- ${shadowLookup}
7260
+ ${tileflowShadowLookup}
6653
7261
  out vec4 fragColor;
6654
7262
  void main() {
6655
7263
  float shaded = u_shadow * (1.0 - visibility(v_shadow, 0.15));
@@ -6681,18 +7289,6 @@ void main() {
6681
7289
  v_out = a_flags.x;
6682
7290
  v_strength = p;
6683
7291
  }`;
6684
- var tileflowBuildingBandFragment = `#version 300 es
6685
- precision highp float;
6686
- in float v_out;
6687
- in float v_strength;
6688
- in vec3 v_flood;
6689
- in vec3 v_shop;
6690
- uniform float u_band_strength;
6691
- out vec4 fragColor;
6692
- void main() {
6693
- float near = 1.0 - v_out;
6694
- fragColor = vec4(vec3(1.0 - u_band_strength * near * near * v_strength), 1.0);
6695
- }`;
6696
7292
  var tileflowBuildingFloodFragment = `#version 300 es
6697
7293
  precision highp float;
6698
7294
  in float v_out;
@@ -6707,6 +7303,50 @@ void main() {
6707
7303
  fragColor = vec4((v_flood * 0.35 * near * near + v_shop * 0.12 * close) * u_flood * v_strength, 0.0);
6708
7304
  }`;
6709
7305
 
7306
+ // src/buildings-3d-view.ts
7307
+ function buildingBoxVisible(matrix, bounds) {
7308
+ for (let axis = 0; axis < 3; axis += 1)
7309
+ for (const sign of [-1, 1]) {
7310
+ const a = matrix[3] + sign * matrix[axis];
7311
+ const b2 = matrix[7] + sign * matrix[axis + 4];
7312
+ const c = matrix[11] + sign * matrix[axis + 8];
7313
+ const d2 = matrix[15] + sign * matrix[axis + 12];
7314
+ const distance = a * bounds[a < 0 ? 0 : 3] + b2 * bounds[b2 < 0 ? 1 : 4] + c * bounds[c < 0 ? 2 : 5] + d2;
7315
+ if (!Number.isFinite(distance)) return true;
7316
+ if (distance < -1e-7) return false;
7317
+ }
7318
+ return true;
7319
+ }
7320
+ function buildingDrawRanges(mesh, visible, wallsOnly = false) {
7321
+ if (!mesh.groups)
7322
+ return [
7323
+ {
7324
+ from: wallsOnly ? mesh.wallsFrom : 0,
7325
+ count: mesh.indices.length - (wallsOnly ? mesh.wallsFrom : 0)
7326
+ }
7327
+ ];
7328
+ const groups = mesh.groups.filter(visible);
7329
+ const ranges = [];
7330
+ const append = (from, count) => {
7331
+ if (!count) return;
7332
+ const last = ranges.at(-1);
7333
+ if (last && last.from + last.count === from) last.count += count;
7334
+ else ranges.push({ from, count });
7335
+ };
7336
+ if (!wallsOnly) for (const group of groups) append(group.roofsFrom, group.roofsCount);
7337
+ for (const group of groups) append(group.wallsFrom, group.wallsCount);
7338
+ return ranges;
7339
+ }
7340
+ function buildingCornerDetail(zoom, latitude = 40.42, radius = 0.8, pitch = 0, previous) {
7341
+ if (pitch > 70) return 2;
7342
+ const metresPerPixel = 40075016686e-3 * Math.max(0.01, Math.cos(latitude * Math.PI / 180)) / (512 * 2 ** zoom);
7343
+ const pixels = radius / metresPerPixel * (1 + Math.sin(Math.max(0, pitch) * Math.PI / 180));
7344
+ if (previous === 0 && pixels < 0.66) return 0;
7345
+ if (previous === 1 && pixels >= 0.54 && pixels < 3.3) return 1;
7346
+ if (previous === 2 && pixels >= 2.7) return 2;
7347
+ return pixels < 0.6 ? 0 : pixels < 3 ? 1 : 2;
7348
+ }
7349
+
6710
7350
  // ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/browser.js
6711
7351
  var u8 = Uint8Array;
6712
7352
  var u16 = Uint16Array;
@@ -8202,7 +8842,7 @@ function createCameraTiles(options) {
8202
8842
  tiles.set(key, entry);
8203
8843
  }
8204
8844
  entry.used = now;
8205
- if (!entry.value && !entry.loading && (entry.retryAt ?? 0) <= now)
8845
+ if ((!entry.value || entry.retryAt !== void 0) && !entry.loading && (entry.retryAt ?? 0) <= now)
8206
8846
  load(source, key, entry, tileMetres);
8207
8847
  }
8208
8848
  evict();
@@ -8211,17 +8851,26 @@ function createCameraTiles(options) {
8211
8851
  function load(source, key, entry, tileMetres) {
8212
8852
  const controller = new AbortController();
8213
8853
  entry.loading = controller;
8854
+ entry.retryAt = void 0;
8214
8855
  source.readTile({ z: zoom, x: entry.x, y: entry.y }, { signal: controller.signal }).then((tile) => {
8215
- entry.loading = void 0;
8216
- if (disposed || tiles.get(key) !== entry) return;
8856
+ if (disposed || tiles.get(key) !== entry || entry.loading !== controller) return;
8217
8857
  if (!tile) {
8218
8858
  if (entry.value && entry.value !== options.empty) options.release(entry.value);
8219
8859
  entry.value = options.empty;
8860
+ entry.loading = void 0;
8861
+ options.changed();
8862
+ map.triggerRepaint();
8220
8863
  return;
8221
8864
  }
8222
- queue.push({ key, tile, metres: tileMetres });
8865
+ for (let at = queue.length - 1; at >= 0; at -= 1) {
8866
+ const queued = queue[at];
8867
+ if (queued.entry.loading !== queued.controller || queued.controller.signal.aborted)
8868
+ queue.splice(at, 1);
8869
+ }
8870
+ queue.push({ key, entry, controller, tile, metres: tileMetres });
8223
8871
  drain();
8224
8872
  }).catch(() => {
8873
+ if (disposed || entry.loading !== controller) return;
8225
8874
  entry.loading = void 0;
8226
8875
  entry.retryAt = performance.now() + 15e3;
8227
8876
  });
@@ -8229,29 +8878,46 @@ function createCameraTiles(options) {
8229
8878
  function drain() {
8230
8879
  if (building || disposed) return;
8231
8880
  building = true;
8232
- const step = () => {
8881
+ const step = async () => {
8233
8882
  const started = performance.now();
8234
8883
  let built = false;
8235
- while (queue.length && performance.now() - started < buildBudgetMs) {
8236
- const { key, tile, metres: tileMetres } = queue.shift();
8237
- const entry = tiles.get(key);
8238
- if (!entry) continue;
8239
- const previous = entry.value;
8884
+ while (!disposed && queue.length && performance.now() - started < buildBudgetMs) {
8885
+ const { key, entry, controller, tile, metres: tileMetres } = queue.shift();
8886
+ const current = () => !disposed && tiles.get(key) === entry && entry.loading === controller && !controller.signal.aborted;
8887
+ if (!current()) continue;
8888
+ let value;
8240
8889
  try {
8241
- entry.value = options.build(tile, tileMetres);
8890
+ value = await options.build(tile, tileMetres, controller.signal);
8242
8891
  } catch {
8243
- entry.value = options.empty;
8892
+ if (current()) {
8893
+ entry.loading = void 0;
8894
+ entry.retryAt = performance.now() + 15e3;
8895
+ }
8896
+ continue;
8244
8897
  }
8245
- if (previous && previous !== options.empty && previous !== entry.value)
8246
- options.release(previous);
8898
+ if (!current()) {
8899
+ if (value !== options.empty) options.release(value);
8900
+ continue;
8901
+ }
8902
+ const previous = entry.value;
8903
+ entry.value = value;
8904
+ entry.loading = void 0;
8905
+ if (previous && previous !== options.empty && previous !== value) options.release(previous);
8247
8906
  built = true;
8248
8907
  }
8249
- if (built) options.changed();
8250
- map.triggerRepaint();
8251
- if (queue.length && !disposed) cameraFrame(step);
8908
+ if (built && !disposed) {
8909
+ options.changed();
8910
+ map.triggerRepaint();
8911
+ }
8912
+ if (queue.length && !disposed)
8913
+ cameraFrame(() => {
8914
+ void step();
8915
+ });
8252
8916
  else building = false;
8253
8917
  };
8254
- cameraFrame(step);
8918
+ cameraFrame(() => {
8919
+ void step();
8920
+ });
8255
8921
  }
8256
8922
  function evict() {
8257
8923
  const keep = new Set(wanted);
@@ -8910,23 +9576,8 @@ uniform vec3 u_sun_dir;
8910
9576
  uniform vec3 u_sun_light;
8911
9577
  uniform vec3 u_sky_light;
8912
9578
  uniform float u_night;
8913
- uniform highp sampler2DShadow u_shadow_map;
8914
- uniform vec2 u_shadow_texel;
8915
- uniform float u_shadow;
8916
- uniform float u_depth_unit;
8917
9579
  out vec4 fragColor;
8918
-
8919
- float visibility(vec3 c, float bias) {
8920
- if (u_shadow <= 0.0 || c.x <= 0.0 || c.y <= 0.0 || c.x >= 1.0 || c.y >= 1.0 || c.z >= 1.0)
8921
- return 1.0;
8922
- float z = c.z - bias * u_depth_unit;
8923
- vec2 o = u_shadow_texel * 0.75;
8924
- return 0.25 * (
8925
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, -o.y), z)) +
8926
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, -o.y), z)) +
8927
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, o.y), z)) +
8928
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, o.y), z)));
8929
- }
9580
+ ${tileflowShadowLookup}
8930
9581
 
8931
9582
  // The buildings' light: a sunlit roof keeps its colour, a face turned to a low sun glows, and shade
8932
9583
  // takes the sky's colour; a face that looks down sees little sky.
@@ -9356,10 +10007,9 @@ function releaseBuffers(context, entry) {
9356
10007
 
9357
10008
  // src/scene-3d-browser.ts
9358
10009
  var tileflowSceneExtent = 8192;
9359
- var tileflowSceneShadowFloor = 3;
9360
- var shadowFade = [0.5, 7];
10010
+ var tileflowSceneShadowFloor = 15;
9361
10011
  function tileflowSceneLight(sun, shadows = true) {
9362
- const linear = (value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
10012
+ const linear = (value) => value ** 2.2;
9363
10013
  const sunLight = sun.rgb.map((channel) => linear(channel) * sun.intensity);
9364
10014
  const skyLight = sun.ambient.rgb.map((channel) => linear(channel) * sun.ambient.intensity);
9365
10015
  const up = sun.direction[2];
@@ -9374,11 +10024,7 @@ function tileflowSceneLight(sun, shadows = true) {
9374
10024
  Math.cos(azimuth) * Math.cos(floor),
9375
10025
  Math.sin(floor)
9376
10026
  ];
9377
- const low = Math.min(
9378
- 1,
9379
- Math.max(0, (sun.altitude - shadowFade[0]) / (shadowFade[1] - shadowFade[0]))
9380
- );
9381
- const strength = shadows ? Math.min(1, sun.shadowIntensity) * Math.min(1, sun.intensity * 4) * low * low * (3 - 2 * low) : 0;
10027
+ const strength = shadows && sun.intensity > 0 ? Math.min(1, sun.shadowIntensity) : 0;
9382
10028
  const night = Math.min(1, Math.max(0, (0.14 - sun.brightness) / 0.09));
9383
10029
  return {
9384
10030
  direction: sun.direction,
@@ -9394,12 +10040,12 @@ var scenes = /* @__PURE__ */ new WeakMap();
9394
10040
  function tileflowScene3D(map) {
9395
10041
  let scene = scenes.get(map);
9396
10042
  if (!scene) {
9397
- scene = createScene();
10043
+ scene = createScene(map);
9398
10044
  scenes.set(map, scene);
9399
10045
  }
9400
10046
  return scene;
9401
10047
  }
9402
- function createScene() {
10048
+ function createScene(map) {
9403
10049
  const casters = /* @__PURE__ */ new Set();
9404
10050
  let target;
9405
10051
  let key;
@@ -9439,6 +10085,7 @@ function createScene() {
9439
10085
  return target;
9440
10086
  }
9441
10087
  function prepare(context, light, metres) {
10088
+ if (light.strength <= 0) return;
9442
10089
  const shadow = ensureTarget(context);
9443
10090
  if (shadow.failed) return;
9444
10091
  const casting = [];
@@ -9446,7 +10093,6 @@ function createScene() {
9446
10093
  const bounds = caster.bounds();
9447
10094
  if (bounds) casting.push({ caster, bounds });
9448
10095
  }
9449
- if (light.strength <= 0) return;
9450
10096
  if (casting.length === 0) {
9451
10097
  frame = void 0;
9452
10098
  key = void 0;
@@ -9463,6 +10109,21 @@ function createScene() {
9463
10109
  tallest = Math.max(tallest, bounds.tallest);
9464
10110
  }
9465
10111
  const origin = [left, top];
10112
+ const scale = metres;
10113
+ const unit2 = tileflowSceneExtent * scale;
10114
+ const lightView = tileflowSunLightMatrix(
10115
+ light.shadowDirection,
10116
+ {
10117
+ min: [0, -(bottom + 1 - top) * unit2, 0],
10118
+ max: [(right + 1 - left) * unit2, 0, tallest]
10119
+ },
10120
+ {
10121
+ receivers: viewportReceivers(map, zoom, origin, scale, tallest),
10122
+ resolution: shadow.size,
10123
+ origin: [origin[0] * unit2, -origin[1] * unit2, 0],
10124
+ gridScale: unit2 * 2 ** zoom
10125
+ }
10126
+ );
9466
10127
  const next = [
9467
10128
  ...light.shadowDirection.map((value) => value.toFixed(5)),
9468
10129
  zoom,
@@ -9471,15 +10132,11 @@ function createScene() {
9471
10132
  right,
9472
10133
  bottom,
9473
10134
  tallest.toFixed(2),
10135
+ scale,
10136
+ ...lightView.matrix,
9474
10137
  ...casting.map(({ caster }) => caster.key())
9475
10138
  ].join();
9476
10139
  if (next === key && frame) return;
9477
- const scale = metres;
9478
- const unit2 = tileflowSceneExtent * scale;
9479
- const lightView = tileflowSunLightMatrix(light.shadowDirection, {
9480
- min: [0, -(bottom + 1 - top) * unit2, 0],
9481
- max: [(right + 1 - left) * unit2, 0, tallest]
9482
- });
9483
10140
  const offsetOf = (tile, tileZoom = zoom) => {
9484
10141
  const factor = 2 ** (zoom - tileZoom);
9485
10142
  return [
@@ -9542,18 +10199,22 @@ function createScene() {
9542
10199
  const on = strength > 0 && frame && target && !target.failed;
9543
10200
  context.activeTexture(context.TEXTURE0);
9544
10201
  context.bindTexture(context.TEXTURE_2D, target?.texture ?? null);
9545
- context.uniform1i(uniforms.u_shadow_map, 0);
9546
- context.uniform1f(uniforms.u_shadow, on ? strength : 0);
10202
+ context.uniform1i(uniforms.u_shadow_map ?? null, 0);
10203
+ context.uniform1f(uniforms.u_shadow ?? null, on ? strength : 0);
9547
10204
  if (!on || !frame || !target) return;
9548
- context.uniformMatrix4fv(uniforms.u_light_matrix, false, frame.light.matrix);
9549
- context.uniform2f(uniforms.u_shadow_texel, 1 / target.size, 1 / target.size);
9550
- context.uniform1f(uniforms.u_depth_unit, 1 / frame.light.depth);
9551
- context.uniform1f(uniforms.u_normal_offset, frame.light.extent / target.size * 1.2);
10205
+ context.uniformMatrix4fv(uniforms.u_light_matrix ?? null, false, frame.light.matrix);
10206
+ context.uniform1f(uniforms.u_depth_unit ?? null, 1 / frame.light.depth);
10207
+ context.uniform1f(
10208
+ uniforms.u_normal_offset ?? null,
10209
+ Math.min(0.15, Math.max(0.02, frame.light.extent / target.size * 0.5))
10210
+ );
9552
10211
  },
9553
10212
  drawsGround(caster) {
9554
10213
  let first;
9555
- for (const entry of casters)
10214
+ for (const entry of casters) {
10215
+ if (!entry.bounds()) continue;
9556
10216
  if (!first || entry.groundOrder < first.groundOrder) first = entry;
10217
+ }
9557
10218
  return first === caster;
9558
10219
  },
9559
10220
  release(context) {
@@ -9564,9 +10225,86 @@ function createScene() {
9564
10225
  frame = void 0;
9565
10226
  key = void 0;
9566
10227
  }
9567
- };
10228
+ };
10229
+ }
10230
+ function viewportReceivers(input, zoom, origin, metres, tallest) {
10231
+ const map = input;
10232
+ try {
10233
+ if (typeof map.getCanvas !== "function" || typeof map.getCenter !== "function" || typeof map.getBearing !== "function" || typeof map.getPitch !== "function" || typeof map.getZoom !== "function" || typeof map.getVerticalFieldOfView !== "function" || typeof map.project !== "function" || typeof map.unproject !== "function")
10234
+ return void 0;
10235
+ if (map.getTerrain?.()) return void 0;
10236
+ if (map.getCenterElevation && Math.abs(map.getCenterElevation()) > 1e-5) return void 0;
10237
+ const { clientWidth: width, clientHeight: height } = map.getCanvas();
10238
+ const pitch = map.getPitch();
10239
+ const fov = map.getVerticalFieldOfView();
10240
+ if (!(width > 0 && height > 0 && fov > 0 && fov < 90 && pitch >= 0 && pitch + fov / 2 < 85))
10241
+ return void 0;
10242
+ const count = 2 ** zoom;
10243
+ const unit2 = tileflowSceneExtent * metres;
10244
+ const centre = map.getCenter();
10245
+ const centreX = (centre.lng + 180) / 360 * count;
10246
+ const point = ({ lng, lat }) => {
10247
+ if (!Number.isFinite(lng) || !Number.isFinite(lat) || Math.abs(lat) >= 85.051129)
10248
+ throw new Error("Unprojected shadow receiver is outside Mercator.");
10249
+ const x3 = (lng + 180) / 360 * count;
10250
+ const wrappedX = x3 + Math.round((centreX - x3) / count) * count;
10251
+ const phi = lat * Math.PI / 180;
10252
+ const y2 = (1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2 * count;
10253
+ return [(wrappedX - origin[0]) * unit2, -(y2 - origin[1]) * unit2, 0];
10254
+ };
10255
+ const pixels = [
10256
+ [0, 0],
10257
+ [width, 0],
10258
+ [width, height],
10259
+ [0, height]
10260
+ ];
10261
+ const ground = pixels.map((pixel2) => point(map.unproject(pixel2)));
10262
+ const centrePoint = point(centre);
10263
+ const pixel = unit2 / (512 * 2 ** (map.getZoom() - zoom));
10264
+ const distance = height * pixel / (2 * Math.tan(fov * Math.PI / 360));
10265
+ const angle = pitch * Math.PI / 180;
10266
+ const bearing = map.getBearing() * Math.PI / 180;
10267
+ const elevation = distance * Math.cos(angle);
10268
+ if (!(elevation > 0.1 && Number.isFinite(elevation))) return void 0;
10269
+ const camera = [
10270
+ centrePoint[0] - distance * Math.sin(angle) * Math.sin(bearing),
10271
+ centrePoint[1] - distance * Math.sin(angle) * Math.cos(bearing),
10272
+ elevation
10273
+ ];
10274
+ const screenCentre = map.project(centre);
10275
+ const right = [Math.cos(bearing), -Math.sin(bearing), 0];
10276
+ const forward = [
10277
+ Math.sin(bearing) * Math.sin(angle),
10278
+ Math.cos(bearing) * Math.sin(angle),
10279
+ -Math.cos(angle)
10280
+ ];
10281
+ const up = [
10282
+ Math.sin(bearing) * Math.cos(angle),
10283
+ Math.cos(bearing) * Math.cos(angle),
10284
+ Math.sin(angle)
10285
+ ];
10286
+ const dot3 = (a, b2) => a.reduce((sum, value, index) => sum + value * b2[index], 0);
10287
+ for (const [index, corner] of ground.entries()) {
10288
+ const ray = corner.map((value, axis) => value - camera[axis]);
10289
+ const depth = dot3(ray, forward);
10290
+ if (!(depth > 0)) return void 0;
10291
+ const x3 = screenCentre.x + dot3(ray, right) / depth * (distance / pixel);
10292
+ const y2 = screenCentre.y - dot3(ray, up) / depth * (distance / pixel);
10293
+ if (!Number.isFinite(x3) || !Number.isFinite(y2) || Math.abs(x3 - pixels[index][0]) > Math.max(2, width * 5e-3) || Math.abs(y2 - pixels[index][1]) > Math.max(2, height * 5e-3))
10294
+ return void 0;
10295
+ }
10296
+ const fraction2 = Math.min(Math.max(0, tallest) / elevation, 1);
10297
+ return [
10298
+ ...ground,
10299
+ ...ground.map(
10300
+ (corner) => corner.map((value, index) => value + (camera[index] - value) * fraction2)
10301
+ )
10302
+ ];
10303
+ } catch {
10304
+ return void 0;
10305
+ }
9568
10306
  }
9569
- function tileflowSunLightMatrix(direction, box) {
10307
+ function tileflowSunLightMatrix(direction, box, view = {}) {
9570
10308
  const length = Math.hypot(direction[0], direction[1], direction[2]) || 1;
9571
10309
  const towards = direction.map((value) => value / length);
9572
10310
  const forward = towards.map((value) => -value);
@@ -9583,24 +10321,44 @@ function tileflowSunLightMatrix(direction, box) {
9583
10321
  const right = normalize2(cross(forward, hint));
9584
10322
  const up = cross(right, forward);
9585
10323
  const dot3 = (a, b2) => a[0] * b2[0] + a[1] * b2[1] + a[2] * b2[2];
9586
- const range = (axis) => {
10324
+ const corners = [];
10325
+ for (const x3 of [box.min[0], box.max[0]])
10326
+ for (const y2 of [box.min[1], box.max[1]])
10327
+ for (const z2 of [box.min[2], box.max[2]]) corners.push([x3, y2, z2]);
10328
+ const range = (axis, points) => {
9587
10329
  let low = Infinity;
9588
10330
  let high = -Infinity;
9589
- for (const x3 of [box.min[0], box.max[0]])
9590
- for (const y2 of [box.min[1], box.max[1]])
9591
- for (const z2 of [box.min[2], box.max[2]]) {
9592
- const value = dot3([x3, y2, z2], axis);
9593
- low = Math.min(low, value);
9594
- high = Math.max(high, value);
9595
- }
10331
+ for (const point of points) {
10332
+ const value = dot3(point, axis);
10333
+ low = Math.min(low, value);
10334
+ high = Math.max(high, value);
10335
+ }
9596
10336
  return [low, high];
9597
10337
  };
9598
10338
  const pad = ([low, high], extra) => [low - extra, high + extra];
9599
- const [u0, u1] = range(right);
9600
- const [v0, v1] = range(up);
9601
- const [a0, a1] = pad([u0, u1], (u1 - u0) * 0.01 + 2);
9602
- const [b0, b1] = pad([v0, v1], (v1 - v0) * 0.01 + 2);
9603
- const [c0, c1] = pad(range(forward), 10);
10339
+ const receivers = view.receivers?.length ? view.receivers : corners;
10340
+ const fit = (axis) => {
10341
+ const [low, high] = range(axis, receivers);
10342
+ const extra = (high - low) * 0.01 + 2;
10343
+ if (!(view.resolution && view.resolution >= 1)) return pad([low, high], extra);
10344
+ const span = high - low + extra * 2;
10345
+ const gridScale = view.gridScale && view.gridScale > 0 ? view.gridScale : 1;
10346
+ const gridSpan = span / gridScale;
10347
+ const quantum = 2 ** Math.floor(Math.log2(gridSpan)) / 16;
10348
+ const extent5 = Math.ceil(gridSpan / quantum) * quantum * gridScale;
10349
+ const texel = extent5 / view.resolution;
10350
+ const offset = dot3(view.origin ?? [0, 0, 0], axis);
10351
+ const centre = Math.round(((low + high) / 2 + offset) / texel) * texel - offset;
10352
+ return [centre - extent5 / 2, centre + extent5 / 2];
10353
+ };
10354
+ const [a0, a1] = fit(right);
10355
+ const [b0, b1] = fit(up);
10356
+ const endpoints = towards[2] > 1e-5 ? corners.map(([x3, y2, z2]) => [
10357
+ x3 - z2 * towards[0] / towards[2],
10358
+ y2 - z2 * towards[1] / towards[2],
10359
+ 0
10360
+ ]) : [];
10361
+ const [c0, c1] = pad(range(forward, [...corners, ...endpoints, ...receivers]), 10);
9604
10362
  const hu = (a1 - a0) / 2;
9605
10363
  const hv = (b1 - b0) / 2;
9606
10364
  const hd = (c1 - c0) / 2;
@@ -9616,13 +10374,19 @@ function tileflowSunLightMatrix(direction, box) {
9616
10374
  return { matrix, depth: hd * 2, extent: Math.max(hu, hv) * 2 };
9617
10375
  }
9618
10376
 
10377
+ // src/scene-3d-visibility.ts
10378
+ var tileflowSceneMinzoom = 15;
10379
+ var tileflowSceneFadeZooms = 0.25;
10380
+ function tileflowSceneOpacity(zoom, minzoom = tileflowSceneMinzoom) {
10381
+ return Math.min(1, Math.max(0, (zoom - minzoom) / tileflowSceneFadeZooms));
10382
+ }
10383
+
9619
10384
  // src/buildings-3d-browser.ts
9620
10385
  init_sun();
9621
10386
  init_theme_blend();
9622
10387
  var tileflowBuildings3DLayerId = "tileflow-buildings-3d";
9623
10388
  var preferredZoom = 15;
9624
10389
  var defaultTransition = 0;
9625
- var occlusion = { ground: 8, groundStrength: 0.42, wall: 0.36, corner: 3 };
9626
10390
  var flood = { reach: 10, colour: [1, 0.7, 0.4] };
9627
10391
  var edgeLine = { width: 1, day: 0.5 };
9628
10392
  var paletteWidth = 256;
@@ -9636,15 +10400,17 @@ var derivedParts = {
9636
10400
  };
9637
10401
  function createTileflowBuildings3D(options) {
9638
10402
  const { map } = options;
9639
- const minzoom = options.minzoom ?? 14.5;
10403
+ const minzoom = options.minzoom ?? tileflowSceneMinzoom;
9640
10404
  const maxTiles = Math.round(options.maxTiles ?? 25);
9641
10405
  if (!Number.isFinite(minzoom) || minzoom < 0 || minzoom > 24)
9642
10406
  throw new RangeError("Tileflow 3D buildings minzoom must be a zoom from 0 to 24.");
9643
10407
  if (!(maxTiles >= 1 && maxTiles <= 64))
9644
10408
  throw new RangeError("Tileflow 3D buildings maxTiles must be from 1 to 64.");
9645
- let scale = 0;
10409
+ const mesher = createBuildingMesher();
10410
+ let detail = buildingCornerDetail(map.getZoom());
10411
+ let scale = 1;
9646
10412
  let opacity = 1;
9647
- let target = 0;
10413
+ let target = 1;
9648
10414
  let blank;
9649
10415
  let motion;
9650
10416
  let gl;
@@ -9652,6 +10418,7 @@ function createTileflowBuildings3D(options) {
9652
10418
  let variant;
9653
10419
  let linking;
9654
10420
  let ground;
10421
+ const contactMask = createBuildingContactMask();
9655
10422
  let disposed = false;
9656
10423
  const scene = tileflowScene3D(map);
9657
10424
  let generation = 0;
@@ -9705,6 +10472,7 @@ function createTileflowBuildings3D(options) {
9705
10472
  context.deleteVertexArray(ground.vao);
9706
10473
  context.deleteBuffer(ground.vbo);
9707
10474
  }
10475
+ contactMask.release(context);
9708
10476
  if (blank) context.deleteTexture(blank);
9709
10477
  blank = void 0;
9710
10478
  programs = {};
@@ -9740,12 +10508,15 @@ function createTileflowBuildings3D(options) {
9740
10508
  } catch {
9741
10509
  return;
9742
10510
  }
9743
- if (visibleScale() <= 0) return;
9744
- upload(context);
10511
+ if (target > 0 || scale > 0) upload(context);
10512
+ if (visibleScale() <= 0) {
10513
+ contactMask.release(context);
10514
+ return;
10515
+ }
9745
10516
  draw(context, args);
9746
10517
  }
9747
10518
  };
9748
- const makeCamera = (layers, source) => createCameraTiles({
10519
+ const makeCamera = (layers, source, edgeRadius = 0) => createCameraTiles({
9749
10520
  map,
9750
10521
  source,
9751
10522
  layers: [layers.building, ...layers.places ? [layers.places] : []],
@@ -9754,16 +10525,22 @@ function createTileflowBuildings3D(options) {
9754
10525
  maxTiles,
9755
10526
  ...options.transformRequest ? { transformRequest: options.transformRequest } : {},
9756
10527
  active: () => target > 0 || scale > 0,
9757
- build(tile, tileMetres) {
10528
+ async build(tile, tileMetres, signal) {
9758
10529
  const decoded = tile.layers[layers.building];
9759
10530
  if (!decoded) return emptyTile;
9760
10531
  const places = layers.places ? tile.layers[layers.places] : void 0;
9761
10532
  const masks = models.masksIn(tile, tileflowBuildingExtent);
9762
- const mesh = meshTileflowBuildings(decoded, {
9763
- tileMetres,
9764
- ...places ? { places } : {},
9765
- ...masks ? { masks } : {}
9766
- });
10533
+ const mesh = await mesher.build(
10534
+ decoded,
10535
+ {
10536
+ tileMetres,
10537
+ edgeRadius: detail === 0 ? 0 : edgeRadius,
10538
+ cornerAngle: detail === 1 ? Math.PI / 3 : Math.PI / 6,
10539
+ ...places ? { places } : {},
10540
+ ...masks ? { masks } : {}
10541
+ },
10542
+ signal
10543
+ );
9767
10544
  return mesh ? { mesh } : emptyTile;
9768
10545
  },
9769
10546
  empty: emptyTile,
@@ -9776,26 +10553,45 @@ function createTileflowBuildings3D(options) {
9776
10553
  });
9777
10554
  let cameraKey = JSON.stringify([
9778
10555
  tileflowDefaultBuildings3DDesign.source,
9779
- tileflowDefaultBuildings3DDesign.layers
10556
+ tileflowDefaultBuildings3DDesign.layers,
10557
+ 0
9780
10558
  ]);
9781
10559
  let camera = makeCamera(
9782
10560
  tileflowDefaultBuildings3DDesign.layers,
9783
10561
  tileflowDefaultBuildings3DDesign.source
9784
10562
  );
9785
10563
  function ensureCamera() {
9786
- const { source, layers } = design();
9787
- const key = JSON.stringify([source, layers]);
9788
- if (key === cameraKey) return;
10564
+ const { source, layers, edgeRadius = 0 } = design();
10565
+ const key = JSON.stringify([source, layers, edgeRadius]);
10566
+ const nextDetail = buildingCornerDetail(
10567
+ map.getZoom(),
10568
+ map.getCenter().lat,
10569
+ edgeRadius,
10570
+ map.getPitch(),
10571
+ detail
10572
+ );
10573
+ if (key === cameraKey) {
10574
+ if (nextDetail !== detail && (target > 0 || scale > 0)) {
10575
+ detail = nextDetail;
10576
+ if (edgeRadius > 0) camera.refresh(() => true);
10577
+ }
10578
+ return;
10579
+ }
10580
+ detail = nextDetail;
9789
10581
  for (const entry of camera.all()) if (entry.value && gl) release(gl, entry.value);
9790
10582
  camera.dispose();
9791
10583
  cameraKey = key;
9792
- camera = makeCamera(layers, source);
10584
+ camera = makeCamera(layers, source, edgeRadius);
9793
10585
  generation += 1;
9794
10586
  if (target > 0) camera.update();
9795
10587
  }
9796
10588
  function release(context, tile) {
9797
10589
  if (tile.gl) {
9798
10590
  context.deleteVertexArray(tile.gl.vao);
10591
+ if (tile.gl.contact) {
10592
+ context.deleteVertexArray(tile.gl.contact.vao);
10593
+ context.deleteBuffer(tile.gl.contact.vbo);
10594
+ }
9799
10595
  context.deleteBuffer(tile.gl.vbo);
9800
10596
  context.deleteBuffer(tile.gl.ibo);
9801
10597
  tile.gl = void 0;
@@ -9806,7 +10602,9 @@ function createTileflowBuildings3D(options) {
9806
10602
  }
9807
10603
  }
9808
10604
  function upload(context) {
10605
+ const modelKey = models.key();
9809
10606
  models.upload(context);
10607
+ if (modelKey !== models.key()) map.triggerRepaint();
9810
10608
  let budget = 2;
9811
10609
  for (const { value: entry } of camera.ready()) {
9812
10610
  if (entry.gl) continue;
@@ -9839,14 +10637,18 @@ function createTileflowBuildings3D(options) {
9839
10637
  context.bindVertexArray(null);
9840
10638
  entry.gl = { vao, vbo, ibo };
9841
10639
  generation += 1;
10640
+ map.triggerRepaint();
9842
10641
  }
9843
10642
  }
10643
+ let fallbackStyle;
10644
+ let fallbackSun;
10645
+ function standaloneStyle() {
10646
+ return fallbackStyle ??= map.getStyle() ?? {};
10647
+ }
9844
10648
  let designs;
9845
10649
  function designsOnScreen() {
9846
10650
  const read = options.controller?.getThemeMetadata?.(tileflowBuildingsMetadataKey) ?? {
9847
- values: [
9848
- map.getStyle()?.metadata?.[tileflowBuildingsMetadataKey]
9849
- ],
10651
+ values: [standaloneStyle().metadata?.[tileflowBuildingsMetadataKey]],
9850
10652
  position: 0
9851
10653
  };
9852
10654
  if (!designs || designs.values.length !== read.values.length || designs.values.some((value, index) => value !== read.values[index])) {
@@ -9970,14 +10772,17 @@ function createTileflowBuildings3D(options) {
9970
10772
  return [...rgbOf2(typeof text2 === "string" ? text2 : "#d8d0c0") ?? [0.85, 0.82, 0.76]];
9971
10773
  }
9972
10774
  function sunNow() {
9973
- return options.controller?.getSun?.() ?? resolveTileflowSun(tileflowDefaultSunPreset);
10775
+ return options.controller?.getSun?.() ?? (fallbackSun ??= resolveTileflowSun(tileflowStyleSunPreset(standaloneStyle())));
9974
10776
  }
9975
10777
  function lighting() {
9976
- return tileflowSceneLight(sunNow(), design().shadows);
10778
+ const light = tileflowSceneLight(sunNow(), design().shadows);
10779
+ return { ...light, strength: light.strength * visibleOpacity() };
9977
10780
  }
9978
10781
  function visibleScale() {
9979
- const fade = Math.min(1, Math.max(0, (map.getZoom() - (minzoom - 0.5)) / 0.5));
9980
- return scale * fade;
10782
+ return visibleOpacity() > 0 ? scale : 0;
10783
+ }
10784
+ function visibleOpacity() {
10785
+ return opacity * tileflowSceneOpacity(map.getZoom(), minzoom);
9981
10786
  }
9982
10787
  function drawn() {
9983
10788
  return camera.ready().filter((entry) => entry.value.gl);
@@ -10013,19 +10818,45 @@ function createTileflowBuildings3D(options) {
10013
10818
  const program = programs.depth;
10014
10819
  if (!program) return;
10015
10820
  context.useProgram(program.program);
10016
- context.uniform1f(program.uniforms.u_preview_opacity ?? null, opacity);
10017
10821
  context.uniform1f(program.uniforms.u_scale, visibleScale());
10018
10822
  context.uniform1f(program.uniforms.u_metres, frame.metres);
10019
10823
  context.uniformMatrix4fv(program.uniforms.u_light_matrix, false, frame.light.matrix);
10020
10824
  for (const { x: x3, y: y2, value } of drawn()) {
10021
- context.uniform2f(program.uniforms.u_offset, ...frame.offsetOf({ x: x3, y: y2 }, camera.zoom));
10825
+ const offset = frame.offsetOf({ x: x3, y: y2 }, camera.zoom);
10826
+ if (!buildingBoxVisible(frame.light.matrix, [
10827
+ (offset[0] - 1) * frame.metres,
10828
+ -(offset[1] + tileflowBuildingExtent + 1) * frame.metres,
10829
+ 0,
10830
+ (offset[0] + tileflowBuildingExtent + 1) * frame.metres,
10831
+ -(offset[1] - 1) * frame.metres,
10832
+ value.mesh.top + 0.1
10833
+ ]))
10834
+ continue;
10835
+ const ranges = buildingDrawRanges(
10836
+ value.mesh,
10837
+ ({ bounds: [left, top, right, bottom, height] }) => buildingBoxVisible(frame.light.matrix, [
10838
+ (left + offset[0]) * frame.metres,
10839
+ -(bottom + offset[1]) * frame.metres,
10840
+ 0,
10841
+ (right + offset[0]) * frame.metres,
10842
+ -(top + offset[1]) * frame.metres,
10843
+ height
10844
+ ])
10845
+ );
10846
+ if (!ranges.length) continue;
10847
+ context.uniform2f(program.uniforms.u_offset, ...offset);
10022
10848
  context.bindVertexArray(value.gl.vao);
10023
- context.drawElements(context.TRIANGLES, value.mesh.indices.length, context.UNSIGNED_INT, 0);
10849
+ for (const range of ranges)
10850
+ context.drawElements(
10851
+ context.TRIANGLES,
10852
+ range.count,
10853
+ context.UNSIGNED_INT,
10854
+ range.from * 4
10855
+ );
10024
10856
  }
10025
10857
  const modelDepth = programs.modelDepth;
10026
10858
  if (!modelDepth) return;
10027
10859
  context.useProgram(modelDepth.program);
10028
- context.uniform1f(modelDepth.uniforms.u_preview_opacity ?? null, opacity);
10029
10860
  context.uniformMatrix4fv(modelDepth.uniforms.u_light_matrix, false, frame.light.matrix);
10030
10861
  context.uniform1f(modelDepth.uniforms.u_scale, visibleScale());
10031
10862
  context.uniform1f(modelDepth.uniforms.u_metres, frame.metres);
@@ -10102,6 +10933,46 @@ function createTileflowBuildings3D(options) {
10102
10933
  context.uniform1f(uniforms.u_projection_transition, data.projectionTransition);
10103
10934
  context.uniformMatrix4fv(uniforms.u_projection_fallback_matrix, false, data.fallbackMatrix);
10104
10935
  }
10936
+ function viewRanges(args, tile, margin = 0, wallsOnly = false) {
10937
+ const data = args.getProjectionData({
10938
+ tileID: { canonical: { x: tile.x, y: tile.y, z: camera.zoom }, wrap: 0 }
10939
+ });
10940
+ const mercator = args.shaderData.variantName === "mercator";
10941
+ const reach = margin / camera.metres + 1;
10942
+ if (mercator && !buildingBoxVisible(data.mainMatrix, [
10943
+ -reach,
10944
+ -reach,
10945
+ 0,
10946
+ tileflowBuildingExtent + reach,
10947
+ tileflowBuildingExtent + reach,
10948
+ tile.value.mesh.top + 0.1
10949
+ ]))
10950
+ return [];
10951
+ return buildingDrawRanges(
10952
+ tile.value.mesh,
10953
+ ({ bounds: [left, top, right, bottom, height] }) => !mercator || buildingBoxVisible(data.mainMatrix, [
10954
+ left - reach,
10955
+ top - reach,
10956
+ 0,
10957
+ right + reach,
10958
+ bottom + reach,
10959
+ height
10960
+ ]),
10961
+ wallsOnly
10962
+ );
10963
+ }
10964
+ function groundVisible(args, x3, y2) {
10965
+ if (args.shaderData.variantName !== "mercator") return true;
10966
+ const data = args.getProjectionData({ tileID: { canonical: { x: x3, y: y2, z: camera.zoom }, wrap: 0 } });
10967
+ return buildingBoxVisible(data.mainMatrix, [
10968
+ 0,
10969
+ 0,
10970
+ 0,
10971
+ tileflowBuildingExtent,
10972
+ tileflowBuildingExtent,
10973
+ 0
10974
+ ]);
10975
+ }
10105
10976
  function pixelRatio2(context) {
10106
10977
  const canvas = context.canvas;
10107
10978
  const width = canvas.clientWidth;
@@ -10117,90 +10988,105 @@ function createTileflowBuildings3D(options) {
10117
10988
  const nightLight2 = mixed2((entry) => entry.nightLight);
10118
10989
  const edges = mixed2((entry) => entry.edges);
10119
10990
  const { occlusion: occlusionOn, windows: windowsOn } = design();
10120
- const buildings = programs.buildings;
10121
- const u = buildings.uniforms;
10122
- context.useProgram(buildings.program);
10123
- context.uniform1f(buildings.uniforms.u_preview_opacity ?? null, opacity);
10124
- context.uniform1f(u.u_scale, shown);
10125
- context.uniform1f(u.u_metres, placed2(0, 0).metres);
10126
- context.uniform3fv(u.u_colour, themeColour());
10127
- context.uniform3fv(u.u_sun_dir, [...light.direction]);
10128
- context.uniform3fv(u.u_shadow_dir, light.shadowDirection);
10129
- context.uniform3fv(u.u_sun_light, light.sunLight);
10130
- context.uniform3fv(u.u_sky_light, light.skyLight);
10131
- context.uniform1f(u.u_occlusion, occlusionOn ? occlusion.wall : 0);
10132
- context.uniform1f(u.u_corner, occlusion.corner);
10133
- context.uniform1f(u.u_night, light.night);
10134
- context.uniform1f(u.u_windows, windowsOn ? 1 : 0);
10135
- context.uniform1f(u.u_flood, light.night * nightLight2);
10136
- context.uniform3fv(u.u_flood_colour, flood.colour);
10137
- context.uniform1f(
10138
- u.u_variation,
10139
- mixed2((entry) => entry.colorVariation)
10140
- );
10141
- const styleLight = tileflowBuildingStyleLight(
10142
- map.getLight?.() ?? fallbackStyleLight,
10143
- map.getBearing()
10144
- );
10145
- context.uniform3fv(u.u_style_light_position, styleLight.position);
10146
- context.uniform3fv(u.u_style_light_colour, styleLight.colour);
10147
- context.uniform1f(u.u_style_light_intensity, styleLight.intensity);
10148
- context.uniform1f(
10149
- u.u_style_gradient,
10150
- mixed2((entry) => entry.verticalGradient ? 1 : 0)
10151
- );
10152
- context.uniform1f(
10153
- u.u_sun_lighting,
10154
- mixed2((entry) => entry.lighting === "sun" ? 1 : 0)
10155
- );
10156
- const edgeColour = edgeColourNow();
10157
- const lightEdge = edges * (edgeLine.day + (1 - edgeLine.day) * light.night);
10158
- context.uniform1f(u.u_edge, lightEdge + (edges - lightEdge) * edgeColour.share);
10159
- context.uniform3fv(u.u_edge_colour, edgeColour.rgb);
10160
- context.uniform1f(u.u_edge_own, edgeColour.share);
10161
- context.uniform1f(u.u_edge_width, edgeLine.width * pixelRatio2(context));
10162
- scene.bindShadow(context, u, light.strength);
10163
- for (const { x: x3, y: y2, value: entry } of entries) {
10164
- project(context, buildings, args, x3, y2);
10165
- context.uniform2f(u.u_offset, ...placed2(x3, y2).offset);
10166
- const palette = themes ? paletteOf(context, entry, themes.themes) : void 0;
10167
- context.uniform1f(u.u_palette_on, palette ? 1 : 0);
10168
- context.activeTexture(context.TEXTURE1);
10169
- context.bindTexture(context.TEXTURE_2D, palette?.texture ?? blank ?? null);
10170
- context.uniform1i(u.u_palette, 1);
10171
- if (palette && themes) {
10172
- context.uniform2f(u.u_palette_rows, palette.rows, paletteWidth);
10173
- context.uniform2f(u.u_palette_themes, themes.lower, themes.upper);
10174
- context.uniform1f(u.u_palette_mix, themes.mix);
10175
- }
10176
- context.bindVertexArray(entry.gl.vao);
10177
- context.drawElements(context.TRIANGLES, entry.mesh.indices.length, context.UNSIGNED_INT, 0);
10178
- }
10179
- const model = programs.model;
10180
- if (model && standing.length > 0) {
10181
- const m = model.uniforms;
10182
- context.useProgram(model.program);
10183
- context.uniform1f(model.uniforms.u_preview_opacity ?? null, opacity);
10184
- context.disable(context.CULL_FACE);
10185
- context.uniform1f(m.u_scale, shown);
10186
- context.uniform3fv(m.u_sun_dir, [...light.direction]);
10187
- context.uniform3fv(m.u_shadow_dir, light.shadowDirection);
10188
- context.uniform3fv(m.u_sun_light, light.sunLight);
10189
- context.uniform3fv(m.u_sky_light, light.skyLight);
10190
- context.uniform1f(m.u_night, light.night);
10191
- context.uniform3fv(m.u_part_colours, partColoursNow());
10192
- scene.bindShadow(context, m, light.strength);
10193
- const centre = map.getCenter();
10194
- for (const entry of standing) {
10195
- const stand = standOf(entry);
10196
- project(context, model, args, stand.tileX, stand.tileY);
10197
- const at = placed2(stand.tileX, stand.tileY);
10198
- context.uniform2f(m.u_offset, ...at.offset);
10199
- context.uniform1f(m.u_metres, at.metres);
10200
- setPlacement(context, model, entry, stand);
10201
- drawModel(context, model, entry, models.levelOf(entry, centre));
10991
+ context.enable(context.DEPTH_TEST);
10992
+ context.depthFunc(context.LEQUAL);
10993
+ context.depthMask(true);
10994
+ context.disable(context.BLEND);
10995
+ drawBuildingOpacity(context, visibleOpacity(), () => {
10996
+ const buildings = programs.buildings;
10997
+ const u = buildings.uniforms;
10998
+ context.useProgram(buildings.program);
10999
+ context.uniform1f(u.u_scale, shown);
11000
+ context.uniform1f(u.u_metres, placed2(0, 0).metres);
11001
+ context.uniform3fv(u.u_colour, themeColour());
11002
+ context.uniform3fv(u.u_sun_dir, [...light.direction]);
11003
+ context.uniform3fv(u.u_shadow_dir, light.shadowDirection);
11004
+ context.uniform3fv(u.u_sun_light, light.sunLight);
11005
+ context.uniform3fv(u.u_sky_light, light.skyLight);
11006
+ context.uniform1f(u.u_occlusion, occlusionOn ? tileflowBuildingOcclusion.wall : 0);
11007
+ context.uniform1f(u.u_occlusion_radius, tileflowBuildingOcclusion.wallRadius);
11008
+ context.uniform1f(u.u_corner, tileflowBuildingOcclusion.corner);
11009
+ context.uniform1f(u.u_night, light.night);
11010
+ context.uniform1f(u.u_windows, windowsOn ? 1 : 0);
11011
+ context.uniform1f(u.u_flood, light.night * nightLight2);
11012
+ context.uniform3fv(u.u_flood_colour, flood.colour);
11013
+ context.uniform1f(
11014
+ u.u_variation,
11015
+ mixed2((entry) => entry.colorVariation)
11016
+ );
11017
+ const styleLight = tileflowBuildingStyleLight(
11018
+ map.getLight?.() ?? fallbackStyleLight,
11019
+ map.getBearing()
11020
+ );
11021
+ context.uniform3fv(u.u_style_light_position, styleLight.position);
11022
+ context.uniform3fv(u.u_style_light_colour, styleLight.colour);
11023
+ context.uniform1f(u.u_style_light_intensity, styleLight.intensity);
11024
+ context.uniform1f(u.u_edge_radius, design().edgeRadius ?? 0);
11025
+ context.uniform1f(
11026
+ u.u_style_gradient,
11027
+ mixed2((entry) => entry.verticalGradient ? 1 : 0)
11028
+ );
11029
+ context.uniform1f(
11030
+ u.u_sun_lighting,
11031
+ mixed2((entry) => entry.lighting === "sun" ? 1 : 0)
11032
+ );
11033
+ const edgeColour = edgeColourNow();
11034
+ const lightEdge = edges * (edgeLine.day + (1 - edgeLine.day) * light.night);
11035
+ context.uniform1f(u.u_edge, lightEdge + (edges - lightEdge) * edgeColour.share);
11036
+ context.uniform3fv(u.u_edge_colour, edgeColour.rgb);
11037
+ context.uniform1f(u.u_edge_own, edgeColour.share);
11038
+ context.uniform1f(u.u_edge_width, edgeLine.width * pixelRatio2(context));
11039
+ scene.bindShadow(context, u, light.strength);
11040
+ for (const tile of entries) {
11041
+ const { x: x3, y: y2, value: entry } = tile;
11042
+ const ranges = viewRanges(args, tile);
11043
+ if (!ranges.length) continue;
11044
+ project(context, buildings, args, x3, y2);
11045
+ context.uniform2f(u.u_offset, ...placed2(x3, y2).offset);
11046
+ const palette = themes ? paletteOf(context, entry, themes.themes) : void 0;
11047
+ context.uniform1f(u.u_palette_on, palette ? 1 : 0);
11048
+ context.activeTexture(context.TEXTURE1);
11049
+ context.bindTexture(context.TEXTURE_2D, palette?.texture ?? blank ?? null);
11050
+ context.uniform1i(u.u_palette, 1);
11051
+ if (palette && themes) {
11052
+ context.uniform2f(u.u_palette_rows, palette.rows, paletteWidth);
11053
+ context.uniform2f(u.u_palette_themes, themes.lower, themes.upper);
11054
+ context.uniform1f(u.u_palette_mix, themes.mix);
11055
+ }
11056
+ context.bindVertexArray(entry.gl.vao);
11057
+ for (const range of ranges)
11058
+ context.drawElements(
11059
+ context.TRIANGLES,
11060
+ range.count,
11061
+ context.UNSIGNED_INT,
11062
+ range.from * 4
11063
+ );
10202
11064
  }
10203
- }
11065
+ const model = programs.model;
11066
+ if (model && standing.length > 0) {
11067
+ const m = model.uniforms;
11068
+ context.useProgram(model.program);
11069
+ context.disable(context.CULL_FACE);
11070
+ context.uniform1f(m.u_scale, shown);
11071
+ context.uniform3fv(m.u_sun_dir, [...light.direction]);
11072
+ context.uniform3fv(m.u_shadow_dir, light.shadowDirection);
11073
+ context.uniform3fv(m.u_sun_light, light.sunLight);
11074
+ context.uniform3fv(m.u_sky_light, light.skyLight);
11075
+ context.uniform1f(m.u_night, light.night);
11076
+ context.uniform3fv(m.u_part_colours, partColoursNow());
11077
+ scene.bindShadow(context, m, light.strength);
11078
+ const centre = map.getCenter();
11079
+ for (const entry of standing) {
11080
+ const stand = standOf(entry);
11081
+ project(context, model, args, stand.tileX, stand.tileY);
11082
+ const at = placed2(stand.tileX, stand.tileY);
11083
+ context.uniform2f(m.u_offset, ...at.offset);
11084
+ context.uniform1f(m.u_metres, at.metres);
11085
+ setPlacement(context, model, entry, stand);
11086
+ drawModel(context, model, entry, models.levelOf(entry, centre));
11087
+ }
11088
+ }
11089
+ });
10204
11090
  context.depthMask(false);
10205
11091
  context.enable(context.BLEND);
10206
11092
  context.blendFuncSeparate(context.DST_COLOR, context.ZERO, context.ZERO, context.ONE);
@@ -10208,7 +11094,6 @@ function createTileflowBuildings3D(options) {
10208
11094
  if (light.strength > 0 && frame && frame.zoom === camera.zoom && ground && scene.drawsGround(caster)) {
10209
11095
  const program = programs.ground;
10210
11096
  context.useProgram(program.program);
10211
- context.uniform1f(program.uniforms.u_preview_opacity ?? null, opacity);
10212
11097
  context.uniform1f(program.uniforms.u_metres, frame.metres);
10213
11098
  context.uniform3fv(program.uniforms.u_ground_shadow, light.groundShadow);
10214
11099
  scene.bindShadow(context, program.uniforms, light.strength);
@@ -10223,45 +11108,80 @@ function createTileflowBuildings3D(options) {
10223
11108
  const [left, top, right, bottom] = frame.tiles;
10224
11109
  for (let x3 = left - margin; x3 <= right + margin; x3 += 1)
10225
11110
  for (let y2 = top - margin; y2 <= bottom + margin; y2 += 1) {
11111
+ if (!groundVisible(args, x3, y2)) continue;
10226
11112
  project(context, program, args, x3, y2);
10227
11113
  context.uniform2f(program.uniforms.u_offset, ...frame.offsetOf({ x: x3, y: y2 }));
10228
11114
  context.drawArrays(context.TRIANGLE_STRIP, 0, 4);
10229
11115
  }
10230
11116
  }
10231
11117
  const walls = (program) => {
10232
- for (const { x: x3, y: y2, value } of entries) {
11118
+ for (const tile of entries) {
11119
+ const { x: x3, y: y2, value } = tile;
10233
11120
  const mesh = value.mesh;
10234
11121
  if (mesh.indices.length <= mesh.wallsFrom) continue;
10235
11122
  project(context, program, args, x3, y2);
10236
11123
  context.uniform2f(program.uniforms.u_offset, ...camera.offsetOf({ x: x3, y: y2 }));
10237
11124
  context.bindVertexArray(value.gl.vao);
10238
- context.drawElements(
10239
- context.TRIANGLES,
10240
- mesh.indices.length - mesh.wallsFrom,
10241
- context.UNSIGNED_INT,
10242
- mesh.wallsFrom * 4
10243
- );
11125
+ for (const range of viewRanges(args, tile, flood.reach, true))
11126
+ context.drawElements(
11127
+ context.TRIANGLES,
11128
+ range.count,
11129
+ context.UNSIGNED_INT,
11130
+ range.from * 4
11131
+ );
10244
11132
  }
10245
11133
  };
10246
- if (occlusionOn) {
10247
- const program = programs.band;
10248
- context.useProgram(program.program);
10249
- context.uniform1f(program.uniforms.u_preview_opacity ?? null, opacity);
10250
- context.uniform1f(program.uniforms.u_scale, shown);
10251
- context.uniform1f(program.uniforms.u_metres, camera.metres);
10252
- context.uniform1f(program.uniforms.u_band, occlusion.ground);
10253
- context.uniform1f(program.uniforms.u_band_strength, occlusion.groundStrength);
10254
- walls(program);
11134
+ if (occlusionOn && entries.length > 0 && ground) {
11135
+ const program = programs.contact;
11136
+ const texture = contactMask.render(context, () => {
11137
+ context.useProgram(program.program);
11138
+ context.uniform1f(program.uniforms.u_scale, shown);
11139
+ context.uniform1f(program.uniforms.u_metres, camera.metres);
11140
+ context.uniform1f(program.uniforms.u_band, tileflowBuildingOcclusion.ground);
11141
+ context.uniform1f(
11142
+ program.uniforms.u_band_strength,
11143
+ tileflowBuildingOcclusion.groundStrength * visibleOpacity()
11144
+ );
11145
+ for (const tile of entries) {
11146
+ const { x: x3, y: y2, value } = tile;
11147
+ const ranges = viewRanges(args, tile, tileflowBuildingOcclusion.ground, true);
11148
+ if (!ranges.length) continue;
11149
+ const buffers = value.gl;
11150
+ buffers.contact ??= createBuildingContactVao(context, value.mesh);
11151
+ project(context, program, args, x3, y2);
11152
+ context.bindVertexArray(buffers.contact.vao);
11153
+ for (const range of ranges) {
11154
+ buffers.contact.offset((range.from - value.mesh.wallsFrom) / 6);
11155
+ context.drawArraysInstanced(context.TRIANGLE_STRIP, 0, 4, range.count / 6);
11156
+ }
11157
+ }
11158
+ });
11159
+ const composite = programs.contactComposite;
11160
+ context.useProgram(composite.program);
11161
+ context.activeTexture(context.TEXTURE2);
11162
+ context.bindTexture(context.TEXTURE_2D, texture);
11163
+ context.uniform1i(composite.uniforms.u_contact, 2);
11164
+ context.bindVertexArray(ground.vao);
11165
+ const xs = entries.map(({ x: x3 }) => x3);
11166
+ const ys = entries.map(({ y: y2 }) => y2);
11167
+ for (let x3 = Math.min(...xs) - 1; x3 <= Math.max(...xs) + 1; x3 += 1)
11168
+ for (let y2 = Math.min(...ys) - 1; y2 <= Math.max(...ys) + 1; y2 += 1) {
11169
+ if (!groundVisible(args, x3, y2)) continue;
11170
+ project(context, composite, args, x3, y2);
11171
+ context.drawArrays(context.TRIANGLE_STRIP, 0, 4);
11172
+ }
11173
+ context.bindTexture(context.TEXTURE_2D, null);
11174
+ } else {
11175
+ contactMask.release(context);
10255
11176
  }
10256
11177
  if (nightLight2 > 0 && light.night > 0.01) {
10257
11178
  const program = programs.flood;
10258
11179
  context.useProgram(program.program);
10259
- context.uniform1f(program.uniforms.u_preview_opacity ?? null, opacity);
10260
11180
  context.blendFuncSeparate(context.ONE, context.ONE, context.ZERO, context.ONE);
10261
11181
  context.uniform1f(program.uniforms.u_scale, shown);
10262
11182
  context.uniform1f(program.uniforms.u_metres, camera.metres);
10263
11183
  context.uniform1f(program.uniforms.u_band, flood.reach);
10264
- context.uniform1f(program.uniforms.u_flood, light.night * nightLight2);
11184
+ context.uniform1f(program.uniforms.u_flood, light.night * nightLight2 * visibleOpacity());
10265
11185
  context.uniform3fv(program.uniforms.u_flood_colour, flood.colour);
10266
11186
  walls(program);
10267
11187
  }
@@ -10275,7 +11195,8 @@ ${shaderData.define}`;
10275
11195
  const sources = {
10276
11196
  buildings: [tileflowBuildingVertex(projected), tileflowBuildingFragment],
10277
11197
  ground: [tileflowBuildingGroundVertex(projected), tileflowBuildingGroundFragment],
10278
- band: [tileflowBuildingBandVertex(projected), tileflowBuildingBandFragment],
11198
+ contact: [buildingContactVertex(projected), buildingContactFragment],
11199
+ contactComposite: [tileflowBuildingGroundVertex(projected), buildingContactCompositeFragment],
10279
11200
  flood: [tileflowBuildingBandVertex(projected), tileflowBuildingFloodFragment]
10280
11201
  };
10281
11202
  sources.model = [tileflowModelVertex(projected), tileflowModelFragment];
@@ -10292,11 +11213,7 @@ ${shaderData.define}`;
10292
11213
  };
10293
11214
  const program = context.createProgram();
10294
11215
  const vertex = shader(context.VERTEX_SHADER, vertexText);
10295
- const fadeFragment = name.toLowerCase().includes("depth") ? fragmentText : fragmentText.replace(
10296
- "void main() {",
10297
- "uniform float u_preview_opacity;\nvoid main() {\n if (u_preview_opacity < 1.0 && fract(sin(dot(floor(gl_FragCoord.xy), vec2(12.9898, 78.233))) * 43758.5453) >= u_preview_opacity) discard;"
10298
- );
10299
- const fragment = shader(context.FRAGMENT_SHADER, fadeFragment);
11216
+ const fragment = shader(context.FRAGMENT_SHADER, fragmentText);
10300
11217
  context.attachShader(program, vertex);
10301
11218
  context.attachShader(program, fragment);
10302
11219
  const bound = name === "model" || name === "modelDepth" ? tileflowModelAttributes : tileflowBuildingAttributes;
@@ -10380,13 +11297,15 @@ ${shaderData.define}`;
10380
11297
  }
10381
11298
  let fallbackStyleLight;
10382
11299
  const onStyle = () => {
10383
- if (!map.getLight)
10384
- fallbackStyleLight = map.getStyle()?.light;
11300
+ fallbackStyle = void 0;
11301
+ fallbackSun = void 0;
11302
+ if (!map.getLight) fallbackStyleLight = standaloneStyle().light;
10385
11303
  attach();
10386
11304
  };
10387
11305
  map.on("styledata", onStyle);
10388
11306
  map.on("idle", onStyle);
10389
11307
  onStyle();
11308
+ camera.update();
10390
11309
  function grow(goal, change) {
10391
11310
  if (disposed) return Promise.resolve();
10392
11311
  if (!Number.isFinite(goal))
@@ -10508,6 +11427,7 @@ ${shaderData.define}`;
10508
11427
  } catch {
10509
11428
  }
10510
11429
  camera.dispose();
11430
+ mesher.dispose();
10511
11431
  }
10512
11432
  };
10513
11433
  }
@@ -10831,11 +11751,12 @@ function createTileflowStreetLights(options) {
10831
11751
  let corners;
10832
11752
  let disposed = false;
10833
11753
  let lastUpdate = 0;
11754
+ let fallbackMetadata;
10834
11755
  let designs;
10835
11756
  function designsOnScreen() {
10836
11757
  const read = options.controller?.getThemeMetadata?.(tileflowStreetLightsMetadataKey) ?? {
10837
11758
  values: [
10838
- map.getStyle()?.metadata?.[tileflowStreetLightsMetadataKey]
11759
+ (fallbackMetadata ??= map.getStyle()?.metadata ?? {})[tileflowStreetLightsMetadataKey]
10839
11760
  ],
10840
11761
  position: 0
10841
11762
  };
@@ -10845,7 +11766,9 @@ function createTileflowStreetLights(options) {
10845
11766
  );
10846
11767
  designs = {
10847
11768
  values: read.values,
10848
- parsed: parsed.map((entry) => entry ?? tileflowDefaultStreetLightsDesign),
11769
+ parsed: parsed.map(
11770
+ (entry) => options.enabled === false ? disabledStreetLightsDesign : entry ?? (options.enabled === true ? tileflowDefaultStreetLightsDesign : disabledStreetLightsDesign)
11771
+ ),
10849
11772
  declared: parsed.some((entry) => entry !== void 0)
10850
11773
  };
10851
11774
  }
@@ -10929,6 +11852,7 @@ function createTileflowStreetLights(options) {
10929
11852
  return options.controller?.getSun?.() ?? resolveTileflowSun(tileflowDefaultSunPreset);
10930
11853
  }
10931
11854
  function onNow() {
11855
+ if (appearance().intensity <= 0) return 0;
10932
11856
  const brightness = sunNow().brightness;
10933
11857
  return Math.min(1, Math.max(0, (switchOn[1] - brightness) / (switchOn[1] - switchOn[0])));
10934
11858
  }
@@ -11106,13 +12030,22 @@ ${shaderData.define}`;
11106
12030
  return style?.layers?.find((entry) => entry.type === "symbol")?.id;
11107
12031
  }
11108
12032
  function attach() {
11109
- if (disposed || map.getLayer(tileflowStreetLightsLayerId) || !map.isStyleLoaded()) return;
12033
+ if (disposed || !map.isStyleLoaded()) return;
12034
+ const enabled = designsOnScreen().themes.some((theme) => theme.intensity > 0);
12035
+ if (!enabled) {
12036
+ if (map.getLayer(tileflowStreetLightsLayerId)) map.removeLayer(tileflowStreetLightsLayerId);
12037
+ return;
12038
+ }
12039
+ if (map.getLayer(tileflowStreetLightsLayerId)) return;
11110
12040
  try {
11111
12041
  map.addLayer(layer, beforeId());
11112
12042
  } catch {
11113
12043
  }
11114
12044
  }
11115
- const onStyle = () => attach();
12045
+ const onStyle = () => {
12046
+ fallbackMetadata = void 0;
12047
+ attach();
12048
+ };
11116
12049
  map.on("styledata", onStyle);
11117
12050
  map.on("idle", onStyle);
11118
12051
  attach();
@@ -11148,6 +12081,10 @@ ${shaderData.define}`;
11148
12081
  }
11149
12082
  };
11150
12083
  }
12084
+ var disabledStreetLightsDesign = Object.freeze({
12085
+ ...tileflowDefaultStreetLightsDesign,
12086
+ intensity: 0
12087
+ });
11151
12088
  var emptyTile2 = Object.freeze({
11152
12089
  placement: Object.freeze({ lamps: new Uint8Array(0), mapped: 0, estimated: 0 })
11153
12090
  });
@@ -11265,7 +12202,9 @@ var tileflowWorldTreeFields = Object.freeze({
11265
12202
  canopyRank: "rank",
11266
12203
  canopyClearance: "clearance_px_z15",
11267
12204
  canopyCover: "cover_pct",
11268
- canopyHeight: "height_m"
12205
+ canopyHeight: "height_m",
12206
+ subclass: "subclass",
12207
+ hedge: "hedge"
11269
12208
  });
11270
12209
  var tileflowDefaultUnknownTreeForms = Object.freeze({
11271
12210
  round: 1,
@@ -11289,6 +12228,11 @@ function tileflowTreeFormProportion(form) {
11289
12228
  }
11290
12229
  var heightBounds = [2, 45];
11291
12230
  var crownBounds = [1, 25];
12231
+ var hedgeSpacing = 1.2;
12232
+ var hedgeLength = 1.5;
12233
+ var hedgeHeight = 1.3;
12234
+ var hedgeBank = 1;
12235
+ var hedgeBounds = [0.3, 10];
11292
12236
  var palms = /* @__PURE__ */ new Set([
11293
12237
  "acoelorrhaphe",
11294
12238
  "adonidia",
@@ -11586,6 +12530,49 @@ function placeTileflowTrees(layers, options) {
11586
12530
  }
11587
12531
  }
11588
12532
  }
12533
+ const hedges = [];
12534
+ const barriers = layers.hedges;
12535
+ if (barriers) {
12536
+ const scale = extent3 / barriers.extent;
12537
+ const step = hedgeSpacing / (tileMetres / barriers.extent);
12538
+ for (const feature of barriers.features) {
12539
+ if (feature.type === "Point" || feature.properties[fields.subclass] !== "hedge") continue;
12540
+ const bank = feature.properties[fields.hedge] === "hedge_bank" ? hedgeBank : 0;
12541
+ const height = (measured(feature.properties[fields.height], hedgeBounds) ?? hedgeHeight) + bank;
12542
+ for (const points2 of feature.geometry) {
12543
+ let next = step / 2;
12544
+ let along = 0;
12545
+ for (let index = 0; index + 3 < points2.length; index += 2) {
12546
+ const [ax, ay, bx, by] = [
12547
+ points2[index],
12548
+ points2[index + 1],
12549
+ points2[index + 2],
12550
+ points2[index + 3]
12551
+ ];
12552
+ const length = Math.hypot(bx - ax, by - ay);
12553
+ const turn = (Math.atan2(ay - by, bx - ax) / (Math.PI * 2) + 1) % 1;
12554
+ while (next <= along + length) {
12555
+ const t = length > 0 ? (next - along) / length : 0;
12556
+ const px = ax + (bx - ax) * t;
12557
+ const py = ay + (by - ay) * t;
12558
+ if (px >= 0 && py >= 0 && px < barriers.extent && py < barriers.extent) {
12559
+ const seed = hash3(tile.x * extent3 + px * scale, tile.y * extent3 + py * scale, 17);
12560
+ hedges.push({
12561
+ x: px * scale,
12562
+ y: py * scale,
12563
+ height,
12564
+ turn,
12565
+ shade: unit(seed, 5),
12566
+ seed: unit(seed, 7)
12567
+ });
12568
+ }
12569
+ next += step;
12570
+ }
12571
+ along += length;
12572
+ }
12573
+ }
12574
+ }
12575
+ }
11589
12576
  const variantsOf = (form) => clamp(Math.floor(options.variants?.[tileflowTreeForms[form]] ?? 1), [1, 4]);
11590
12577
  const variant = (tree) => Math.min(
11591
12578
  variantsOf(tree.form) - 1,
@@ -11630,7 +12617,20 @@ function placeTileflowTrees(layers, options) {
11630
12617
  count(ranges, index, tree.rank);
11631
12618
  count(ranges.variants[variants[from]], index, tree.rank);
11632
12619
  });
11633
- return { trees: bytes, forms, tallest, mapped, canopy };
12620
+ const hedgeBytes = new Uint8Array(hedges.length * tileflowTreeBytes);
12621
+ const hedgeView = new DataView(hedgeBytes.buffer);
12622
+ hedges.forEach((piece, index) => {
12623
+ const at = index * tileflowTreeBytes;
12624
+ hedgeView.setUint16(at, Math.min(extent3 - 1, Math.round(piece.x)), true);
12625
+ hedgeView.setUint16(at + 2, Math.min(extent3 - 1, Math.round(piece.y)), true);
12626
+ hedgeView.setUint16(at + 4, Math.round(piece.height * 10), true);
12627
+ hedgeView.setUint16(at + 6, Math.round(hedgeLength * 10), true);
12628
+ hedgeBytes[at + 8] = Math.round(piece.turn * 256) & 255;
12629
+ hedgeBytes[at + 9] = Math.floor(piece.shade * 256) & 255;
12630
+ hedgeBytes[at + 12] = Math.floor(piece.seed * 256) & 255;
12631
+ tallest = Math.max(tallest, piece.height);
12632
+ });
12633
+ return { trees: bytes, forms, hedges: hedgeBytes, tallest, mapped, canopy };
11634
12634
  }
11635
12635
  function broadleafOf(mix2) {
11636
12636
  const broad = { round: mix2.round ?? 0, open: mix2.open ?? 0, tall: mix2.tall ?? 0 };
@@ -11688,6 +12688,7 @@ function readTileflowVegetation3D(metadata) {
11688
12688
  const conifer = palette(colors.conifer);
11689
12689
  const palm = palette(colors.palm) ?? broadleaf;
11690
12690
  if (!broadleaf || !conifer || !palm) return void 0;
12691
+ const hedge = palette(colors.hedge) ?? broadleaf.slice(0, 2);
11691
12692
  const scale = (number) => typeof number === "number" && number > 0 && number <= 10 ? number : 1;
11692
12693
  const mix2 = tileflowTreeFormMixProblem(unknownForms) === void 0 ? unknownForms : void 0;
11693
12694
  const models = Object.fromEntries(
@@ -11731,10 +12732,11 @@ function readTileflowVegetation3D(metadata) {
11731
12732
  source,
11732
12733
  layers: {
11733
12734
  tree: layers.tree,
11734
- ...typeof layers.canopy === "string" ? { canopy: layers.canopy } : {}
12735
+ ...typeof layers.canopy === "string" ? { canopy: layers.canopy } : {},
12736
+ ...typeof layers.hedge === "string" ? { hedge: layers.hedge } : {}
11735
12737
  },
11736
12738
  fields: named,
11737
- colors: { bark: colors.bark, broadleaf, conifer, palm },
12739
+ colors: { bark: colors.bark, broadleaf, conifer, palm, hedge },
11738
12740
  heightScale: scale(heightScale),
11739
12741
  crownScale: scale(crownScale),
11740
12742
  forms,
@@ -11774,6 +12776,75 @@ function tileflowTreeMeshes() {
11774
12776
  ])
11775
12777
  );
11776
12778
  }
12779
+ function tileflowHedgeMesh() {
12780
+ const out = {
12781
+ positions: [],
12782
+ normals: [],
12783
+ rounds: [],
12784
+ parts: [],
12785
+ shades: [],
12786
+ occlusion: []
12787
+ };
12788
+ const half = 0.275;
12789
+ const profile = [
12790
+ [-half, 0],
12791
+ [-half, 0.78],
12792
+ [-half * 0.62, 1],
12793
+ [half * 0.62, 1],
12794
+ [half, 0.78],
12795
+ [half, 0]
12796
+ ];
12797
+ const round2 = ([y2, z2]) => normalize([0, y2 / half, Math.max(0, z2 - 0.45) / 0.55]);
12798
+ const shaded = (z2) => 0.55 + 0.45 * z2;
12799
+ for (let index = 0; index + 1 < profile.length; index += 1) {
12800
+ const [y0, z0] = profile[index];
12801
+ const [y1, z1] = profile[index + 1];
12802
+ let facet = normalize([0, z1 - z0, -(y1 - y0)]);
12803
+ if (facet[1] * (y0 + y1) + facet[2] * (z0 + z1 - 0.9) < 0)
12804
+ facet = [-facet[0], -facet[1], -facet[2]];
12805
+ const quad = [
12806
+ [-0.5, y0, z0],
12807
+ [0.5, y0, z0],
12808
+ [0.5, y1, z1],
12809
+ [-0.5, y1, z1]
12810
+ ];
12811
+ const rounds = [round2([y0, z0]), round2([y0, z0]), round2([y1, z1]), round2([y1, z1])];
12812
+ for (const [a, b2, c] of [
12813
+ [0, 1, 2],
12814
+ [0, 2, 3]
12815
+ ])
12816
+ triangle(
12817
+ out,
12818
+ [quad[a], quad[b2], quad[c]],
12819
+ [facet, facet, facet],
12820
+ tileflowTreeParts.leaves,
12821
+ 0,
12822
+ [shaded(quad[a][2]), shaded(quad[b2][2]), shaded(quad[c][2])],
12823
+ [rounds[a], rounds[b2], rounds[c]]
12824
+ );
12825
+ }
12826
+ for (const x3 of [-0.5, 0.5]) {
12827
+ const facet = [Math.sign(x3), 0, 0];
12828
+ const centre = [x3, 0, 0.5];
12829
+ for (let index = 0; index + 1 < profile.length; index += 1) {
12830
+ const points = [
12831
+ centre,
12832
+ [x3, ...profile[index]],
12833
+ [x3, ...profile[index + 1]]
12834
+ ];
12835
+ triangle(
12836
+ out,
12837
+ points,
12838
+ [facet, facet, facet],
12839
+ tileflowTreeParts.leaves,
12840
+ 0,
12841
+ [shaded(0.5), shaded(points[1][2]), shaded(points[2][2])],
12842
+ points.map((point) => normalize([Math.sign(x3), point[1] / half, point[2] - 0.45]))
12843
+ );
12844
+ }
12845
+ }
12846
+ return pack(out);
12847
+ }
11777
12848
  function tileflowTreeModelMeshOf(model) {
11778
12849
  const out = {
11779
12850
  positions: [],
@@ -11971,12 +13042,12 @@ function lobe(out, centre, radii, seed, shade, crownBase, shape = "icosa") {
11971
13042
  if (dot2(facet, outward) < 0) facet = [-facet[0], -facet[1], -facet[2]];
11972
13043
  const normals = [facet, facet, facet];
11973
13044
  const rounds = [a, b2, c].map((index) => round2[index]);
11974
- const occlusion2 = points.map((point) => {
13045
+ const occlusion = points.map((point) => {
11975
13046
  const height = smooth2(crownBase, 1, point[2]);
11976
13047
  const outward2 = Math.min(1, Math.hypot(point[0], point[1]) / 0.45);
11977
13048
  return 0.5 + 0.32 * height + 0.18 * outward2;
11978
13049
  });
11979
- triangle(out, points, normals, tileflowTreeParts.leaves, shade, occlusion2, rounds);
13050
+ triangle(out, points, normals, tileflowTreeParts.leaves, shade, occlusion, rounds);
11980
13051
  }
11981
13052
  }
11982
13053
  function cone(out, z0, z1, radius, sides, shade, closed) {
@@ -12071,7 +13142,7 @@ function palmTrunk(out, rings, sides) {
12071
13142
  ringAt.z
12072
13143
  ];
12073
13144
  const normal = (angle) => normalize([Math.cos(angle), Math.sin(angle), 0.05]);
12074
- const occlusion2 = [
13145
+ const occlusion = [
12075
13146
  0.7 + 0.3 * (a.z / palmTop[2]),
12076
13147
  0.7 + 0.3 * (a.z / palmTop[2]),
12077
13148
  0.7 + 0.3 * (b2.z / palmTop[2])
@@ -12082,7 +13153,7 @@ function palmTrunk(out, rings, sides) {
12082
13153
  [normal(a0), normal(a1), normal(a1)],
12083
13154
  tileflowTreeParts.stem,
12084
13155
  shade,
12085
- occlusion2
13156
+ occlusion
12086
13157
  );
12087
13158
  triangle(
12088
13159
  out,
@@ -12090,7 +13161,7 @@ function palmTrunk(out, rings, sides) {
12090
13161
  [normal(a0), normal(a1), normal(a0)],
12091
13162
  tileflowTreeParts.stem,
12092
13163
  shade,
12093
- [occlusion2[0], occlusion2[2], occlusion2[2]]
13164
+ [occlusion[0], occlusion[2], occlusion[2]]
12094
13165
  );
12095
13166
  }
12096
13167
  }
@@ -12126,7 +13197,7 @@ function fronds(out, count, segments, tier) {
12126
13197
  ];
12127
13198
  };
12128
13199
  const [a, b2] = [spine[step], spine[step + 1]];
12129
- const occlusion2 = [0.72 + 0.28 * t0, 0.72 + 0.28 * t1, 0.72 + 0.28 * t1];
13200
+ const occlusion = [0.72 + 0.28 * t0, 0.72 + 0.28 * t1, 0.72 + 0.28 * t1];
12130
13201
  for (const sign of [-1, 1]) {
12131
13202
  const quad = [a, b2, edge(b2, t1, sign), edge(a, t0, sign)];
12132
13203
  let facet = faceNormal([quad[0], quad[1], quad[2]]);
@@ -12138,7 +13209,7 @@ function fronds(out, count, segments, tier) {
12138
13209
  [normal, normal, normal],
12139
13210
  tileflowTreeParts.fronds,
12140
13211
  shade,
12141
- occlusion2
13212
+ occlusion
12142
13213
  );
12143
13214
  triangle(
12144
13215
  out,
@@ -12146,13 +13217,13 @@ function fronds(out, count, segments, tier) {
12146
13217
  [normal, normal, normal],
12147
13218
  tileflowTreeParts.fronds,
12148
13219
  shade,
12149
- [occlusion2[0], occlusion2[2], occlusion2[0]]
13220
+ [occlusion[0], occlusion[2], occlusion[0]]
12150
13221
  );
12151
13222
  }
12152
13223
  }
12153
13224
  }
12154
13225
  }
12155
- function triangle(out, points, normals, part, shade, occlusion2, rounds = normals) {
13226
+ function triangle(out, points, normals, part, shade, occlusion, rounds = normals) {
12156
13227
  const facing = dot2(faceNormal(points), [
12157
13228
  normals[0][0] + normals[1][0] + normals[2][0],
12158
13229
  normals[0][1] + normals[1][1] + normals[2][1],
@@ -12165,7 +13236,7 @@ function triangle(out, points, normals, part, shade, occlusion2, rounds = normal
12165
13236
  out.rounds.push(...rounds[index]);
12166
13237
  out.parts.push(part);
12167
13238
  out.shades.push(shade);
12168
- out.occlusion.push(Math.min(1, Math.max(0, occlusion2[index])));
13239
+ out.occlusion.push(Math.min(1, Math.max(0, occlusion[index])));
12169
13240
  }
12170
13241
  }
12171
13242
  function pack(out) {
@@ -12322,7 +13393,7 @@ function decodeTileflowTreeModel(text2, level) {
12322
13393
  const normals = [];
12323
13394
  const parts = [];
12324
13395
  const shades = [];
12325
- const occlusion2 = [];
13396
+ const occlusion = [];
12326
13397
  for (let index = 0; index < count; index += 1) {
12327
13398
  const at = headerBytes + index * vertexBytes;
12328
13399
  for (let axis = 0; axis < 3; axis += 1) {
@@ -12332,7 +13403,7 @@ function decodeTileflowTreeModel(text2, level) {
12332
13403
  if (bytes[at + 9] >= partCount2) return void 0;
12333
13404
  parts.push(bytes[at + 9]);
12334
13405
  shades.push(bytes[at + 10] / 255 * 2 - 1);
12335
- occlusion2.push(bytes[at + 11] / 255);
13406
+ occlusion.push(bytes[at + 11] / 255);
12336
13407
  }
12337
13408
  const indices = [];
12338
13409
  const indexAt = headerBytes + count * vertexBytes;
@@ -12341,7 +13412,7 @@ function decodeTileflowTreeModel(text2, level) {
12341
13412
  if (vertex >= count) return void 0;
12342
13413
  indices.push(vertex);
12343
13414
  }
12344
- return { proportion, positions, normals, parts, shades, occlusion: occlusion2, indices };
13415
+ return { proportion, positions, normals, parts, shades, occlusion, indices };
12345
13416
  }
12346
13417
 
12347
13418
  // src/trees-3d-shaders.ts
@@ -12404,9 +13475,20 @@ vec3 sceneOf(vec3 local) {
12404
13475
  return vec3((a_tree_pos + u_offset) * u_metres * vec2(1.0, -1.0), 0.0) + local;
12405
13476
  }
12406
13477
  `;
13478
+ var treeDither = `
13479
+ // An ordered dither shared by visible and depth passes, so faded trees cast faded shadows.
13480
+ float dither(vec2 p) {
13481
+ ivec2 i = ivec2(mod(p, 4.0));
13482
+ int index = i.x + i.y * 4;
13483
+ float table[16] = float[16](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
13484
+ return (table[index] + 0.5) / 16.0;
13485
+ }`;
12407
13486
  var tileflowTreeDepthVertex = `#version 300 es
12408
13487
  ${attributes3}
12409
13488
  uniform mat4 u_light_matrix;
13489
+ uniform vec2 u_centre;
13490
+ uniform vec2 u_reach;
13491
+ out float v_fade;
12410
13492
  ${placement2}
12411
13493
  void main() {
12412
13494
  float growth = growthOf();
@@ -12415,10 +13497,17 @@ void main() {
12415
13497
  return;
12416
13498
  }
12417
13499
  gl_Position = u_light_matrix * vec4(sceneOf(standing(growth)), 1.0);
13500
+ float distance = length((a_tree_pos - u_centre) * u_metres);
13501
+ v_fade = 1.0 - smoothstep(u_reach.x, u_reach.y, distance);
12418
13502
  }`;
12419
13503
  var tileflowTreeDepthFragment = `#version 300 es
12420
13504
  precision highp float;
12421
- void main() {}`;
13505
+ in float v_fade;
13506
+ uniform float u_fade;
13507
+ ${treeDither}
13508
+ void main() {
13509
+ if (v_fade * u_fade < dither(gl_FragCoord.xy)) discard;
13510
+ }`;
12422
13511
  var tileflowTreeVertex = (projected) => `#version 300 es
12423
13512
  ${projected}
12424
13513
  ${attributes3}
@@ -12468,31 +13557,10 @@ uniform float u_toon; // 1 for light in bands, as a drawing has it
12468
13557
  uniform vec3 u_bark;
12469
13558
  uniform vec3 u_leaves[4]; // the form's colours, sunlit
12470
13559
  uniform float u_leaf_count;
12471
- uniform highp sampler2DShadow u_shadow_map;
12472
- uniform vec2 u_shadow_texel;
12473
- uniform float u_shadow;
12474
- uniform float u_depth_unit;
12475
13560
  out vec4 fragColor;
13561
+ ${tileflowShadowLookup}
12476
13562
 
12477
- float visibility(vec3 c, float bias) {
12478
- if (u_shadow <= 0.0 || c.x <= 0.0 || c.y <= 0.0 || c.x >= 1.0 || c.y >= 1.0 || c.z >= 1.0)
12479
- return 1.0;
12480
- float z = c.z - bias * u_depth_unit;
12481
- vec2 o = u_shadow_texel * 0.75;
12482
- return 0.25 * (
12483
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, -o.y), z)) +
12484
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, -o.y), z)) +
12485
- texture(u_shadow_map, vec3(c.xy + vec2(-o.x, o.y), z)) +
12486
- texture(u_shadow_map, vec3(c.xy + vec2(o.x, o.y), z)));
12487
- }
12488
-
12489
- // An ordered dither, so a fading tree thins out instead of turning see-through.
12490
- float dither(vec2 p) {
12491
- ivec2 i = ivec2(mod(p, 4.0));
12492
- int index = i.x + i.y * 4;
12493
- float table[16] = float[16](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
12494
- return (table[index] + 0.5) / 16.0;
12495
- }
13563
+ ${treeDither}
12496
13564
 
12497
13565
  vec3 leafColour() {
12498
13566
  float count = max(u_leaf_count, 1.0);
@@ -12536,9 +13604,10 @@ void main() {
12536
13604
  vec3 roof = u_sky_light + u_sun_light * max(u_sun_dir.z, 0.0);
12537
13605
  vec3 colour = base * min(pow(light / max(roof, vec3(1e-4)), vec3(1.0 / 2.2)), vec3(1.5));
12538
13606
  // At night the tops of crowns keep near their theme's colour, as a night theme draws them, so
12539
- // trees stay apart from the dark ground.
13607
+ // trees stay apart from the dark ground. The lift is neutral: colour comes from the material
13608
+ // and scene illumination, so a grey tree in neutral light stays grey.
12540
13609
  float crownTop = bark ? 0.0 : smoothstep(0.1, 0.9, normal.z);
12541
- colour = mix(colour, base * 0.95 + vec3(0.02, 0.03, 0.04), u_night * 0.45 * crownTop);
13610
+ colour = mix(colour, base * 0.95 + vec3(0.03), u_night * 0.45 * crownTop);
12542
13611
  fragColor = vec4(colour, 1.0);
12543
13612
  }`;
12544
13613
  var tileflowTreeEdgeVertex = (projected) => `#version 300 es
@@ -12589,6 +13658,40 @@ void main() {
12589
13658
  fragColor = vec4(u_edge_colour, 1.0);
12590
13659
  }`;
12591
13660
 
13661
+ // src/trees-3d-view.ts
13662
+ function tileflowTreeView(camera) {
13663
+ const n = 2 ** camera.tileZoom;
13664
+ const phi = camera.lat * Math.PI / 180;
13665
+ const centre = [
13666
+ (camera.lng + 180) / 360 * n,
13667
+ (1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2 * n
13668
+ ];
13669
+ const tileMetres = 40075016686e-3 * Math.cos(phi) / n;
13670
+ const far = Math.min(
13671
+ 3e3,
13672
+ Math.max(500, 900 * 2 ** (16 - camera.zoom) * (1 + camera.pitch / 90))
13673
+ );
13674
+ const reach = [far * 0.7, far];
13675
+ const base = camera.zoom >= 16.5 ? 15 : camera.zoom >= 16 ? 12 : camera.zoom >= 15.5 ? 9 : 7;
13676
+ return {
13677
+ centre,
13678
+ metres: tileMetres / 8192,
13679
+ reach,
13680
+ // Camera movement changes instance density, mesh selection and the per-tree fade.
13681
+ key: `${centre.join()}:${reach.join()}:${base}`,
13682
+ tile(x3, y2) {
13683
+ const [cx, cy] = centre;
13684
+ const distance = Math.hypot(Math.max(x3, Math.min(cx, x3 + 1)) - cx, Math.max(y2, Math.min(cy, y2 + 1)) - cy) * tileMetres;
13685
+ if (distance > far) return void 0;
13686
+ const fall = distance > reach[0] ? Math.ceil((distance - reach[0]) / (far - reach[0]) * 6) : 0;
13687
+ return {
13688
+ level: distance < reach[0] * 0.45 ? "near" : "far",
13689
+ rank: Math.max(2, Math.min(15, base - fall))
13690
+ };
13691
+ }
13692
+ };
13693
+ }
13694
+
12592
13695
  // src/trees-3d-browser.ts
12593
13696
  var tileflowTrees3DLayerId = "tileflow-trees-3d";
12594
13697
  var preferredZoom3 = 15;
@@ -12598,7 +13701,7 @@ var leafSlots = 4;
12598
13701
  var finishes = { faceted: { round: 0.5 }, smooth: { round: 1 }, toon: { round: 0.3 } };
12599
13702
  function createTileflowTrees3D(options) {
12600
13703
  const { map } = options;
12601
- const minzoom = options.minzoom ?? 15;
13704
+ const minzoom = options.minzoom ?? tileflowSceneMinzoom;
12602
13705
  const maxTiles = Math.round(options.maxTiles ?? 25);
12603
13706
  if (!Number.isFinite(minzoom) || minzoom < 0 || minzoom > 24)
12604
13707
  throw new RangeError("Tileflow 3D trees minzoom must be a zoom from 0 to 24.");
@@ -12610,7 +13713,14 @@ function createTileflowTrees3D(options) {
12610
13713
  let gl;
12611
13714
  let programs = {};
12612
13715
  let variant;
13716
+ const programVariants = /* @__PURE__ */ new Map();
13717
+ let fallbackStyle;
13718
+ let fallbackSun;
13719
+ function standaloneStyle() {
13720
+ return fallbackStyle ??= map.getStyle() ?? {};
13721
+ }
12613
13722
  let meshes;
13723
+ let hedgeMesh;
12614
13724
  const modelMeshes = /* @__PURE__ */ new Map();
12615
13725
  let ground;
12616
13726
  let disposed = false;
@@ -12623,9 +13733,7 @@ function createTileflowTrees3D(options) {
12623
13733
  let design;
12624
13734
  function vegetationOnScreen() {
12625
13735
  const read = options.controller?.getThemeMetadata?.(tileflowVegetationMetadataKey) ?? {
12626
- values: [
12627
- map.getStyle()?.metadata?.[tileflowVegetationMetadataKey]
12628
- ],
13736
+ values: [standaloneStyle().metadata?.[tileflowVegetationMetadataKey]],
12629
13737
  position: 0
12630
13738
  };
12631
13739
  if (!design || design.values.length !== read.values.length || design.values.some((value, index) => value !== read.values[index]))
@@ -12669,7 +13777,7 @@ function createTileflowTrees3D(options) {
12669
13777
  const make = (source, layers) => createCameraTiles({
12670
13778
  map,
12671
13779
  source,
12672
- layers: [layers.tree, layers.canopy].filter((name) => !!name),
13780
+ layers: [layers.tree, layers.canopy, layers.hedge].filter((name) => !!name),
12673
13781
  zoom: preferredZoom3,
12674
13782
  minzoom,
12675
13783
  maxTiles,
@@ -12678,9 +13786,10 @@ function createTileflowTrees3D(options) {
12678
13786
  build(tile, tileMetres) {
12679
13787
  const trees = layers.tree ? tile.layers[layers.tree] : void 0;
12680
13788
  const canopy = layers.canopy ? tile.layers[layers.canopy] : void 0;
12681
- if (!trees && !canopy) return emptyTile3;
13789
+ const hedges = layers.hedge ? tile.layers[layers.hedge] : void 0;
13790
+ if (!trees && !canopy && !hedges) return emptyTile3;
12682
13791
  const placement3 = placeTileflowTrees(
12683
- { ...trees ? { trees } : {}, ...canopy ? { canopy } : {} },
13792
+ { ...trees ? { trees } : {}, ...canopy ? { canopy } : {}, ...hedges ? { hedges } : {} },
12684
13793
  {
12685
13794
  tile,
12686
13795
  tileMetres,
@@ -12695,7 +13804,7 @@ function createTileflowTrees3D(options) {
12695
13804
  )
12696
13805
  }
12697
13806
  );
12698
- return placement3.trees.length > 0 ? { placement: placement3 } : emptyTile3;
13807
+ return placement3.trees.length > 0 || placement3.hedges.length > 0 ? { placement: placement3 } : emptyTile3;
12699
13808
  },
12700
13809
  empty: emptyTile3,
12701
13810
  release(value) {
@@ -12709,7 +13818,8 @@ function createTileflowTrees3D(options) {
12709
13818
  tiles.push(
12710
13819
  make(described.source, {
12711
13820
  tree: described.layers.tree,
12712
- ...canopyElsewhere || !described.layers.canopy ? {} : { canopy: described.layers.canopy }
13821
+ ...canopyElsewhere || !described.layers.canopy ? {} : { canopy: described.layers.canopy },
13822
+ ...described.layers.hedge ? { hedge: described.layers.hedge } : {}
12713
13823
  })
12714
13824
  );
12715
13825
  if (canopyElsewhere)
@@ -12721,6 +13831,10 @@ function createTileflowTrees3D(options) {
12721
13831
  if (!tile.gl) return;
12722
13832
  for (const { vao } of tile.gl.vaos.values()) context.deleteVertexArray(vao);
12723
13833
  context.deleteBuffer(tile.gl.vbo);
13834
+ if (tile.gl.hedge) {
13835
+ context.deleteVertexArray(tile.gl.hedge.vao);
13836
+ context.deleteBuffer(tile.gl.hedge.vbo);
13837
+ }
12724
13838
  tile.gl = void 0;
12725
13839
  }
12726
13840
  function drawn() {
@@ -12731,11 +13845,12 @@ function createTileflowTrees3D(options) {
12731
13845
  return entries;
12732
13846
  }
12733
13847
  function upload(context) {
12734
- if (!meshes) return;
13848
+ if (!meshes || !hedgeMesh) return;
12735
13849
  let budget = 2;
12736
13850
  for (const set of tiles)
12737
13851
  for (const { value: tile } of set.ready()) {
12738
- if (tile.gl || tile.placement.trees.length === 0) continue;
13852
+ if (tile.gl || tile.placement.trees.length === 0 && tile.placement.hedges.length === 0)
13853
+ continue;
12739
13854
  if (budget-- <= 0) {
12740
13855
  map.triggerRepaint();
12741
13856
  return;
@@ -12758,9 +13873,21 @@ function createTileflowTrees3D(options) {
12758
13873
  vaos.set(`${form}:${model}:${level}`, { vao, count: mesh.count, crown });
12759
13874
  }
12760
13875
  });
13876
+ let hedge;
13877
+ if (tile.placement.hedges.length > 0) {
13878
+ const hedgeVbo = context.createBuffer();
13879
+ context.bindBuffer(context.ARRAY_BUFFER, hedgeVbo);
13880
+ context.bufferData(context.ARRAY_BUFFER, tile.placement.hedges, context.STATIC_DRAW);
13881
+ const vao = context.createVertexArray();
13882
+ context.bindVertexArray(vao);
13883
+ bindMesh(context, hedgeMesh.buffer);
13884
+ bindTrees(context, hedgeVbo, 0);
13885
+ hedge = { vao, vbo: hedgeVbo, count: hedgeMesh.count, crown: 1 };
13886
+ }
12761
13887
  context.bindVertexArray(null);
12762
- tile.gl = { vbo, vaos };
13888
+ tile.gl = { vbo, vaos, ...hedge ? { hedge } : {} };
12763
13889
  generation += 1;
13890
+ map.triggerRepaint();
12764
13891
  }
12765
13892
  }
12766
13893
  function modelOf(context, form, model, level) {
@@ -12811,7 +13938,7 @@ function createTileflowTrees3D(options) {
12811
13938
  instance(7, 2, context.UNSIGNED_BYTE, true, 12);
12812
13939
  }
12813
13940
  function sunNow() {
12814
- return options.controller?.getSun?.() ?? resolveTileflowSun(tileflowDefaultSunPreset);
13941
+ return options.controller?.getSun?.() ?? (fallbackSun ??= resolveTileflowSun(tileflowStyleSunPreset(standaloneStyle())));
12815
13942
  }
12816
13943
  const parsedColours = /* @__PURE__ */ new Map();
12817
13944
  function rgb(colour) {
@@ -12854,6 +13981,7 @@ function createTileflowTrees3D(options) {
12854
13981
  broadleaf: slots(theme?.colors.broadleaf ?? ["#6f9a5b"]),
12855
13982
  conifer: slots(theme?.colors.conifer ?? ["#557f52"]),
12856
13983
  palm: slots(theme?.colors.palm ?? ["#7fa45f"]),
13984
+ hedge: slots(theme?.colors.hedge ?? ["#5f8a55"]),
12857
13985
  size: [[theme?.heightScale ?? 1, theme?.crownScale ?? 1, 0]],
12858
13986
  forms: tileflowTreeForms.map((form) => [
12859
13987
  theme?.forms[form]?.heightScale ?? 1,
@@ -12872,6 +14000,7 @@ function createTileflowTrees3D(options) {
12872
14000
  broadleaf: mixList(x3.broadleaf, y2.broadleaf),
12873
14001
  conifer: mixList(x3.conifer, y2.conifer),
12874
14002
  palm: mixList(x3.palm, y2.palm),
14003
+ hedge: mixList(x3.hedge, y2.hedge),
12875
14004
  size,
12876
14005
  /** Each form's height and crown scale: the map's times the form's own. */
12877
14006
  sizes: Object.fromEntries(
@@ -12890,27 +14019,22 @@ function createTileflowTrees3D(options) {
12890
14019
  return scale;
12891
14020
  }
12892
14021
  function fadeIn() {
12893
- const t = Math.min(1, Math.max(0, (map.getZoom() - minzoom) / 0.8));
12894
- return t * t * (3 - 2 * t);
12895
- }
12896
- function reach() {
12897
- const zoom = map.getZoom();
12898
- const far = Math.min(3e3, Math.max(500, 900 * 2 ** (16 - zoom) * (1 + map.getPitch() / 90)));
12899
- return [far * 0.7, far];
12900
- }
12901
- function rankOf(distance) {
12902
- const zoom = map.getZoom();
12903
- const base = zoom >= 16.5 ? 15 : zoom >= 16 ? 12 : zoom >= 15.5 ? 9 : 7;
12904
- const [start, end] = reach();
12905
- const fall = distance > start ? Math.ceil((distance - start) / (end - start) * 6) : 0;
12906
- return Math.max(2, Math.min(15, base - fall));
14022
+ return tileflowSceneOpacity(map.getZoom(), minzoom);
14023
+ }
14024
+ function view() {
14025
+ return tileflowTreeView({
14026
+ ...map.getCenter(),
14027
+ zoom: map.getZoom(),
14028
+ pitch: map.getPitch(),
14029
+ tileZoom: preferredZoom3
14030
+ });
12907
14031
  }
12908
14032
  const caster = {
12909
14033
  groundOrder: 1,
12910
14034
  bounds() {
12911
14035
  const shown = visibleScale();
12912
14036
  const entries = drawn();
12913
- if (shown <= 0 || entries.length === 0 || map.getZoom() < minzoom) return void 0;
14037
+ if (shown <= 0 || entries.length === 0 || fadeIn() <= 0) return void 0;
12914
14038
  let [left, top, right, bottom, tallest] = [Infinity, Infinity, -Infinity, -Infinity, 0];
12915
14039
  for (const { x: x3, y: y2, set, value } of entries) {
12916
14040
  if (set.zoom !== preferredZoom3) continue;
@@ -12928,8 +14052,8 @@ function createTileflowTrees3D(options) {
12928
14052
  tallest: tallest * shown * size[0]
12929
14053
  };
12930
14054
  },
12931
- // The shadow map is drawn again only when the trees it holds or their size change.
12932
- key: () => `trees:${visibleScale().toFixed(4)}:${generation}:${programs.depth ? 1 : 0}:${Object.values(
14055
+ // Camera-dependent density, detail and fading also change the trees casting shadows.
14056
+ key: () => `trees:${view().key}:${fadeIn().toFixed(4)}:${visibleScale().toFixed(4)}:${generation}:${programs.depth ? 1 : 0}:${Object.values(
12933
14057
  colours().sizes
12934
14058
  ).flat().map((value) => value.toFixed(3)).join()}:${drawn().map(({ x: x3, y: y2 }) => `${x3}/${y2}`).join(";")}`,
12935
14059
  drawDepth(context, frame) {
@@ -12941,16 +14065,38 @@ function createTileflowTrees3D(options) {
12941
14065
  context.uniform1f(u.u_metres, frame.metres);
12942
14066
  context.uniformMatrix4fv(u.u_light_matrix, false, frame.light.matrix);
12943
14067
  const sizes = colours().sizes;
14068
+ const camera = view();
14069
+ context.uniform1f(u.u_fade, 1);
14070
+ context.uniform2fv(u.u_reach, [...camera.reach]);
12944
14071
  for (const { x: x3, y: y2, set, value } of drawn()) {
14072
+ const selection = camera.tile(x3, y2);
14073
+ if (!selection) continue;
12945
14074
  context.uniform2f(u.u_offset, ...frame.offsetOf({ x: x3, y: y2 }, set.zoom));
14075
+ context.uniform2f(
14076
+ u.u_centre,
14077
+ (camera.centre[0] - x3) * tileflowSceneExtent,
14078
+ (camera.centre[1] - y2) * tileflowSceneExtent
14079
+ );
12946
14080
  for (const form of tileflowTreeForms)
12947
14081
  value.placement.forms[form].variants.forEach((range, model) => {
12948
- const drawing = value.gl.vaos.get(`${form}:${model}:far`);
12949
- if (range.count === 0 || !drawing) return;
14082
+ const instances = range.upTo[selection.rank] ?? 0;
14083
+ const drawing = value.gl.vaos.get(`${form}:${model}:${selection.level}`);
14084
+ if (instances === 0 || !drawing) return;
12950
14085
  context.uniform2f(u.u_size_scale, sizes[form][0], sizes[form][1] * drawing.crown);
12951
14086
  context.bindVertexArray(drawing.vao);
12952
- context.drawArraysInstanced(context.TRIANGLES, 0, drawing.count, range.count);
14087
+ context.drawArraysInstanced(context.TRIANGLES, 0, drawing.count, instances);
12953
14088
  });
14089
+ const hedge = value.gl.hedge;
14090
+ if (hedge) {
14091
+ context.uniform2f(u.u_size_scale, 1, 1);
14092
+ context.bindVertexArray(hedge.vao);
14093
+ context.drawArraysInstanced(
14094
+ context.TRIANGLES,
14095
+ 0,
14096
+ hedge.count,
14097
+ value.placement.hedges.length / tileflowTreeBytes
14098
+ );
14099
+ }
12954
14100
  }
12955
14101
  }
12956
14102
  };
@@ -12968,17 +14114,13 @@ function createTileflowTrees3D(options) {
12968
14114
  drawnLast = 0;
12969
14115
  if (entries.length === 0 || !meshes || !programs.trees) return;
12970
14116
  const sun = sunNow();
12971
- const light = tileflowSceneLight(sun);
14117
+ const sunLight = tileflowSceneLight(sun);
14118
+ const light = { ...sunLight, strength: sunLight.strength * fadeIn() };
12972
14119
  const palette2 = colours();
12973
14120
  const frame = scene.frame();
12974
- const centre = map.getCenter();
12975
- const zoom = preferredZoom3;
12976
- const n = 2 ** zoom;
12977
- const cx = (centre.lng + 180) / 360 * n;
12978
- const phi = centre.lat * Math.PI / 180;
12979
- const cy = (1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2 * n;
12980
- const metres = frame?.metres ?? 40075016686e-3 * Math.cos(phi) / n / tileflowSceneExtent;
12981
- const [near, far] = reach();
14121
+ const camera = view();
14122
+ const [cx, cy] = camera.centre;
14123
+ const metres = frame?.metres ?? camera.metres;
12982
14124
  const program = programs.trees;
12983
14125
  const u = program.uniforms;
12984
14126
  context.useProgram(program.program);
@@ -12997,7 +14139,7 @@ function createTileflowTrees3D(options) {
12997
14139
  context.uniform3fv(u.u_sky_light, light.skyLight);
12998
14140
  context.uniform1f(u.u_night, light.night);
12999
14141
  context.uniform1f(u.u_fade, fadeIn());
13000
- context.uniform2fv(u.u_reach, reach());
14142
+ context.uniform2fv(u.u_reach, [...camera.reach]);
13001
14143
  context.uniform3fv(u.u_bark, palette2.bark);
13002
14144
  context.uniform1f(u.u_leaf_count, leafSlots);
13003
14145
  scene.bindShadow(context, u, light.strength);
@@ -13012,10 +14154,9 @@ function createTileflowTrees3D(options) {
13012
14154
  (cx - x3) * tileflowSceneExtent,
13013
14155
  (cy - y2) * tileflowSceneExtent
13014
14156
  );
13015
- const distance = Math.hypot(Math.max(x3, Math.min(cx, x3 + 1)) - cx, Math.max(y2, Math.min(cy, y2 + 1)) - cy) * tileflowSceneExtent * metres;
13016
- if (distance > far) continue;
13017
- const level = distance < near * 0.45 ? "near" : "far";
13018
- const rank = rankOf(distance);
14157
+ const selection = camera.tile(x3, y2);
14158
+ if (!selection) continue;
14159
+ const { level, rank } = selection;
13019
14160
  for (const form of tileflowTreeForms) {
13020
14161
  const leaves = form === "palm" ? palette2.palm : form === "conifer" || form === "columnar" ? palette2.conifer : palette2.broadleaf;
13021
14162
  value.placement.forms[form].variants.forEach((range, model) => {
@@ -13023,14 +14164,26 @@ function createTileflowTrees3D(options) {
13023
14164
  const drawing = value.gl.vaos.get(`${form}:${model}:${level}`);
13024
14165
  if (instances === 0 || !drawing) return;
13025
14166
  context.uniform3fv(u.u_leaves, leaves.flat());
13026
- const size = palette2.sizes[form];
13027
- context.uniform2f(u.u_size_scale, size[0], size[1] * drawing.crown);
14167
+ const size = [
14168
+ palette2.sizes[form][0],
14169
+ palette2.sizes[form][1] * drawing.crown
14170
+ ];
14171
+ context.uniform2f(u.u_size_scale, ...size);
13028
14172
  context.bindVertexArray(drawing.vao);
13029
14173
  context.drawArraysInstanced(context.TRIANGLES, 0, drawing.count, instances);
13030
14174
  count += instances;
13031
- draws.push({ x: x3, y: y2, offset, form, instances, drawing });
14175
+ draws.push({ x: x3, y: y2, offset, leaves, size, instances, drawing });
13032
14176
  });
13033
14177
  }
14178
+ const hedge = value.gl.hedge;
14179
+ if (hedge) {
14180
+ const instances = value.placement.hedges.length / tileflowTreeBytes;
14181
+ context.uniform3fv(u.u_leaves, palette2.hedge.flat());
14182
+ context.uniform2f(u.u_size_scale, 1, 1);
14183
+ context.bindVertexArray(hedge.vao);
14184
+ context.drawArraysInstanced(context.TRIANGLES, 0, hedge.count, instances);
14185
+ draws.push({ x: x3, y: y2, offset, leaves: palette2.hedge, size: [1, 1], instances, drawing: hedge });
14186
+ }
13034
14187
  }
13035
14188
  drawnLast = count;
13036
14189
  const edges = palette2.edges;
@@ -13044,7 +14197,7 @@ function createTileflowTrees3D(options) {
13044
14197
  context.uniform1f(e.u_metres, metres);
13045
14198
  context.uniform1f(e.u_round, finishes[palette2.finish].round);
13046
14199
  context.uniform1f(e.u_fade, fadeIn());
13047
- context.uniform2fv(e.u_reach, reach());
14200
+ context.uniform2fv(e.u_reach, [...camera.reach]);
13048
14201
  context.uniform2f(e.u_viewport, context.drawingBufferWidth, context.drawingBufferHeight);
13049
14202
  context.uniform1f(e.u_edge_width, edges * 1.5 * pixelRatio(context));
13050
14203
  let tile = "";
@@ -13059,16 +14212,14 @@ function createTileflowTrees3D(options) {
13059
14212
  (cy - draw2.y) * tileflowSceneExtent
13060
14213
  );
13061
14214
  }
13062
- const leaves = draw2.form === "palm" ? palette2.palm : draw2.form === "conifer" || draw2.form === "columnar" ? palette2.conifer : palette2.broadleaf;
13063
14215
  const own = palette2.ownEdge;
13064
14216
  context.uniform3fv(
13065
14217
  e.u_edge_colour,
13066
14218
  [0, 1, 2].map(
13067
- (channel) => palette2.edgeColor[channel] * own + leaves[0][channel] * 0.4 * (1 - own)
14219
+ (channel) => palette2.edgeColor[channel] * own + draw2.leaves[0][channel] * 0.4 * (1 - own)
13068
14220
  )
13069
14221
  );
13070
- const size = palette2.sizes[draw2.form];
13071
- context.uniform2f(e.u_size_scale, size[0], size[1] * draw2.drawing.crown);
14222
+ context.uniform2f(e.u_size_scale, ...draw2.size);
13072
14223
  context.bindVertexArray(draw2.drawing.vao);
13073
14224
  context.drawArraysInstanced(context.TRIANGLES, 0, draw2.drawing.count, draw2.instances);
13074
14225
  }
@@ -13138,40 +14289,56 @@ function createTileflowTrees3D(options) {
13138
14289
  function link(context, shaderData) {
13139
14290
  const projected = `${shaderData.vertexShaderPrelude}
13140
14291
  ${shaderData.define}`;
13141
- const next = {
13142
- trees: compile(
14292
+ const cached = programVariants.get(projected);
14293
+ if (cached) {
14294
+ programVariants.delete(projected);
14295
+ programVariants.set(projected, cached);
14296
+ programs = cached;
14297
+ variant = projected;
14298
+ return;
14299
+ }
14300
+ const next = {};
14301
+ try {
14302
+ next.trees = compile(
13143
14303
  context,
13144
14304
  tileflowTreeVertex(projected),
13145
14305
  tileflowTreeFragment,
13146
14306
  tileflowTreeAttributes
13147
- ),
13148
- ground: compile(
14307
+ );
14308
+ next.ground = compile(
13149
14309
  context,
13150
14310
  tileflowBuildingGroundVertex(projected),
13151
14311
  tileflowBuildingGroundFragment,
13152
14312
  ["a_pos"]
13153
- ),
13154
- edges: compile(
13155
- context,
13156
- tileflowTreeEdgeVertex(projected),
13157
- tileflowTreeEdgeFragment,
13158
- tileflowTreeAttributes
13159
- ),
13160
- depth: programs.depth ?? compile(
14313
+ );
14314
+ next.depth = programs.depth ?? compile(
13161
14315
  context,
13162
14316
  tileflowTreeDepthVertex,
13163
14317
  tileflowTreeDepthFragment,
13164
14318
  tileflowTreeAttributes
13165
- )
13166
- };
13167
- for (const name of ["trees", "ground", "edges"]) {
13168
- const previous = programs[name];
13169
- if (previous) context.deleteProgram(previous.program);
14319
+ );
14320
+ } catch (error) {
14321
+ for (const name of ["trees", "ground"])
14322
+ if (next[name]) context.deleteProgram(next[name].program);
14323
+ throw error;
14324
+ }
14325
+ programVariants.set(projected, next);
14326
+ if (programVariants.size > 4) {
14327
+ const oldest = programVariants.keys().next().value;
14328
+ const expired = programVariants.get(oldest);
14329
+ for (const name of ["trees", "ground", "edges"])
14330
+ if (expired[name]) context.deleteProgram(expired[name].program);
14331
+ programVariants.delete(oldest);
13170
14332
  }
13171
14333
  programs = next;
13172
- variant = shaderData.variantName;
14334
+ variant = projected;
13173
14335
  }
13174
14336
  function createMeshes(context) {
14337
+ const hedge = tileflowHedgeMesh();
14338
+ const hedgeBuffer = context.createBuffer();
14339
+ context.bindBuffer(context.ARRAY_BUFFER, hedgeBuffer);
14340
+ context.bufferData(context.ARRAY_BUFFER, hedge.vertices, context.STATIC_DRAW);
14341
+ hedgeMesh = { buffer: hedgeBuffer, count: hedge.count };
13175
14342
  const built = tileflowTreeMeshes();
13176
14343
  meshes = Object.fromEntries(
13177
14344
  tileflowTreeForms.map((form) => [
@@ -13220,19 +14387,20 @@ ${shaderData.define}`;
13220
14387
  onAdd(_2, context) {
13221
14388
  if (!isWebGL23(context)) return;
13222
14389
  gl = context;
13223
- createMeshes(context);
13224
14390
  },
13225
14391
  onRemove(_2, context) {
13226
14392
  if (!isWebGL23(context)) return;
13227
14393
  for (const set of tiles)
13228
14394
  for (const entry of set.all()) if (entry.value) release(context, entry.value);
13229
- for (const program of Object.values(programs))
13230
- if (program) context.deleteProgram(program.program);
14395
+ const owned = new Set([...programVariants.values()].flatMap((value) => Object.values(value)));
14396
+ for (const program of owned) if (program) context.deleteProgram(program.program);
14397
+ programVariants.clear();
13231
14398
  if (meshes)
13232
14399
  for (const levels of Object.values(meshes))
13233
14400
  for (const mesh of Object.values(levels)) context.deleteBuffer(mesh.buffer);
13234
14401
  for (const mesh of modelMeshes.values()) if (mesh) context.deleteBuffer(mesh.buffer);
13235
14402
  modelMeshes.clear();
14403
+ if (hedgeMesh) context.deleteBuffer(hedgeMesh.buffer);
13236
14404
  if (ground) {
13237
14405
  context.deleteVertexArray(ground.vao);
13238
14406
  context.deleteBuffer(ground.vbo);
@@ -13242,6 +14410,7 @@ ${shaderData.define}`;
13242
14410
  scene.release(context);
13243
14411
  programs = {};
13244
14412
  meshes = void 0;
14413
+ hedgeMesh = void 0;
13245
14414
  ground = void 0;
13246
14415
  variant = void 0;
13247
14416
  gl = void 0;
@@ -13257,13 +14426,23 @@ ${shaderData.define}`;
13257
14426
  },
13258
14427
  render(context, args) {
13259
14428
  if (!isWebGL23(context)) return;
13260
- try {
13261
- if (variant !== args.shaderData.variantName) link(context, args.shaderData);
13262
- } catch {
14429
+ if (visibleScale() <= 0 || fadeIn() <= 0) {
14430
+ drawnLast = 0;
13263
14431
  return;
13264
14432
  }
13265
- if (visibleScale() <= 0 || map.getZoom() < minzoom) {
13266
- drawnLast = 0;
14433
+ try {
14434
+ const projected = `${args.shaderData.vertexShaderPrelude}
14435
+ ${args.shaderData.define}`;
14436
+ if (variant !== projected) link(context, args.shaderData);
14437
+ if (colours().edges > 0.01 && !programs.edges)
14438
+ programs.edges = compile(
14439
+ context,
14440
+ tileflowTreeEdgeVertex(projected),
14441
+ tileflowTreeEdgeFragment,
14442
+ tileflowTreeAttributes
14443
+ );
14444
+ if (!meshes) createMeshes(context);
14445
+ } catch {
13267
14446
  return;
13268
14447
  }
13269
14448
  ensureTiles();
@@ -13308,6 +14487,8 @@ ${shaderData.define}`;
13308
14487
  }
13309
14488
  }
13310
14489
  const onStyle = () => {
14490
+ fallbackStyle = void 0;
14491
+ fallbackSun = void 0;
13311
14492
  attach();
13312
14493
  if (target > 0) {
13313
14494
  standIn(true);
@@ -13315,7 +14496,7 @@ ${shaderData.define}`;
13315
14496
  }
13316
14497
  };
13317
14498
  map.on("styledata", onStyle);
13318
- map.on("idle", onStyle);
14499
+ map.on("idle", attach);
13319
14500
  attach();
13320
14501
  function grow(goal, change) {
13321
14502
  if (disposed) return Promise.resolve();
@@ -13376,20 +14557,20 @@ ${shaderData.define}`;
13376
14557
  const model = described.models[id];
13377
14558
  const decoded = model ? decodeTileflowTreeModel(model.near, "near") : void 0;
13378
14559
  const mesh = decoded ? tileflowTreeModelMeshOf(decoded) : built[form].near;
13379
- const view = new DataView(
14560
+ const view2 = new DataView(
13380
14561
  mesh.vertices.buffer,
13381
14562
  mesh.vertices.byteOffset,
13382
14563
  mesh.vertices.byteLength
13383
14564
  );
13384
14565
  const indices = Array.from({ length: mesh.count }, (_2, i2) => i2);
13385
14566
  const preview = inspectModelTriangles(indices, (index) => [
13386
- view.getFloat32(index * 24, true),
13387
- view.getFloat32(index * 24 + 4, true),
13388
- view.getFloat32(index * 24 + 8, true)
14567
+ view2.getFloat32(index * 24, true),
14568
+ view2.getFloat32(index * 24 + 4, true),
14569
+ view2.getFloat32(index * 24 + 8, true)
13389
14570
  ]);
13390
14571
  const colors = [];
13391
14572
  for (let i2 = 0; i2 < mesh.count; i2 += 3) {
13392
- const part = view.getUint8(i2 * 24 + 15);
14573
+ const part = view2.getUint8(i2 * 24 + 15);
13393
14574
  const palette2 = described.colors[form === "conifer" ? "conifer" : form === "palm" ? "palm" : "broadleaf"];
13394
14575
  colors.push(part === 0 || part === 3 ? described.colors.bark : palette2[0]);
13395
14576
  }
@@ -13420,13 +14601,15 @@ ${shaderData.define}`;
13420
14601
  let trees = 0;
13421
14602
  let mapped = 0;
13422
14603
  let canopy = 0;
14604
+ let hedges = 0;
13423
14605
  for (const set of tiles)
13424
14606
  for (const { value } of set.ready()) {
13425
- if (value.placement.trees.length === 0) continue;
14607
+ if (value.placement.trees.length === 0 && value.placement.hedges.length === 0) continue;
13426
14608
  count += 1;
13427
14609
  trees += value.placement.trees.length / tileflowTreeBytes;
13428
14610
  mapped += value.placement.mapped;
13429
14611
  canopy += value.placement.canopy;
14612
+ hedges += value.placement.hedges.length / tileflowTreeBytes;
13430
14613
  }
13431
14614
  return {
13432
14615
  declared: vegetation() !== void 0,
@@ -13434,6 +14617,7 @@ ${shaderData.define}`;
13434
14617
  trees,
13435
14618
  mapped,
13436
14619
  canopy,
14620
+ hedges,
13437
14621
  drawn: drawnLast
13438
14622
  };
13439
14623
  },
@@ -13443,7 +14627,7 @@ ${shaderData.define}`;
13443
14627
  motion?.done();
13444
14628
  standIn(false);
13445
14629
  map.off("styledata", onStyle);
13446
- map.off("idle", onStyle);
14630
+ map.off("idle", attach);
13447
14631
  unregister();
13448
14632
  try {
13449
14633
  if (map.getLayer(tileflowTrees3DLayerId)) map.removeLayer(tileflowTrees3DLayerId);
@@ -13460,6 +14644,7 @@ var emptyTile3 = Object.freeze({
13460
14644
  forms: Object.fromEntries(
13461
14645
  tileflowTreeForms.map((form) => [form, { start: 0, count: 0, upTo: new Array(16).fill(0) }])
13462
14646
  ),
14647
+ hedges: new Uint8Array(0),
13463
14648
  tallest: 0,
13464
14649
  mapped: 0,
13465
14650
  canopy: 0
@@ -15015,6 +16200,19 @@ function createTileflowThemeController(options) {
15015
16200
  const blendMetadata = /* @__PURE__ */ new WeakMap();
15016
16201
  let nextStyleIdentity = 0;
15017
16202
  let current = options.initial;
16203
+ let metadataStyle;
16204
+ const invalidateMetadata = () => {
16205
+ metadataStyle = void 0;
16206
+ };
16207
+ const releaseMetadata = () => {
16208
+ options.map.off("styledata", invalidateMetadata);
16209
+ options.map.off("style.load", invalidateMetadata);
16210
+ options.map.off("remove", releaseMetadata);
16211
+ invalidateMetadata();
16212
+ };
16213
+ options.map.on("styledata", invalidateMetadata);
16214
+ options.map.on("style.load", invalidateMetadata);
16215
+ options.map.on("remove", releaseMetadata);
15018
16216
  let shown = { style: options.initial.style };
15019
16217
  let activeBlend;
15020
16218
  let pendingBlend;
@@ -15060,6 +16258,7 @@ function createTileflowThemeController(options) {
15060
16258
  return {
15061
16259
  dispose() {
15062
16260
  disposed = true;
16261
+ releaseMetadata();
15063
16262
  images?.dispose();
15064
16263
  requestId += 1;
15065
16264
  activeBlend?.blender.dispose();
@@ -15109,7 +16308,7 @@ function createTileflowThemeController(options) {
15109
16308
  const blended = activeBlend && blendMetadata.get(activeBlend.plan);
15110
16309
  if (activeBlend && blended)
15111
16310
  return { values: blended.map(read), position: activeBlend.blender.position };
15112
- const style = typeof current.style === "string" ? shownStyleDocument() : current.style;
16311
+ const style = typeof current.style === "string" ? metadataStyle ??= shownStyleDocument() : current.style;
15113
16312
  return { values: [read(style?.metadata)], position: 0 };
15114
16313
  },
15115
16314
  getSun() {
@@ -15308,7 +16507,9 @@ function createTileflowThemeController(options) {
15308
16507
  const styleInput = target.kind === "theme" ? target.style.style : target.plan.styleAt(target.position());
15309
16508
  try {
15310
16509
  if (target.kind === "blend") images?.assertBlend(target.plan.images);
16510
+ invalidateMetadata();
15311
16511
  await applyMapStyle(options.map, styleInput, timeoutMs);
16512
+ invalidateMetadata();
15312
16513
  if (disposed) {
15313
16514
  cover?.cancel();
15314
16515
  return { status: "superseded", theme: targetTheme };
@@ -16283,6 +17484,7 @@ export {
16283
17484
  attachTileflowAtmosphere,
16284
17485
  attachTileflowFairUseNotice,
16285
17486
  attachTileflowMapLifecycle,
17487
+ createBoundaryFeatureState,
16286
17488
  createTileflowBuildings3D,
16287
17489
  createTileflowDepthMarkers,
16288
17490
  createTileflowGroundLayers,