diff --git a/demo/index.ts b/demo/index.ts index e8c05f5..f500215 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -35,6 +35,7 @@ async function start() { hoverHighlight: { intencity: 0.1, }, + groundCoveringColor: 'rgba(233, 232, 220, 0.8)', }); (window as any).gltfPlugin = plugin; diff --git a/demo/mocks.ts b/demo/mocks.ts index bd053ba..3e4f9f7 100644 --- a/demo/mocks.ts +++ b/demo/mocks.ts @@ -8,7 +8,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ rotateX: 90, rotateY: -15.1240072739039, scale: 191.637678, - linkedIds: ['70030076555821177'], + linkedIds: ['70030076555823021'], mapOptions: { center: [47.24547737708662, 56.134591508663135], pitch: 40, @@ -83,6 +83,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ id: '000034', text: '11', modelUrl: 'zgktechnology1_floor11.glb', + isUnderground: true, mapOptions: { center: [47.24556663327373, 56.13456998211929], pitch: 40, @@ -146,7 +147,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ rotateX: 90, rotateY: -15.1240072739039, scale: 191.637678, - linkedIds: ['70030076555823021'], + linkedIds: ['70030076555821177'], mapOptions: { center: [47.245008950283065, 56.1344698491912], pitch: 45, @@ -163,6 +164,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ id: 'aaa777', text: '2-15', modelUrl: 'zgktechnology2_floor2.glb', + isUnderground: true, mapOptions: { center: [47.24463456947374, 56.134675042798094], pitch: 35, diff --git a/package-lock.json b/package-lock.json index 968550b..f2908b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@2gis/mapgl-gltf", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@2gis/mapgl-gltf", - "version": "1.2.1", + "version": "1.3.0", "license": "BSD-2-Clause", "devDependencies": { "@2gis/mapgl": "1.37.2", diff --git a/package.json b/package.json index 6e990c0..3d427ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@2gis/mapgl-gltf", - "version": "1.2.1", + "version": "1.3.0", "description": "Plugin for the rendering glTF models with MapGL", "main": "dist/bundle.js", "typings": "dist/types/index.d.ts", @@ -8,7 +8,10 @@ "type": "git", "url": "https://github.com/2gis/mapgl-gltf.git" }, - "files": ["dist/libs", "dist/types"], + "files": [ + "dist/libs", + "dist/types" + ], "scripts": { "build": "npm run build:bundle && npm run build:typings && npm run build:transpile && npm run build:docs", "build:bundle": "webpack --env type=production", diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..21cb673 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,32 @@ +import type { GeoJsonSourceOptions } from '@2gis/mapgl/types'; + +export const GROUND_COVERING_SOURCE_DATA: GeoJsonSourceOptions['data'] = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-180, -85], + [180, -85], + [180, 85], + [-180, 85], + [-180, -85], + ], + ], + }, +}; + +export const GROUND_COVERING_SOURCE_PURPOSE = '__mapglPlugins_mapgl-gltf'; +export const GROUND_COVERING_LAYER = { + id: '__mapglPlugins_mapgl-gltf', + type: 'polygon', + style: { + color: ['to-color', ['sourceAttr', 'color']], + }, + filter: [ + 'all', + ['match', ['sourceAttr', 'purpose'], [GROUND_COVERING_SOURCE_PURPOSE], true, false], + ['to-boolean', ['sourceAttr', 'color']], + ], +}; diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index 83f8d9f..728b59a 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -25,4 +25,5 @@ export const defaultOptions: Required = { floorsControl: { position: 'centerLeft', }, + groundCoveringColor: '#F8F8EBCC', }; diff --git a/src/plugin.ts b/src/plugin.ts index 9489d69..ab468c3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -18,6 +18,7 @@ import type { import type { BuildingOptions } from './types/realtyScene'; import type { GltfPluginEventTable } from './types/events'; import { applyOptionalDefaults, disposeObject } from './utils/common'; +import { GROUND_COVERING_LAYER } from './constants'; export class GltfPlugin extends Evented { private isThreeJsInitialized = false; @@ -99,7 +100,7 @@ export class GltfPlugin extends Evented { this.map.setHiddenObjects(hiddenIds); } - this.addThreeJsLayer(); + this.addLayers(); this.poiGroups.onMapStyleUpdate(); }; @@ -120,6 +121,18 @@ export class GltfPlugin extends Evented { return Array.from(this.linkedIds); } + public setOptions(pluginOptions: Pick, 'groundCoveringColor'>) { + Object.keys(pluginOptions).forEach((option) => { + switch (option) { + case 'groundCoveringColor': { + this.options.groundCoveringColor = pluginOptions.groundCoveringColor; + this.realtyScene?.resetGroundCoveringColor(); + break; + } + } + }); + } + /** * Add the list of models to the map * Use this method if you want to add @@ -338,6 +351,10 @@ export class GltfPlugin extends Evented { this.viewport.height * window.devicePixelRatio, ); + if (this.realtyScene?.isUndergroundFloorShown()) { + this.renderer.clearDepth(); + } + this.renderer.render(this.scene, this.camera); } @@ -366,7 +383,8 @@ export class GltfPlugin extends Evented { this.onPluginInit(); } - private addThreeJsLayer() { + private addLayers() { + this.map.addLayer(GROUND_COVERING_LAYER); this.map.addLayer({ id: 'gltf-plugin-style-layer', type: 'custom', diff --git a/src/realtyScene/realtyScene.ts b/src/realtyScene/realtyScene.ts index c50fc17..59f3221 100644 --- a/src/realtyScene/realtyScene.ts +++ b/src/realtyScene/realtyScene.ts @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import type { Map as MapGL, AnimationOptions, HtmlMarker } from '@2gis/mapgl/types'; +import type { Map as MapGL, AnimationOptions, HtmlMarker, GeoJsonSource } from '@2gis/mapgl/types'; import { EventSource } from '../eventSource'; import { GltfPlugin } from '../plugin'; @@ -21,6 +21,7 @@ import type { GltfPluginPoiEvent, PoiGeoJsonProperties, } from '../types/events'; +import { GROUND_COVERING_SOURCE_DATA, GROUND_COVERING_SOURCE_PURPOSE } from '../constants'; export class RealtyScene { private activeBuilding?: BuildingOptions; @@ -34,6 +35,8 @@ export class RealtyScene { private prevHoveredModelId: Id | null = null; private popup: HtmlMarker | null = null; private scene: BuildingOptions[] | null = null; + private groundCoveringSource: GeoJsonSource; + private undergroundFloors = new Set(); constructor( private plugin: GltfPlugin, @@ -43,6 +46,13 @@ export class RealtyScene { private options: typeof defaultOptions, ) { this.container = map.getContainer(); + this.groundCoveringSource = new mapgl.GeoJsonSource(map, { + maxZoom: 2, + data: GROUND_COVERING_SOURCE_DATA, + attributes: { + purpose: GROUND_COVERING_SOURCE_PURPOSE, + }, + }); } public async addRealtyScene(scene: BuildingOptions[], originalState?: BuildingState) { @@ -72,40 +82,25 @@ export class RealtyScene { this.scene = scene; scene.forEach((scenePart) => { this.buildingFacadeIds.push(scenePart.modelId); - - const modelOptions = { - modelId: scenePart.modelId, - coordinates: scenePart.coordinates, - modelUrl: scenePart.modelUrl, - rotateX: scenePart.rotateX, - rotateY: scenePart.rotateY, - rotateZ: scenePart.rotateZ, - offsetX: scenePart.offsetX, - offsetY: scenePart.offsetY, - offsetZ: scenePart.offsetZ, - scale: scenePart.scale, - linkedIds: scenePart.linkedIds, - interactive: scenePart.interactive, - }; - + const modelOptions = getBuildingModelOptions(scenePart); const floors = scenePart.floors ?? []; let hasFloorByDefault = false; - if (state?.floorId !== undefined) { - for (let floor of floors) { - if (floor.id === state.floorId) { - // for convenience push original building - models.push(modelOptions); - // push modified options for floor - const clonedOptions = clone(modelOptions); - clonedOptions.modelId = floor.id; - clonedOptions.modelUrl = floor.modelUrl; - models.push(clonedOptions); - modelIds.push(floor.id); - hasFloorByDefault = true; - break; - } + + for (let floor of floors) { + if (floor.isUnderground) { + this.undergroundFloors.add(floor.id); + } + + if (state?.floorId !== undefined && floor.id === state.floorId) { + // for convenience push original building + models.push(modelOptions); + // push modified options for floor + models.push(getFloorModelOptions(floor, scenePart)); + modelIds.push(floor.id); + hasFloorByDefault = true; } } + if (!hasFloorByDefault) { models.push(modelOptions); modelIds.push(scenePart.modelId); @@ -116,24 +111,17 @@ export class RealtyScene { if (floor.id === state?.floorId) { continue; } - models.push({ - modelId: floor.id, - coordinates: scenePart.coordinates, - modelUrl: floor.modelUrl, - rotateX: scenePart.rotateX, - rotateY: scenePart.rotateY, - rotateZ: scenePart.rotateZ, - offsetX: scenePart.offsetX, - offsetY: scenePart.offsetY, - offsetZ: scenePart.offsetZ, - scale: scenePart.scale, - linkedIds: scenePart.linkedIds, - interactive: scenePart.interactive, - }); + models.push(getFloorModelOptions(floor, scenePart)); } } }); + // Leave only the underground floor's plan to be shown + if (state?.floorId !== undefined && this.undergroundFloors.has(state.floorId)) { + modelIds.length = 0; + modelIds.push(state.floorId); + } + return this.plugin.addModelsPartially(models, modelIds).then(() => { // set options after adding models if (state?.floorId !== undefined) { @@ -141,6 +129,9 @@ export class RealtyScene { const activeFloor = floors.find((floor) => floor.id === state?.floorId); this.setMapOptions(activeFloor?.mapOptions); this.addFloorPoi(activeFloor); + if (this.undergroundFloors.has(state.floorId)) { + this.switchOnGroundCovering(); + } } else { this.setMapOptions(this.activeBuilding?.mapOptions); } @@ -161,6 +152,20 @@ export class RealtyScene { }); } + public resetGroundCoveringColor() { + const attrs = this.groundCoveringSource.getAttributes(); + if ('color' in attrs) { + this.groundCoveringSource.setAttributes({ + ...attrs, + color: this.options.groundCoveringColor, + }); + } + } + + public isUndergroundFloorShown() { + return this.activeModelId !== undefined && this.undergroundFloors.has(this.activeModelId); + } + public destroy(preserveCache?: boolean) { this.unbindRealtySceneEvents(); @@ -177,6 +182,9 @@ export class RealtyScene { this.clearPoiGroups(); this.eventSource.setCurrentFloorId(null); + this.groundCoveringSource.destroy(); + this.undergroundFloors.clear(); + this.control?.destroy(); this.control = undefined; @@ -349,43 +357,62 @@ export class RealtyScene { } this.clearPoiGroups(); - this.plugin - .addModel({ - modelId: model.modelId, - coordinates: model.coordinates, - modelUrl: model.modelUrl, - rotateX: model.rotateX, - rotateY: model.rotateY, - scale: model.scale, - }) - .then(() => { - if (this.activeModelId) { - this.plugin.removeModel(this.activeModelId, true); + const modelsToAdd: ModelOptions[] = this.isUndergroundFloorShown() + ? (this.scene ?? []).map((scenePart) => getBuildingModelOptions(scenePart)) + : [getBuildingModelOptions(model)]; + + this.plugin.addModels(modelsToAdd).then(() => { + if (this.activeModelId !== undefined) { + this.plugin.removeModel(this.activeModelId, true); + if (this.isUndergroundFloorShown()) { + this.switchOffGroundCovering(); } - this.setMapOptions(model?.mapOptions); - this.activeModelId = model.modelId; - }); + } + this.setMapOptions(model?.mapOptions); + this.activeModelId = model.modelId; + }); } // click to the floor button if (ev.floorId !== undefined) { const selectedFloor = model.floors.find((floor) => floor.id === ev.floorId); if (selectedFloor !== undefined && this.activeModelId !== undefined) { - this.plugin - .addModel({ - modelId: selectedFloor.id, - coordinates: model.coordinates, - modelUrl: selectedFloor.modelUrl, - rotateX: model.rotateX, - rotateY: model.rotateY, - scale: model.scale, - }) - .then(() => { - if (this.activeModelId) { + const selectedFloorModelOption = getFloorModelOptions(selectedFloor, model); + + // In case of underground -> underground and ground -> ground transitions just switch floor's plan + if (this.isUndergroundFloorShown() === Boolean(selectedFloor.isUnderground)) { + this.plugin.addModel(selectedFloorModelOption).then(() => { + if (this.activeModelId !== undefined) { this.plugin.removeModel(this.activeModelId, true); } - this.addFloorPoi(selectedFloor); }); + + return; + } + + const modelsToAdd: ModelOptions[] = this.isUndergroundFloorShown() + ? (this.scene ?? []) + .filter((scenePart) => scenePart.modelId !== model.modelId) + .map((scenePart) => getBuildingModelOptions(scenePart)) + : []; + + modelsToAdd.push(selectedFloorModelOption); + + const modelsToRemove = this.isUndergroundFloorShown() + ? [] + : (this.scene ?? []) + .filter((scenePart) => scenePart.modelId !== model.modelId) + .map((scenePart) => scenePart.modelId); + + modelsToRemove.push(this.activeModelId); + + this.plugin.addModels(modelsToAdd).then(() => { + this.plugin.removeModels(modelsToRemove, true); + this.isUndergroundFloorShown() + ? this.switchOffGroundCovering() + : this.switchOnGroundCovering(); + this.addFloorPoi(selectedFloor); + }); } } } @@ -412,43 +439,32 @@ export class RealtyScene { this.activeModelId && this.activeModelId !== this.activeBuilding?.modelId ) { + // User is able to click on any other buildings as long as ground floor's plan is shown + // because when underground floor's plan is shown other buildings are hidden. const oldId = this.activeModelId; - this.plugin - .addModel({ - modelId: this.activeBuilding.modelId, - coordinates: this.activeBuilding.coordinates, - modelUrl: this.activeBuilding.modelUrl, - rotateX: this.activeBuilding.rotateX, - rotateY: this.activeBuilding.rotateY, - scale: this.activeBuilding.scale, - }) - .then(() => { - this.clearPoiGroups(); - this.plugin.removeModel(oldId, true); - }); + this.plugin.addModel(getBuildingModelOptions(this.activeBuilding)).then(() => { + this.clearPoiGroups(); + this.plugin.removeModel(oldId, true); + }); } // show the highest floor after a click on the building const floors = selectedBuilding.floors ?? []; if (floors.length !== 0) { const floorOptions = floors[floors.length - 1]; - this.plugin - .addModel({ - modelId: floorOptions.id, - coordinates: selectedBuilding.coordinates, - modelUrl: floorOptions.modelUrl, - rotateX: selectedBuilding.rotateX, - rotateY: selectedBuilding.rotateY, - scale: selectedBuilding.scale, - }) - .then(() => { - this.plugin.removeModel(selectedBuilding.modelId, true); - this.addFloorPoi(floorOptions); - this.control?.switchCurrentFloorLevel( - selectedBuilding.modelId, - floorOptions.id, - ); - }); + + const modelsToRemove = floorOptions.isUnderground + ? scene.map((scenePart) => scenePart.modelId) + : [selectedBuilding.modelId]; + + this.plugin.addModel(getFloorModelOptions(floorOptions, selectedBuilding)).then(() => { + this.plugin.removeModels(modelsToRemove, true); + if (floorOptions.isUnderground) { + this.switchOnGroundCovering(); + } + this.addFloorPoi(floorOptions); + this.control?.switchCurrentFloorLevel(selectedBuilding.modelId, floorOptions.id); + }); } else { this.activeModelId = selectedBuilding.modelId; this.setMapOptions(selectedBuilding.mapOptions); @@ -571,4 +587,54 @@ export class RealtyScene {

${data.description}

`; } + + private switchOffGroundCovering() { + const attrs = { ...this.groundCoveringSource.getAttributes() }; + delete attrs['color']; + this.groundCoveringSource.setAttributes(attrs); + } + + private switchOnGroundCovering() { + this.groundCoveringSource.setAttributes({ + ...this.groundCoveringSource.getAttributes(), + color: this.options.groundCoveringColor, + }); + } +} + +function getBuildingModelOptions(building: BuildingOptions): ModelOptions { + return { + modelId: building.modelId, + coordinates: building.coordinates, + modelUrl: building.modelUrl, + rotateX: building.rotateX, + rotateY: building.rotateY, + rotateZ: building.rotateZ, + offsetX: building.offsetX, + offsetY: building.offsetY, + offsetZ: building.offsetZ, + scale: building.scale, + linkedIds: building.linkedIds, + interactive: building.interactive, + }; +} + +function getFloorModelOptions( + floor: BuildingFloorOptions, + building: BuildingOptions, +): ModelOptions { + return { + modelId: floor.id, + coordinates: building.coordinates, + modelUrl: floor.modelUrl, + rotateX: building.rotateX, + rotateY: building.rotateY, + rotateZ: building.rotateZ, + offsetX: building.offsetX, + offsetY: building.offsetY, + offsetZ: building.offsetZ, + scale: building.scale, + linkedIds: building.linkedIds, + interactive: building.interactive, + }; } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 284f4d6..7dead45 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -121,6 +121,10 @@ export interface PluginOptions { * Settings of the highlighted models */ hoverHighlight?: HightlightOptions; + /** + * Color for the ground covering when an underground floor's plan is shown. + */ + groundCoveringColor?: string; } /** diff --git a/src/types/realtyScene.ts b/src/types/realtyScene.ts index a3b8518..e566032 100644 --- a/src/types/realtyScene.ts +++ b/src/types/realtyScene.ts @@ -50,6 +50,12 @@ export interface BuildingFloorOptions { * Map's options to apply after selecting the particular floor */ mapOptions?: MapOptions; + /** + * Specifies whether a floor's plan is underground. + * If value is `true` the map will be covered with a ground geometry + * so that only the floor's plan will stay visible. + */ + isUnderground?: boolean; } /**