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 + } ) ];