kopia lustrzana https://github.com/robhawkes/vizicities
Added static methods for points
rodzic
49c46dd8d1
commit
e4d2db0388
|
@ -185,6 +185,12 @@ class GeoJSONLayer extends LayerGroup {
|
|||
var polylineFlat = true;
|
||||
|
||||
var pointAttributes = [];
|
||||
var pointAttributeLengths = {
|
||||
positions: 3,
|
||||
normals: 3,
|
||||
colors: 3
|
||||
};
|
||||
var pointFlat = true;
|
||||
|
||||
this._layers.forEach(layer => {
|
||||
if (layer instanceof PolygonLayer) {
|
||||
|
@ -209,6 +215,14 @@ class GeoJSONLayer extends LayerGroup {
|
|||
}
|
||||
} else if (layer instanceof PointLayer) {
|
||||
pointAttributes.push(layer.getBufferAttributes());
|
||||
|
||||
if (pointFlat && !layer.isFlat()) {
|
||||
pointFlat = false;
|
||||
}
|
||||
|
||||
if (this._options.interactive) {
|
||||
pointAttributeLengths.pickingIds = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -238,8 +252,14 @@ class GeoJSONLayer extends LayerGroup {
|
|||
|
||||
if (pointAttributes.length > 0) {
|
||||
var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes);
|
||||
this._setPointMesh(mergedPointAttributes);
|
||||
this.add(this._pointMesh);
|
||||
this._setPointMesh(mergedPointAttributes, pointAttributeLengths, pointFlat).then((result) => {
|
||||
this._pointMesh = result.mesh;
|
||||
this.add(this._pointMesh);
|
||||
|
||||
if (result.pickingMesh) {
|
||||
this._pickingMesh.add(result.pickingMesh);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up layers
|
||||
|
@ -275,60 +295,12 @@ class GeoJSONLayer extends LayerGroup {
|
|||
return PolylineLayer.SetMesh(attributes, attributeLengths, flat, style, this._options);
|
||||
}
|
||||
|
||||
_setPointMesh(attributes) {
|
||||
var geometry = new THREE.BufferGeometry();
|
||||
_setPointMesh(attributes, attributeLengths, flat) {
|
||||
// TODO: Make this work when style is a function per feature
|
||||
var style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style;
|
||||
style = extend({}, GeoJSON.defaultStyle, style);
|
||||
|
||||
// itemSize = 3 because there are 3 values (components) per vertex
|
||||
geometry.addAttribute('position', new THREE.BufferAttribute(attributes.vertices, 3));
|
||||
geometry.addAttribute('normal', new THREE.BufferAttribute(attributes.normals, 3));
|
||||
geometry.addAttribute('color', new THREE.BufferAttribute(attributes.colours, 3));
|
||||
|
||||
if (attributes.pickingIds) {
|
||||
geometry.addAttribute('pickingId', new THREE.BufferAttribute(attributes.pickingIds, 1));
|
||||
}
|
||||
|
||||
geometry.computeBoundingBox();
|
||||
|
||||
var material;
|
||||
if (this._options.pointMaterial && this._options.pointMaterial instanceof THREE.Material) {
|
||||
material = this._options.pointMaterial;
|
||||
} else if (!this._world._environment._skybox) {
|
||||
material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: THREE.VertexColors
|
||||
// side: THREE.BackSide
|
||||
});
|
||||
} else {
|
||||
material = new THREE.MeshStandardMaterial({
|
||||
vertexColors: THREE.VertexColors
|
||||
// side: THREE.BackSide
|
||||
});
|
||||
material.roughness = 1;
|
||||
material.metalness = 0.1;
|
||||
material.envMapIntensity = 3;
|
||||
material.envMap = this._world._environment._skybox.getRenderTarget();
|
||||
}
|
||||
|
||||
var mesh;
|
||||
|
||||
// Pass mesh callback, if defined
|
||||
if (typeof this._options.onPointMesh === 'function') {
|
||||
mesh = this._options.onPointMesh(geometry, material);
|
||||
} else {
|
||||
mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.castShadow = true;
|
||||
// mesh.receiveShadow = true;
|
||||
}
|
||||
|
||||
if (this._options.interactive && this._pickingMesh) {
|
||||
material = new PickingMaterial();
|
||||
// material.side = THREE.BackSide;
|
||||
|
||||
var pickingMesh = new THREE.Mesh(geometry, material);
|
||||
this._pickingMesh.add(pickingMesh);
|
||||
}
|
||||
|
||||
this._pointMesh = mesh;
|
||||
return PointLayer.SetMesh(attributes, attributeLengths, flat, style, this._options);
|
||||
}
|
||||
|
||||
// TODO: Support all GeoJSON geometry types
|
||||
|
@ -343,11 +315,11 @@ class GeoJSONLayer extends LayerGroup {
|
|||
if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
|
||||
// Get material instance to use for polygon, if provided
|
||||
if (typeof this._options.polygonMaterial === 'function') {
|
||||
options.geometry = this._options.polygonMaterial(feature);
|
||||
options.polygonMaterial = this._options.polygonMaterial(feature);
|
||||
}
|
||||
|
||||
if (typeof this._options.onPolygonMesh === 'function') {
|
||||
options.onMesh = this._options.onPolygonMesh;
|
||||
options.onPolygonMesh = this._options.onPolygonMesh;
|
||||
}
|
||||
|
||||
// Pass onBufferAttributes callback, if defined
|
||||
|
@ -361,11 +333,11 @@ class GeoJSONLayer extends LayerGroup {
|
|||
if (geometry.type === 'LineString' || geometry.type === 'MultiLineString') {
|
||||
// Get material instance to use for line, if provided
|
||||
if (typeof this._options.lineMaterial === 'function') {
|
||||
options.geometry = this._options.lineMaterial(feature);
|
||||
options.lineMaterial = this._options.lineMaterial(feature);
|
||||
}
|
||||
|
||||
if (typeof this._options.onPolylineMesh === 'function') {
|
||||
options.onMesh = this._options.onPolylineMesh;
|
||||
options.onPolylineMesh = this._options.onPolylineMesh;
|
||||
}
|
||||
|
||||
// Pass onBufferAttributes callback, if defined
|
||||
|
@ -384,11 +356,11 @@ class GeoJSONLayer extends LayerGroup {
|
|||
|
||||
// Get material instance to use for point, if provided
|
||||
if (typeof this._options.pointMaterial === 'function') {
|
||||
options.geometry = this._options.pointMaterial(feature);
|
||||
options.pointMaterial = this._options.pointMaterial(feature);
|
||||
}
|
||||
|
||||
if (typeof this._options.onPointMesh === 'function') {
|
||||
options.onMesh = this._options.onPointMesh;
|
||||
options.onPointMesh = this._options.onPointMesh;
|
||||
}
|
||||
|
||||
return new PointLayer(coordinates, options);
|
||||
|
|
|
@ -25,10 +25,12 @@
|
|||
import Layer from '../Layer';
|
||||
import extend from 'lodash.assign';
|
||||
import THREE from 'three';
|
||||
import Geo from '../../geo/Geo';
|
||||
import {latLon as LatLon} from '../../geo/LatLon';
|
||||
import {point as Point} from '../../geo/Point';
|
||||
import PickingMaterial from '../../engine/PickingMaterial';
|
||||
import Buffer from '../../util/Buffer';
|
||||
import PolygonLayer from './PolygonLayer';
|
||||
|
||||
class PointLayer extends Layer {
|
||||
constructor(coordinates, options) {
|
||||
|
@ -40,8 +42,8 @@ class PointLayer extends Layer {
|
|||
// Custom material override
|
||||
//
|
||||
// TODO: Should this be in the style object?
|
||||
material: null,
|
||||
onMesh: null,
|
||||
pointMaterial: null,
|
||||
onPointMesh: null,
|
||||
// This default style is separate to Util.GeoJSON.defaultStyle
|
||||
style: {
|
||||
pointColor: '#ff0000'
|
||||
|
@ -57,42 +59,63 @@ class PointLayer extends Layer {
|
|||
// single point in the array)
|
||||
this._coordinates = (PointLayer.isSingle(coordinates)) ? [coordinates] : coordinates;
|
||||
|
||||
// Point features are always flat (for now at least)
|
||||
//
|
||||
// This won't always be the case once custom point objects / meshes are
|
||||
// added
|
||||
this._flat = true;
|
||||
this._flat = false;
|
||||
}
|
||||
|
||||
_onAdd(world) {
|
||||
this._setCoordinates();
|
||||
return new Promise((resolve, reject) => {
|
||||
this._setCoordinates();
|
||||
|
||||
if (this._options.interactive) {
|
||||
// Only add to picking mesh if this layer is controlling output
|
||||
//
|
||||
// Otherwise, assume another component will eventually add a mesh to
|
||||
// the picking scene
|
||||
if (this.isOutput()) {
|
||||
this._pickingMesh = new THREE.Object3D();
|
||||
this.addToPicking(this._pickingMesh);
|
||||
if (this._options.interactive) {
|
||||
// Only add to picking mesh if this layer is controlling output
|
||||
//
|
||||
// Otherwise, assume another component will eventually add a mesh to
|
||||
// the picking scene
|
||||
if (this.isOutput()) {
|
||||
this._pickingMesh = new THREE.Object3D();
|
||||
this.addToPicking(this._pickingMesh);
|
||||
}
|
||||
|
||||
this._setPickingId();
|
||||
this._addPickingEvents();
|
||||
}
|
||||
|
||||
this._setPickingId();
|
||||
this._addPickingEvents();
|
||||
}
|
||||
// Store geometry representation as instances of THREE.BufferAttribute
|
||||
PointLayer.SetBufferAttributes(this._projectedCoordinates, this._options).then((result) => {
|
||||
this._bufferAttributes = Buffer.mergeAttributes(result.attributes);
|
||||
this._flat = result.flat;
|
||||
|
||||
// Store geometry representation as instances of THREE.BufferAttribute
|
||||
this._setBufferAttributes();
|
||||
var attributeLengths = {
|
||||
positions: 3,
|
||||
normals: 3,
|
||||
colors: 3
|
||||
};
|
||||
|
||||
if (this.isOutput()) {
|
||||
// Set mesh if not merging elsewhere
|
||||
this._setMesh(this._bufferAttributes);
|
||||
if (this._options.interactive) {
|
||||
attributeLengths.pickingIds = 1;
|
||||
}
|
||||
|
||||
// Output mesh
|
||||
this.add(this._mesh);
|
||||
}
|
||||
if (this.isOutput()) {
|
||||
var style = this._options.style;
|
||||
|
||||
return Promise.resolve(this);
|
||||
// Set mesh if not merging elsewhere
|
||||
// TODO: Dedupe with PolygonLayer as they are identical
|
||||
PointLayer.SetMesh(this._bufferAttributes, attributeLengths, this._flat, style, this._options, this._world._environment._skybox).then((result) => {
|
||||
// Output mesh
|
||||
this.add(result.mesh);
|
||||
|
||||
if (result.pickingMesh) {
|
||||
this._pickingMesh.add(result.pickingMesh);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
result.attributes = null;
|
||||
result = null;
|
||||
|
||||
resolve(this);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Return center of point as a LatLon
|
||||
|
@ -123,87 +146,78 @@ class PointLayer extends Layer {
|
|||
});
|
||||
}
|
||||
|
||||
// Create and store reference to THREE.BufferAttribute data for this layer
|
||||
_setBufferAttributes() {
|
||||
var height = 0;
|
||||
static SetBufferAttributes(coordinates, options) {
|
||||
return new Promise((resolve) => {
|
||||
var height = 0;
|
||||
|
||||
// Convert height into world units
|
||||
if (this._options.style.pointHeight) {
|
||||
height = this._world.metresToWorld(this._options.style.pointHeight, this._pointScale);
|
||||
}
|
||||
// Convert height into world units
|
||||
if (options.style.pointHeight) {
|
||||
height = Geo.metresToWorld(options.style.pointHeight, options.pointScale);
|
||||
}
|
||||
|
||||
var colour = new THREE.Color();
|
||||
colour.set(this._options.style.pointColor);
|
||||
var colour = new THREE.Color();
|
||||
colour.set(options.style.pointColor);
|
||||
|
||||
var geometry;
|
||||
// Use default geometry if none has been provided or the provided geometry
|
||||
// isn't valid
|
||||
if (!options.geometry || (!options.geometry instanceof THREE.Geometry || !options.geometry instanceof THREE.BufferGeometry)) {
|
||||
// Debug geometry for points is a thin bar
|
||||
//
|
||||
// TODO: Allow point geometry to be customised / overridden
|
||||
var geometryWidth = Geo.metresToWorld(25, this._pointScale);
|
||||
var geometryHeight = Geo.metresToWorld(200, this._pointScale);
|
||||
var _geometry = new THREE.BoxGeometry(geometryWidth, geometryHeight, geometryWidth);
|
||||
|
||||
// Use default geometry if none has been provided or the provided geometry
|
||||
// isn't valid
|
||||
if (!this._options.geometry || (!this._options.geometry instanceof THREE.Geometry || !this._options.geometry instanceof THREE.BufferGeometry)) {
|
||||
// Debug geometry for points is a thin bar
|
||||
//
|
||||
// TODO: Allow point geometry to be customised / overridden
|
||||
var geometryWidth = this._world.metresToWorld(25, this._pointScale);
|
||||
var geometryHeight = this._world.metresToWorld(200, this._pointScale);
|
||||
var _geometry = new THREE.BoxGeometry(geometryWidth, geometryHeight, geometryWidth);
|
||||
// Shift geometry up so it sits on the ground
|
||||
_geometry.translate(0, geometryHeight * 0.5, 0);
|
||||
|
||||
// Shift geometry up so it sits on the ground
|
||||
_geometry.translate(0, geometryHeight * 0.5, 0);
|
||||
|
||||
// Pull attributes out of debug geometry
|
||||
geometry = new THREE.BufferGeometry().fromGeometry(_geometry);
|
||||
} else {
|
||||
if (this._options.geometry instanceof THREE.BufferGeometry) {
|
||||
geometry = this._options.geometry;
|
||||
// Pull attributes out of debug geometry
|
||||
geometry = new THREE.BufferGeometry().fromGeometry(_geometry);
|
||||
} else {
|
||||
geometry = new THREE.BufferGeometry().fromGeometry(this._options.geometry);
|
||||
}
|
||||
}
|
||||
|
||||
// For each point
|
||||
var attributes = this._projectedCoordinates.map(coordinate => {
|
||||
var _vertices = [];
|
||||
var _normals = [];
|
||||
var _colours = [];
|
||||
|
||||
var _geometry = geometry.clone();
|
||||
|
||||
_geometry.translate(coordinate.x, height, coordinate.y);
|
||||
|
||||
var _vertices = _geometry.attributes.position.clone().array;
|
||||
var _normals = _geometry.attributes.normal.clone().array;
|
||||
var _colours = _geometry.attributes.color.clone().array;
|
||||
|
||||
for (var i = 0; i < _colours.length; i += 3) {
|
||||
_colours[i] = colour.r;
|
||||
_colours[i + 1] = colour.g;
|
||||
_colours[i + 2] = colour.b;
|
||||
}
|
||||
|
||||
var _point = {
|
||||
vertices: _vertices,
|
||||
normals: _normals,
|
||||
colours: _colours
|
||||
};
|
||||
|
||||
if (this._options.interactive && this._pickingId) {
|
||||
// Inject picking ID
|
||||
// point.pickingId = this._pickingId;
|
||||
_point.pickingIds = new Float32Array(_vertices.length / 3);
|
||||
for (var i = 0; i < _point.pickingIds.length; i++) {
|
||||
_point.pickingIds[i] = this._pickingId;
|
||||
if (options.geometry instanceof THREE.BufferGeometry) {
|
||||
geometry = options.geometry;
|
||||
} else {
|
||||
geometry = new THREE.BufferGeometry().fromGeometry(options.geometry);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert point representation to proper attribute arrays
|
||||
// return this._toAttributes(_point);
|
||||
return _point;
|
||||
var attributes = coordinates.map((coordinate) => {
|
||||
var _vertices = [];
|
||||
var _normals = [];
|
||||
var _colours = [];
|
||||
|
||||
var _geometry = geometry.clone();
|
||||
_geometry.translate(coordinate.x, height, coordinate.y);
|
||||
|
||||
var _vertices = _geometry.attributes.position.clone().array;
|
||||
var _normals = _geometry.attributes.normal.clone().array;
|
||||
var _colours = _geometry.attributes.color.clone().array;
|
||||
|
||||
for (var i = 0; i < _colours.length; i += 3) {
|
||||
_colours[i] = colour.r;
|
||||
_colours[i + 1] = colour.g;
|
||||
_colours[i + 2] = colour.b;
|
||||
}
|
||||
|
||||
var _point = {
|
||||
positions: _vertices,
|
||||
normals: _normals,
|
||||
colors: _colours
|
||||
};
|
||||
|
||||
if (options.interactive && options.pickingId) {
|
||||
// Inject picking ID
|
||||
_point.pickingId = options.pickingId;
|
||||
}
|
||||
|
||||
return _point;
|
||||
});
|
||||
|
||||
resolve({
|
||||
attributes: attributes,
|
||||
flat: false
|
||||
});
|
||||
});
|
||||
|
||||
this._bufferAttributes = Buffer.mergeAttributes(attributes);
|
||||
|
||||
// Original attributes are no longer required so free the memory
|
||||
attributes = null;
|
||||
}
|
||||
|
||||
getBufferAttributes() {
|
||||
|
@ -229,64 +243,68 @@ class PointLayer extends Layer {
|
|||
this._projectedCoordinates = null;
|
||||
}
|
||||
|
||||
// Create and store mesh from buffer attributes
|
||||
//
|
||||
// This is only called if the layer is controlling its own output
|
||||
_setMesh(attributes) {
|
||||
static SetMesh(attributes, attributeLengths, flat, style, options, skybox) {
|
||||
var geometry = new THREE.BufferGeometry();
|
||||
|
||||
// itemSize = 3 because there are 3 values (components) per vertex
|
||||
geometry.addAttribute('position', new THREE.BufferAttribute(attributes.vertices, 3));
|
||||
geometry.addAttribute('normal', new THREE.BufferAttribute(attributes.normals, 3));
|
||||
geometry.addAttribute('color', new THREE.BufferAttribute(attributes.colours, 3));
|
||||
|
||||
if (attributes.pickingIds) {
|
||||
geometry.addAttribute('pickingId', new THREE.BufferAttribute(attributes.pickingIds, 1));
|
||||
for (var key in attributes) {
|
||||
geometry.addAttribute(key.slice(0, -1), new THREE.BufferAttribute(attributes[key], attributeLengths[key]));
|
||||
}
|
||||
|
||||
geometry.computeBoundingBox();
|
||||
|
||||
var material;
|
||||
|
||||
if (this._options.material && this._options.material instanceof THREE.Material) {
|
||||
material = this._options.material;
|
||||
} else if (!this._world._environment._skybox) {
|
||||
material = new THREE.MeshBasicMaterial({
|
||||
vertexColors: THREE.VertexColors
|
||||
// side: THREE.BackSide
|
||||
if (options.pointMaterial && options.pointMaterial instanceof THREE.Material) {
|
||||
material = options.pointMaterial;
|
||||
} else if (!skybox) {
|
||||
material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: THREE.VertexColors,
|
||||
// side: THREE.BackSide,
|
||||
transparent: style.transparent,
|
||||
opacity: style.opacity,
|
||||
blending: style.blending
|
||||
});
|
||||
} else {
|
||||
material = new THREE.MeshStandardMaterial({
|
||||
vertexColors: THREE.VertexColors
|
||||
// side: THREE.BackSide
|
||||
vertexColors: THREE.VertexColors,
|
||||
// side: THREE.BackSide,
|
||||
transparent: style.transparent,
|
||||
opacity: style.opacity,
|
||||
blending: style.blending
|
||||
});
|
||||
material.roughness = 1;
|
||||
material.metalness = 0.1;
|
||||
material.envMapIntensity = 3;
|
||||
material.envMap = this._world._environment._skybox.getRenderTarget();
|
||||
material.envMap = skybox.getRenderTarget();
|
||||
}
|
||||
|
||||
var mesh;
|
||||
|
||||
// Pass mesh through callback, if defined
|
||||
if (typeof this._options.onMesh === 'function') {
|
||||
mesh = this._options.onMesh(geometry, material);
|
||||
if (typeof options.onPolygonMesh === 'function') {
|
||||
mesh = options.onPolygonMesh(geometry, material);
|
||||
} else {
|
||||
mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
mesh.castShadow = true;
|
||||
// mesh.receiveShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
}
|
||||
|
||||
if (this._options.interactive && this._pickingMesh) {
|
||||
if (flat) {
|
||||
material.depthWrite = false;
|
||||
mesh.renderOrder = 1;
|
||||
}
|
||||
|
||||
if (options.interactive) {
|
||||
material = new PickingMaterial();
|
||||
// material.side = THREE.BackSide;
|
||||
material.side = THREE.BackSide;
|
||||
|
||||
var pickingMesh = new THREE.Mesh(geometry, material);
|
||||
this._pickingMesh.add(pickingMesh);
|
||||
}
|
||||
|
||||
this._mesh = mesh;
|
||||
return Promise.resolve({
|
||||
mesh: mesh,
|
||||
pickingMesh: pickingMesh
|
||||
});
|
||||
}
|
||||
|
||||
// Convert and project coordinates
|
||||
|
@ -329,72 +347,13 @@ class PointLayer extends Layer {
|
|||
this._offset.x = -1 * _point.x;
|
||||
this._offset.y = -1 * _point.y;
|
||||
|
||||
this._pointScale = this._world.pointScale(latlon);
|
||||
this._options.pointScale = this._world.pointScale(latlon);
|
||||
}
|
||||
|
||||
return _point;
|
||||
});
|
||||
}
|
||||
|
||||
// Transform line representation into attribute arrays that can be used by
|
||||
// THREE.BufferGeometry
|
||||
//
|
||||
// TODO: Can this be simplified? It's messy and huge
|
||||
_toAttributes(line) {
|
||||
// Three components per vertex
|
||||
var vertices = new Float32Array(line.verticesCount * 3);
|
||||
var colours = new Float32Array(line.verticesCount * 3);
|
||||
|
||||
var pickingIds;
|
||||
if (line.pickingId) {
|
||||
// One component per vertex
|
||||
pickingIds = new Float32Array(line.verticesCount);
|
||||
}
|
||||
|
||||
var _vertices = line.vertices;
|
||||
var _colour = line.colours;
|
||||
|
||||
var _pickingId;
|
||||
if (pickingIds) {
|
||||
_pickingId = line.pickingId;
|
||||
}
|
||||
|
||||
var lastIndex = 0;
|
||||
|
||||
for (var i = 0; i < _vertices.length; i++) {
|
||||
var ax = _vertices[i][0];
|
||||
var ay = _vertices[i][1];
|
||||
var az = _vertices[i][2];
|
||||
|
||||
var c1 = _colour[i];
|
||||
|
||||
vertices[lastIndex * 3 + 0] = ax;
|
||||
vertices[lastIndex * 3 + 1] = ay;
|
||||
vertices[lastIndex * 3 + 2] = az;
|
||||
|
||||
colours[lastIndex * 3 + 0] = c1[0];
|
||||
colours[lastIndex * 3 + 1] = c1[1];
|
||||
colours[lastIndex * 3 + 2] = c1[2];
|
||||
|
||||
if (pickingIds) {
|
||||
pickingIds[lastIndex] = _pickingId;
|
||||
}
|
||||
|
||||
lastIndex++;
|
||||
}
|
||||
|
||||
var attributes = {
|
||||
vertices: vertices,
|
||||
colours: colours
|
||||
};
|
||||
|
||||
if (pickingIds) {
|
||||
attributes.pickingIds = pickingIds;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
// Returns true if the line is flat (has no height)
|
||||
isFlat() {
|
||||
return this._flat;
|
||||
|
|
Ładowanie…
Reference in New Issue