diff --git a/docs/api-reference/core/map-controller.md b/docs/api-reference/core/map-controller.md
index 68480bab633..9fcf3b8cd05 100644
--- a/docs/api-reference/core/map-controller.md
+++ b/docs/api-reference/core/map-controller.md
@@ -66,6 +66,88 @@ new Deck({
See the `Controller` class [documentation](./controller.md#methods) for the methods that you can use and/or override.
+## MapState
+
+The `MapState` class manages the internal state of the map controller. It provides methods for handling user interactions like panning, rotating, and zooming.
+
+### Usage
+
+```js
+import {MapState} from '@deck.gl/core';
+
+// Create a MapState instance
+const mapState = new MapState({
+ width: 800,
+ height: 600,
+ latitude: 37.7749,
+ longitude: -122.4194,
+ zoom: 11,
+ bearing: 0,
+ pitch: 0,
+ makeViewport: (props) => new WebMercatorViewport(props)
+});
+
+// Use MapState methods
+const newState = mapState.pan({pos: [100, 100]});
+const zoomedState = mapState.zoom({pos: [400, 300], scale: 2});
+```
+
+### Methods
+
+MapState provides the following interaction methods:
+
+- `panStart({pos})` - Start a pan interaction
+- `pan({pos, startPos})` - Pan the map
+- `panEnd()` - End a pan interaction
+- `rotateStart({pos})` - Start a rotation interaction
+- `rotate({pos, deltaAngleX, deltaAngleY})` - Rotate the map
+- `rotateEnd()` - End a rotation interaction
+- `zoomStart({pos})` - Start a zoom interaction
+- `zoom({pos, startPos, scale})` - Zoom the map
+- `zoomEnd()` - End a zoom interaction
+- `zoomIn(speed)` - Zoom in
+- `zoomOut(speed)` - Zoom out
+- `moveLeft(speed)`, `moveRight(speed)`, `moveUp(speed)`, `moveDown(speed)` - Move the map
+- `rotateLeft(speed)`, `rotateRight(speed)`, `rotateUp(speed)`, `rotateDown(speed)` - Rotate the map
+
+## MapStateProps
+
+The `MapStateProps` type defines the properties for configuring map viewport state.
+
+### Properties
+
+- `width` (number) - The width of the viewport
+- `height` (number) - The height of the viewport
+- `latitude` (number) - The latitude at the center of the viewport
+- `longitude` (number) - The longitude at the center of the viewport
+- `zoom` (number) - The tile zoom level of the map
+- `bearing` (number, optional) - The bearing of the viewport in degrees. Default `0`
+- `pitch` (number, optional) - The pitch of the viewport in degrees. Default `0`
+- `altitude` (number, optional) - Specify the altitude of the viewport camera. Unit: map heights. Default `1.5`
+- `position` ([number, number, number], optional) - Viewport position. Default `[0, 0, 0]`
+- `maxZoom` (number, optional) - Maximum zoom level. Default `20`
+- `minZoom` (number, optional) - Minimum zoom level. Default `0`
+- `maxPitch` (number, optional) - Maximum pitch in degrees. Default `60`
+- `minPitch` (number, optional) - Minimum pitch in degrees. Default `0`
+- `normalize` (boolean, optional) - Normalize viewport props to fit map height into viewport. Default `true`
+
+### Usage
+
+```js
+import type {MapStateProps} from '@deck.gl/core';
+
+const viewState: MapStateProps = {
+ width: 800,
+ height: 600,
+ latitude: 37.7749,
+ longitude: -122.4194,
+ zoom: 11,
+ bearing: 0,
+ pitch: 45,
+ maxZoom: 18,
+ minZoom: 5
+};
+```
## Source
diff --git a/docs/api-reference/layers/column-layer.md b/docs/api-reference/layers/column-layer.md
index 02fac70dc4a..b7248bd9f8f 100644
--- a/docs/api-reference/layers/column-layer.md
+++ b/docs/api-reference/layers/column-layer.md
@@ -237,6 +237,19 @@ Whether to generate a line wireframe of the column. The outline will have
If `true`, the vertical surfaces of the columns use [flat shading](https://en.wikipedia.org/wiki/Shading#Flat_vs._smooth_shading).
If `false`, use smooth shading. Only effective if `extruded` is `true`.
+#### `capShape` (string, optional) {#capshape}
+
+* Default: `'flat'`
+
+The shape of the column's top cap. Available options:
+- `'flat'`: Flat top (default)
+- `'rounded'`: Dome-shaped top, like a silo
+- `'pointy'`: Cone-shaped top, like a missile
+
+Only effective if `extruded` is `true`.
+
+See the [cap shape example](https://github.com/visgl/deck.gl/tree/master/examples/get-started/pure-js/column-cap-shape) for an interactive demonstration.
+
#### `radiusUnits` (string, optional) {#radiusunits}
* Default: `'meters'`
diff --git a/examples/get-started/pure-js/column-cap-shape/README.md b/examples/get-started/pure-js/column-cap-shape/README.md
new file mode 100644
index 00000000000..d6f08b81a37
--- /dev/null
+++ b/examples/get-started/pure-js/column-cap-shape/README.md
@@ -0,0 +1,48 @@
+# ColumnLayer Cap Shape Example
+
+This example demonstrates the new `capShape` property for the ColumnLayer, which allows you to customize the top of columns.
+
+## Running this example
+
+To run this example, you need to install dependencies first:
+
+```bash
+# From the repository root
+cd examples/get-started/pure-js/column-cap-shape
+yarn
+yarn start
+```
+
+Or to use the local development version of deck.gl:
+
+```bash
+yarn start-local
+```
+
+## Features
+
+The example shows:
+- **Cap Shape Selection**: Switch between flat (default), rounded (dome), and pointy (cone) column tops
+- **Disk Resolution**: Adjust the number of sides (4-40) to see how it affects geometry smoothness
+- **Interactive Controls**: Real-time updates when changing settings
+- **3D Visualization**: Rotate and zoom to inspect columns from different angles
+
+## Usage
+
+```javascript
+new ColumnLayer({
+ data: myData,
+ extruded: true,
+ capShape: 'rounded', // 'flat' | 'rounded' | 'pointy'
+ diskResolution: 20,
+ getPosition: d => d.position,
+ getElevation: d => d.elevation,
+ getFillColor: d => d.color
+});
+```
+
+## Cap Shapes
+
+- **`flat`** (default): Traditional flat top - preserves original behavior
+- **`rounded`**: Dome-shaped top, like a silo - uses multiple latitude rings
+- **`pointy`**: Cone-shaped top, like a missile - converges to a single apex point
diff --git a/examples/get-started/pure-js/column-cap-shape/app.js b/examples/get-started/pure-js/column-cap-shape/app.js
new file mode 100644
index 00000000000..1e7945c0b89
--- /dev/null
+++ b/examples/get-started/pure-js/column-cap-shape/app.js
@@ -0,0 +1,73 @@
+// deck.gl
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {Deck} from '@deck.gl/core';
+import {ColumnLayer} from '@deck.gl/layers';
+
+// Generate sample data - grid of columns
+const data = [];
+for (let x = -2; x <= 2; x++) {
+ for (let y = -2; y <= 2; y++) {
+ data.push({
+ position: [-122.4 + x * 0.01, 37.8 + y * 0.01],
+ elevation: Math.random() * 3000 + 1000,
+ color: [
+ Math.floor(Math.random() * 200) + 55,
+ Math.floor(Math.random() * 200) + 55,
+ Math.floor(Math.random() * 200) + 55,
+ 255
+ ]
+ });
+ }
+}
+
+const INITIAL_VIEW_STATE = {
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 13,
+ pitch: 45,
+ bearing: 0
+};
+
+function createLayer(capShape, diskResolution) {
+ return new ColumnLayer({
+ id: 'column-layer',
+ data: data,
+ diskResolution: diskResolution,
+ radius: 200,
+ extruded: true,
+ capShape: capShape, // NEW: 'flat', 'rounded', or 'pointy'
+ elevationScale: 1,
+ getPosition: d => d.position,
+ getFillColor: d => d.color,
+ getElevation: d => d.elevation,
+ pickable: true
+ });
+}
+
+// Initialize deck.gl
+const deckgl = new Deck({
+ initialViewState: INITIAL_VIEW_STATE,
+ controller: true,
+ layers: [createLayer('flat', 20)],
+ getTooltip: ({object}) => object && `Elevation: ${object.elevation.toFixed(0)}m`
+});
+
+// Set up controls
+const capShapeSelect = document.getElementById('capShape');
+const diskResolutionSlider = document.getElementById('diskResolution');
+const diskResValueSpan = document.getElementById('diskResValue');
+
+function updateLayers() {
+ const capShape = capShapeSelect.value;
+ const diskResolution = parseInt(diskResolutionSlider.value);
+ diskResValueSpan.textContent = diskResolution;
+
+ deckgl.setProps({
+ layers: [createLayer(capShape, diskResolution)]
+ });
+}
+
+capShapeSelect.addEventListener('change', updateLayers);
+diskResolutionSlider.addEventListener('input', updateLayers);
diff --git a/examples/get-started/pure-js/column-cap-shape/index.html b/examples/get-started/pure-js/column-cap-shape/index.html
new file mode 100644
index 00000000000..fd2492c9882
--- /dev/null
+++ b/examples/get-started/pure-js/column-cap-shape/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+ deck.gl ColumnLayer Cap Shape Example
+
+
+
+
+
ColumnLayer Cap Shape
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/get-started/pure-js/column-cap-shape/package.json b/examples/get-started/pure-js/column-cap-shape/package.json
new file mode 100644
index 00000000000..19ec090f190
--- /dev/null
+++ b/examples/get-started/pure-js/column-cap-shape/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "deckgl-example-column-cap-shape",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "scripts": {
+ "start": "vite --open",
+ "start-local": "vite --config ../../../vite.config.local.mjs",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@deck.gl/core": "^9.0.0",
+ "@deck.gl/layers": "^9.0.0"
+ },
+ "devDependencies": {
+ "vite": "^4.0.0"
+ }
+}
diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts
index ef4463aab19..244ac4c40d6 100644
--- a/modules/core/src/index.ts
+++ b/modules/core/src/index.ts
@@ -64,7 +64,7 @@ export {default as _GlobeView} from './views/globe-view';
// Controllers
export {default as Controller} from './controllers/controller';
-export {default as MapController} from './controllers/map-controller';
+export {default as MapController, MapState} from './controllers/map-controller';
export {default as _GlobeController} from './controllers/globe-controller';
export {default as FirstPersonController} from './controllers/first-person-controller';
export {default as OrbitController} from './controllers/orbit-controller';
@@ -143,6 +143,7 @@ export type {
ViewStateChangeParameters,
InteractionState
} from './controllers/controller';
+export type {MapStateProps} from './controllers/map-controller';
// INTERNAL, DO NOT USE
// @deprecated internal do not use
diff --git a/modules/layers/src/column-layer/column-geometry.ts b/modules/layers/src/column-layer/column-geometry.ts
index e48a918744f..efe2d319d99 100644
--- a/modules/layers/src/column-layer/column-geometry.ts
+++ b/modules/layers/src/column-layer/column-geometry.ts
@@ -13,6 +13,7 @@ type ColumnGeometryProps = {
height?: number;
nradial?: number;
vertices?: number[];
+ capShape?: 'flat' | 'rounded' | 'pointy';
};
export default class ColumnGeometry extends Geometry {
@@ -32,7 +33,7 @@ function tesselateColumn(props: ColumnGeometryProps): {
indices: Uint16Array;
attributes: Record;
} {
- const {radius, height = 1, nradial = 10} = props;
+ const {radius, height = 1, nradial = 10, capShape = 'flat'} = props;
let {vertices} = props;
if (vertices) {
@@ -43,9 +44,29 @@ function tesselateColumn(props: ColumnGeometryProps): {
const isExtruded = height > 0;
const vertsAroundEdge = nradial + 1; // loop
- const numVertices = isExtruded
- ? vertsAroundEdge * 3 + 1 // top, side top edge, side bottom edge, one additional degenerage vertex
- : nradial; // top
+
+ // Calculate number of vertices based on cap shape
+ let numVertices: number;
+
+ if (!isExtruded) {
+ numVertices = nradial; // flat disk
+ } else if (capShape === 'pointy') {
+ // Cone cap: alternating pattern of edge vertex and apex vertex in triangle strip
+ // For each edge around the top (nradial+1 to close the loop): edge vertex + apex vertex
+ // +1 for transition degenerate vertex
+ numVertices = vertsAroundEdge * 2 + 1 + 1 + vertsAroundEdge * 2; // sides + degenerate + transition + cone vertices
+ } else if (capShape === 'rounded') {
+ // Dome cap: multiple latitude rings from base to top, rendered as triangle strips
+ // Each level needs 2 vertices per position (current ring + next ring) for triangle strip
+ // Use ~25% of nradial as number of latitude rings for good balance of quality/performance
+ const domeLevels = Math.max(3, Math.floor(nradial / 4)); // number of latitude rings for dome
+ // Each level needs vertsAroundEdge * 2 vertices for triangle strip
+ // +1 for transition degenerate vertex
+ numVertices = vertsAroundEdge * 2 + 1 + 1 + (domeLevels * vertsAroundEdge * 2 + 1); // sides + degenerate + transition + dome + apex
+ } else {
+ // flat top (original behavior)
+ numVertices = vertsAroundEdge * 3 + 1; // top, side top edge, side bottom edge, one additional degenerate vertex
+ }
const stepAngle = (Math.PI * 2) / nradial;
@@ -89,34 +110,186 @@ function tesselateColumn(props: ColumnGeometryProps): {
i += 3;
}
- // The column geometry is rendered as a triangle strip, so
- // in order to render sides and top in one go we need to use degenerate triangles.
- // Duplicate last vertex of side trinagles and first vertex of the top cap to preserve winding order.
-
- // top tesselation: 0, -1, 1, -2, 2, -3, 3, ...
- //
- // 0 -- 1
- // / \
- // -1 2
- // | |
- // -2 3
- // \ /
- // -3 -- 4
- //
- for (let j = isExtruded ? 0 : 1; j < vertsAroundEdge; j++) {
- const v = Math.floor(j / 2) * Math.sign(0.5 - (j % 2));
- const a = v * stepAngle;
- const vertexIndex = (v + nradial) % nradial;
- const sin = Math.sin(a);
- const cos = Math.cos(a);
-
- positions[i + 0] = vertices ? vertices[vertexIndex * 2] : cos * radius;
- positions[i + 1] = vertices ? vertices[vertexIndex * 2 + 1] : sin * radius;
+ // Generate top cap based on capShape
+ if (isExtruded && capShape === 'pointy') {
+ // Create cone using triangle strip: edge, apex, edge, apex, ...
+ const apexHeight = height / 2 + radius; // Cone extends radius distance above cylinder top
+ const apex = [0, 0, apexHeight];
+
+ // Calculate cone normal (points outward and upward at angle determined by geometry)
+ // Using right triangle: base = radius, height = radius, hypotenuse = slant
+ const slantHeight = Math.sqrt(radius * radius + radius * radius);
+
+ // Add first vertex of cone cap as degenerate to transition from sides
+ const firstVertexIndex = 0;
+ const firstSin = Math.sin(0);
+ const firstCos = Math.cos(0);
+ positions[i + 0] = vertices ? vertices[firstVertexIndex * 2] : firstCos * radius;
+ positions[i + 1] = vertices ? vertices[firstVertexIndex * 2 + 1] : firstSin * radius;
positions[i + 2] = height / 2;
-
+ if (vertices) {
+ const vertexDist = Math.sqrt(
+ vertices[firstVertexIndex * 2] * vertices[firstVertexIndex * 2] +
+ vertices[firstVertexIndex * 2 + 1] * vertices[firstVertexIndex * 2 + 1]
+ );
+ const customSlantHeight = Math.sqrt(vertexDist * vertexDist + radius * radius);
+ normals[i + 0] = vertices[firstVertexIndex * 2] * vertexDist / customSlantHeight;
+ normals[i + 1] = vertices[firstVertexIndex * 2 + 1] * vertexDist / customSlantHeight;
+ normals[i + 2] = radius / customSlantHeight;
+ } else {
+ normals[i + 0] = firstCos * radius / slantHeight;
+ normals[i + 1] = firstSin * radius / slantHeight;
+ normals[i + 2] = radius / slantHeight;
+ }
+ i += 3;
+
+ for (let j = 0; j < vertsAroundEdge; j++) {
+ const a = j * stepAngle;
+ const vertexIndex = j % nradial;
+ const sin = Math.sin(a);
+ const cos = Math.cos(a);
+
+ // Edge vertex
+ positions[i + 0] = vertices ? vertices[vertexIndex * 2] : cos * radius;
+ positions[i + 1] = vertices ? vertices[vertexIndex * 2 + 1] : sin * radius;
+ positions[i + 2] = height / 2;
+
+ // Normal for cone: outward component proportional to radius, upward component proportional to radius
+ // For custom vertices, normalize the vertex position itself; for circular, use cos/sin
+ if (vertices) {
+ const vertexDist = Math.sqrt(
+ vertices[vertexIndex * 2] * vertices[vertexIndex * 2] +
+ vertices[vertexIndex * 2 + 1] * vertices[vertexIndex * 2 + 1]
+ );
+ const customSlantHeight = Math.sqrt(vertexDist * vertexDist + radius * radius);
+ normals[i + 0] = vertices[vertexIndex * 2] * vertexDist / customSlantHeight;
+ normals[i + 1] = vertices[vertexIndex * 2 + 1] * vertexDist / customSlantHeight;
+ normals[i + 2] = radius / customSlantHeight;
+ } else {
+ normals[i + 0] = cos * radius / slantHeight;
+ normals[i + 1] = sin * radius / slantHeight;
+ normals[i + 2] = radius / slantHeight;
+ }
+
+ i += 3;
+
+ // Apex vertex
+ positions[i + 0] = apex[0];
+ positions[i + 1] = apex[1];
+ positions[i + 2] = apex[2];
+
+ // Apex uses same normal as the edge vertex it's paired with
+ if (vertices) {
+ const vertexDist = Math.sqrt(
+ vertices[vertexIndex * 2] * vertices[vertexIndex * 2] +
+ vertices[vertexIndex * 2 + 1] * vertices[vertexIndex * 2 + 1]
+ );
+ const customSlantHeight = Math.sqrt(vertexDist * vertexDist + radius * radius);
+ normals[i + 0] = vertices[vertexIndex * 2] * vertexDist / customSlantHeight;
+ normals[i + 1] = vertices[vertexIndex * 2 + 1] * vertexDist / customSlantHeight;
+ normals[i + 2] = radius / customSlantHeight;
+ } else {
+ normals[i + 0] = cos * radius / slantHeight;
+ normals[i + 1] = sin * radius / slantHeight;
+ normals[i + 2] = radius / slantHeight;
+ }
+
+ i += 3;
+ }
+ } else if (isExtruded && capShape === 'rounded') {
+ // Generate dome using triangle strips between latitude rings
+ const domeLevels = Math.max(3, Math.floor(nradial / 4));
+
+ // Add first vertex of dome as degenerate to transition from sides
+ const firstTheta = 0;
+ const firstSin = Math.sin(firstTheta);
+ const firstCos = Math.cos(firstTheta);
+ const firstPhi = 0;
+ const firstR = Math.cos(firstPhi) * radius;
+ const firstZ = height / 2 + Math.sin(firstPhi) * radius;
+ positions[i + 0] = firstCos * firstR;
+ positions[i + 1] = firstSin * firstR;
+ positions[i + 2] = firstZ;
+ normals[i + 0] = firstCos * Math.cos(firstPhi);
+ normals[i + 1] = firstSin * Math.cos(firstPhi);
+ normals[i + 2] = Math.sin(firstPhi);
+ i += 3;
+
+ // Start from the base (top edge of cylinder)
+ for (let level = 0; level < domeLevels; level++) {
+ const phi1 = (Math.PI / 2) * level / domeLevels; // current latitude
+ const phi2 = (Math.PI / 2) * (level + 1) / domeLevels; // next latitude
+
+ const r1 = Math.cos(phi1) * radius;
+ const z1 = height / 2 + Math.sin(phi1) * radius;
+ const r2 = Math.cos(phi2) * radius;
+ const z2 = height / 2 + Math.sin(phi2) * radius;
+
+ for (let j = 0; j < vertsAroundEdge; j++) {
+ const theta = (j % nradial) * stepAngle;
+ const sin = Math.sin(theta);
+ const cos = Math.cos(theta);
+
+ // Lower ring vertex
+ positions[i + 0] = cos * r1;
+ positions[i + 1] = sin * r1;
+ positions[i + 2] = z1;
+ normals[i + 0] = cos * Math.cos(phi1);
+ normals[i + 1] = sin * Math.cos(phi1);
+ normals[i + 2] = Math.sin(phi1);
+ i += 3;
+
+ // Upper ring vertex
+ positions[i + 0] = cos * r2;
+ positions[i + 1] = sin * r2;
+ positions[i + 2] = z2;
+ normals[i + 0] = cos * Math.cos(phi2);
+ normals[i + 1] = sin * Math.cos(phi2);
+ normals[i + 2] = Math.sin(phi2);
+ i += 3;
+ }
+ }
+
+ // Add final apex point (degenerate to close the dome)
+ positions[i + 0] = 0;
+ positions[i + 1] = 0;
+ positions[i + 2] = height / 2 + radius;
+ normals[i + 0] = 0;
+ normals[i + 1] = 0;
normals[i + 2] = 1;
-
i += 3;
+ } else {
+ // The column geometry is rendered as a triangle strip, so
+ // in order to render sides and top in one go we need to use degenerate triangles.
+ // Duplicate last vertex of side triangles and first vertex of the top cap to preserve winding order.
+
+ // top tesselation: 0, -1, 1, -2, 2, -3, 3, ...
+ //
+ // 0 -- 1
+ // / \
+ // -1 2
+ // | |
+ // -2 3
+ // \ /
+ // -3 -- 4
+ //
+ for (let j = isExtruded ? 0 : 1; j < vertsAroundEdge; j++) {
+ const v = Math.floor(j / 2) * Math.sign(0.5 - (j % 2));
+ const a = v * stepAngle;
+ const vertexIndex = (v + nradial) % nradial;
+ const sin = Math.sin(a);
+ const cos = Math.cos(a);
+
+ positions[i + 0] = vertices ? vertices[vertexIndex * 2] : cos * radius;
+ positions[i + 1] = vertices ? vertices[vertexIndex * 2 + 1] : sin * radius;
+ positions[i + 2] = height / 2;
+
+ normals[i + 0] = 0;
+ normals[i + 1] = 0;
+ normals[i + 2] = 1;
+
+ i += 3;
+ }
}
if (isExtruded) {
diff --git a/modules/layers/src/column-layer/column-layer.ts b/modules/layers/src/column-layer/column-layer.ts
index 038d3b1de64..e0cffb5e3d0 100644
--- a/modules/layers/src/column-layer/column-layer.ts
+++ b/modules/layers/src/column-layer/column-layer.ts
@@ -47,6 +47,7 @@ const defaultProps: DefaultProps = {
filled: true,
stroked: false,
flatShading: false,
+ capShape: {type: 'string', value: 'flat', compare: true},
getPosition: {type: 'accessor', value: (x: any) => x.position},
getFillColor: {type: 'accessor', value: DEFAULT_COLOR},
@@ -135,6 +136,12 @@ type _ColumnLayerProps = {
*/
flatShading?: boolean;
+ /**
+ * The shape of the column's top cap. Options: 'flat' (default), 'rounded' (dome-like), or 'pointy' (cone-like).
+ * @default 'flat'
+ */
+ capShape?: 'flat' | 'rounded' | 'pointy';
+
/**
* The units of the radius.
* @default 'meters'
@@ -305,18 +312,20 @@ export default class ColumnLayer exten
regenerateModels ||
props.diskResolution !== oldProps.diskResolution ||
props.vertices !== oldProps.vertices ||
+ props.capShape !== oldProps.capShape ||
(props.extruded || props.stroked) !== (oldProps.extruded || oldProps.stroked)
) {
this._updateGeometry(props);
}
}
- getGeometry(diskResolution: number, vertices: number[] | undefined, hasThinkness: boolean) {
+ getGeometry(diskResolution: number, vertices: number[] | undefined, hasThinkness: boolean, capShape: 'flat' | 'rounded' | 'pointy') {
const geometry = new ColumnGeometry({
radius: 1,
height: hasThinkness ? 2 : 0,
vertices,
- nradial: diskResolution
+ nradial: diskResolution,
+ capShape
});
let meanVertexDistance = 0;
@@ -360,8 +369,8 @@ export default class ColumnLayer exten
};
}
- protected _updateGeometry({diskResolution, vertices, extruded, stroked}) {
- const geometry = this.getGeometry(diskResolution, vertices, extruded || stroked);
+ protected _updateGeometry({diskResolution, vertices, extruded, stroked, capShape}) {
+ const geometry = this.getGeometry(diskResolution, vertices, extruded || stroked, capShape);
this.setState({
fillVertexCount: geometry.attributes.POSITION.value.length / 3
diff --git a/test/modules/layers/column-geometry.spec.ts b/test/modules/layers/column-geometry.spec.ts
index 8a34ba92714..36e8ee3e366 100644
--- a/test/modules/layers/column-geometry.spec.ts
+++ b/test/modules/layers/column-geometry.spec.ts
@@ -96,3 +96,41 @@ test('ColumnGeometry#tesselation', t => {
t.end();
});
+
+test('ColumnGeometry#capShape', t => {
+ const nradial = 4;
+ const vertsAroundEdge = nradial + 1;
+
+ t.comment('Flat cap shape');
+ let geometry = new ColumnGeometry({radius: 1, height: 1, nradial, capShape: 'flat'});
+ let attributes = geometry.getAttributes();
+
+ t.ok(ArrayBuffer.isView(attributes.POSITION.value), 'flat cap positions generated');
+ t.ok(ArrayBuffer.isView(attributes.NORMAL.value), 'flat cap normals generated');
+ // Flat cap: sides (vertsAroundEdge * 2) + degenerate (1) + top cap (vertsAroundEdge)
+ t.is(attributes.POSITION.value.length, (vertsAroundEdge * 3 + 1) * 3, 'flat cap POSITION has correct size');
+
+ t.comment('Pointy cap shape');
+ geometry = new ColumnGeometry({radius: 1, height: 1, nradial, capShape: 'pointy'});
+ attributes = geometry.getAttributes();
+
+ t.ok(ArrayBuffer.isView(attributes.POSITION.value), 'pointy cap positions generated');
+ t.ok(ArrayBuffer.isView(attributes.NORMAL.value), 'pointy cap normals generated');
+ t.ok(attributes.POSITION.value.length > 0, 'pointy cap has vertices');
+
+ // Check that there's a point at the top center
+ const positions = attributes.POSITION.value;
+ const lastVertex = [positions[positions.length - 3], positions[positions.length - 2], positions[positions.length - 1]];
+ t.ok(Math.abs(lastVertex[0]) < 0.01 && Math.abs(lastVertex[1]) < 0.01, 'pointy cap has center point');
+ t.ok(lastVertex[2] > 0, 'pointy cap center point is at top');
+
+ t.comment('Rounded cap shape');
+ geometry = new ColumnGeometry({radius: 1, height: 1, nradial, capShape: 'rounded'});
+ attributes = geometry.getAttributes();
+
+ t.ok(ArrayBuffer.isView(attributes.POSITION.value), 'rounded cap positions generated');
+ t.ok(ArrayBuffer.isView(attributes.NORMAL.value), 'rounded cap normals generated');
+ t.ok(attributes.POSITION.value.length > (vertsAroundEdge * 3 + 1) * 3, 'rounded cap has more vertices than flat');
+
+ t.end();
+});
diff --git a/test/render/test-cases/column-layer.ts b/test/render/test-cases/column-layer.ts
index 1498f0f3705..09bcfe79bbc 100644
--- a/test/render/test-cases/column-layer.ts
+++ b/test/render/test-cases/column-layer.ts
@@ -165,5 +165,25 @@ export default [
vertices: polygonCCW,
getElevation: h => h.value * 5000
}
+ ),
+ genColumnLayerTestCase(
+ {
+ name: 'column-lnglat-extruded-pointy'
+ },
+ {
+ extruded: true,
+ capShape: 'pointy',
+ getElevation: h => h.value * 5000
+ }
+ ),
+ genColumnLayerTestCase(
+ {
+ name: 'column-lnglat-extruded-rounded'
+ },
+ {
+ extruded: true,
+ capShape: 'rounded',
+ getElevation: h => h.value * 5000
+ }
)
];