From 2b046d1faaaab4c61f6e05057816ae3f651b55fc Mon Sep 17 00:00:00 2001 From: vladyslavtk Date: Mon, 16 Dec 2024 22:16:27 +0200 Subject: [PATCH] wip: area clipping --- src/apps/permits/demoPermitConfig.ts | 2 +- src/apps/permits/index.ts | 12 +- src/plugins/cesium/interactionHelpers.ts | 22 +- .../ngv-plugin-cesium-model-interact.ts | 44 ++- .../cesium/ngv-plugin-cesium-slicing.ts | 328 ++++++++++++++++++ src/plugins/ui/ngv-layers-list.ts | 71 +++- 6 files changed, 446 insertions(+), 33 deletions(-) create mode 100644 src/plugins/cesium/ngv-plugin-cesium-slicing.ts diff --git a/src/apps/permits/demoPermitConfig.ts b/src/apps/permits/demoPermitConfig.ts index c33f5bc..f666963 100644 --- a/src/apps/permits/demoPermitConfig.ts +++ b/src/apps/permits/demoPermitConfig.ts @@ -27,7 +27,7 @@ export const config: IPermitsConfig = { '@demo': () => import('../../catalogs/demoCatalog.js'), }, layers: { - tiles3d: ['@cesium/googlePhotorealistic'], + // tiles3d: ['@cesium/googlePhotorealistic'], // models: ['@demo/sofa', '@demo/thatopensmall'], imageries: ['@geoadmin/pixel-karte-farbe'], // terrain: '@geoadmin/terrain', diff --git a/src/apps/permits/index.ts b/src/apps/permits/index.ts index 1d101f9..9b9f2fd 100644 --- a/src/apps/permits/index.ts +++ b/src/apps/permits/index.ts @@ -13,9 +13,10 @@ import type {IPermitsConfig} from './ingv-config-permits.js'; import '../../plugins/cesium/ngv-plugin-cesium-widget'; import '../../plugins/cesium/ngv-plugin-cesium-upload'; import '../../plugins/cesium/ngv-plugin-cesium-model-interact'; +import '../../plugins/cesium/ngv-plugin-cesium-slicing'; import type {CesiumWidget, DataSourceCollection} from '@cesium/engine'; -import {PrimitiveCollection} from '@cesium/engine'; +import {PrimitiveCollection, CustomDataSource} from '@cesium/engine'; import type {ViewerInitializedDetails} from '../../plugins/cesium/ngv-plugin-cesium-widget.js'; @customElement('ngv-app-permits') @@ -26,6 +27,7 @@ export class NgvAppPermits extends ABaseApp { private uploadedModelsCollection: PrimitiveCollection = new PrimitiveCollection(); private dataSourceCollection: DataSourceCollection; + private slicingDataSource: CustomDataSource = new CustomDataSource(); private storeOptions = { localStoreKey: 'permits-localStoreModels', @@ -74,6 +76,11 @@ export class NgvAppPermits extends ABaseApp { .storeOptions="${this.storeOptions}" .options="${{listTitle: 'Uploaded models'}}" > + ` : ''} @@ -85,6 +92,9 @@ export class NgvAppPermits extends ABaseApp { this.viewer.scene.primitives.add(this.uploadedModelsCollection); this.dataSourceCollection = evt.detail.dataSourceCollection; this.collections = evt.detail.primitiveCollections; + this.dataSourceCollection + .add(this.slicingDataSource) + .catch((e) => console.error(e)); }} > diff --git a/src/plugins/cesium/interactionHelpers.ts b/src/plugins/cesium/interactionHelpers.ts index 487507e..422a27f 100644 --- a/src/plugins/cesium/interactionHelpers.ts +++ b/src/plugins/cesium/interactionHelpers.ts @@ -1,10 +1,11 @@ import { Cesium3DTileset, ClippingPolygonCollection, - CustomDataSource, Globe, + CustomDataSource, + Globe, Model, PrimitiveCollection, - Scene + Scene, } from '@cesium/engine'; import { ArcType, @@ -528,7 +529,10 @@ export function getClippingPolygon(model: INGVCesiumModel): ClippingPolygon { }); } -export function applyClippingTo3dTileset(tileset: Cesium3DTileset, models: INGVCesiumModel[]): void { +export function applyClippingTo3dTileset( + tileset: Cesium3DTileset, + models: INGVCesiumModel[], +): void { const polygons: ClippingPolygon[] = []; models.forEach((m) => { if (m.id.tilesClipping) { @@ -540,7 +544,11 @@ export function applyClippingTo3dTileset(tileset: Cesium3DTileset, models: INGVC }); } -export function updateModelClipping(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { +export function updateModelClipping( + model: INGVCesiumModel, + tiles3dCollection: PrimitiveCollection, + globe: Globe, +): void { if ((!tiles3dCollection?.length && !globe) || !model?.ready) return; const polygon = model.id.clippingPolygon; const newPolygon = getClippingPolygon(model); @@ -585,7 +593,11 @@ export function updateModelClipping(model: INGVCesiumModel, tiles3dCollection: P model.id.clippingPolygon = newPolygon; } -export function removeClippingFrom3dTilesets(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { +export function removeClippingFrom3dTilesets( + model: INGVCesiumModel, + tiles3dCollection: PrimitiveCollection, + globe: Globe, +): void { if ((!tiles3dCollection?.length && !globe) || !model.ready) return; const polygon = model.id.clippingPolygon; if (tiles3dCollection?.length) { diff --git a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts index cab8764..10e0385 100644 --- a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts +++ b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts @@ -7,7 +7,7 @@ import type { PrimitiveCollection, DataSourceCollection, Cartesian2, - Cesium3DTileset + Cesium3DTileset, } from '@cesium/engine'; import { Model, @@ -38,7 +38,7 @@ import type {BBoxStyles} from './interactionHelpers.js'; import { applyClippingTo3dTileset, removeClippingFrom3dTilesets, - updateModelClipping + updateModelClipping, } from './interactionHelpers.js'; import { getHorizontalMoveVector, @@ -123,7 +123,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { this.primitiveCollection.primitiveAdded.addEventListener( (model: INGVCesiumModel) => { this.onPrimitivesChanged(); - updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe); + updateModelClipping( + model, + this.tiles3dCollection, + this.viewer.scene.globe, + ); }, ); this.primitiveCollection.primitiveRemoved.addEventListener( @@ -198,15 +202,29 @@ export class NgvPluginCesiumModelInteract extends LitElement { const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(this.moveStart); this.movePlane = Plane.fromPointNormal(this.moveStart, normal); - if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { - removeClippingFrom3dTilesets(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + if ( + this.chosenModel?.id.tilesClipping || + this.chosenModel?.id.terrainClipping + ) { + removeClippingFrom3dTilesets( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); } } } onLeftUp(): void { if (this.grabType) { - if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { - updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + if ( + this.chosenModel?.id.tilesClipping || + this.chosenModel?.id.terrainClipping + ) { + updateModelClipping( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); } this.viewer.scene.screenSpaceCameraController.enableInputs = true; this.grabType = undefined; @@ -414,7 +432,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { }); this.primitiveCollection.add(model); model.readyEvent.addEventListener(() => - updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe), + updateModelClipping( + model, + this.tiles3dCollection, + this.viewer.scene.globe, + ), ); }), ); @@ -449,7 +471,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { @clippingChange=${(evt: {detail: ClippingChangeDetail}) => { this.chosenModel.id.terrainClipping = evt.detail.terrainClipping; this.chosenModel.id.tilesClipping = evt.detail.tilesClipping; - updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + updateModelClipping( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); }} @done="${() => { this.chosenModel = undefined; diff --git a/src/plugins/cesium/ngv-plugin-cesium-slicing.ts b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts new file mode 100644 index 0000000..9677d42 --- /dev/null +++ b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts @@ -0,0 +1,328 @@ +import {customElement, property, state} from 'lit/decorators.js'; +import {css, html, type HTMLTemplateResult, LitElement} from 'lit'; +import type { + Cartesian2, + Cartesian3, + Cesium3DTileset, + CesiumWidget, + CustomDataSource, + Entity, + PrimitiveCollection, +} from '@cesium/engine'; +import {ClippingPolygon, ClippingPolygonCollection} from '@cesium/engine'; +import { + CallbackProperty, + Color, + ConstantProperty, + PolygonHierarchy, + ScreenSpaceEventHandler, + ScreenSpaceEventType, +} from '@cesium/engine'; +import '../ui/ngv-layer-details.js'; +import '../ui/ngv-layers-list.js'; +import type {ClippingChangeDetail} from '../ui/ngv-layer-details.js'; + +@customElement('ngv-plugin-cesium-slicing') +export class NgvPluginCesiumSlicing extends LitElement { + @property({type: Object}) + private viewer: CesiumWidget; + @property({type: Object}) + private tiles3dCollection: PrimitiveCollection; + @property({type: Object}) + private slicingDataSource: CustomDataSource; + @state() + private slicingActive: boolean = false; + @state() + private clippingPolygons: {clipping: ClippingPolygon; entity: Entity}[] = []; + @state() + private activePolygon: Entity | undefined = undefined; + private editingClipping: + | {clipping: ClippingPolygon; entity: Entity} + | undefined = undefined; + private eventHandler: ScreenSpaceEventHandler | undefined; + private activePositions: Cartesian3[] = []; + private floatingPoint: Entity | undefined = undefined; + private points: Entity[] = []; + + static styles = css` + button { + border-radius: 4px; + padding: 0 16px; + height: 40px; + cursor: pointer; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.16); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); + transition: background-color 200ms; + } + + .slicing-container { + display: flex; + flex-direction: column; + margin-left: auto; + margin-right: auto; + padding: 10px; + gap: 10px; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.16); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); + } + + .add-slicing-btn { + width: 100%; + } + `; + + createPoint(position: Cartesian3 | CallbackProperty): Entity { + return this.slicingDataSource.entities.add({ + position, + point: { + color: Color.RED, + pixelSize: 5, + }, + }); + } + + drawPolygon(): Entity { + return this.slicingDataSource.entities.add({ + polygon: { + hierarchy: new CallbackProperty(() => { + return new PolygonHierarchy(this.activePositions); + }, false), + material: Color.RED.withAlpha(0.7), + }, + }); + } + + private pickPosition(position: Cartesian2): Cartesian3 { + const ray = this.viewer.camera.getPickRay(position); + return this.viewer.scene.globe.show + ? this.viewer.scene.globe.pick(ray, this.viewer.scene) + : this.viewer.scene.pickPosition(position); + } + + private startDrawing(positions: Cartesian3[], polygon?: Entity) { + this.activePositions = [...positions]; + this.floatingPoint = this.createPoint( + new CallbackProperty(() => { + return this.activePositions[this.activePositions.length - 1]; + }, false), + ); + if (polygon) { + polygon.polygon.hierarchy = new CallbackProperty(() => { + return new PolygonHierarchy(this.activePositions); + }, false); + polygon.show = true; + } + this.activePolygon = polygon ? polygon : this.drawPolygon(); + if (!this.activePolygon.name?.length) { + const date = new Date(); + this.activePolygon.name = `Polygon ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + } + this.activePositions.forEach((position) => { + this.points.push(this.createPoint(position)); + }); + } + + addClippingPolygon(): void { + this.eventHandler = new ScreenSpaceEventHandler(this.viewer.canvas); + this.eventHandler.setInputAction( + (event: ScreenSpaceEventHandler.PositionedEvent) => { + const position = this.pickPosition(event.position); + if (position) { + if (this.activePositions.length === 0) { + this.startDrawing([position]); + } + this.activePositions.push(position); + this.points.push(this.createPoint(position)); + } + }, + ScreenSpaceEventType.LEFT_CLICK, + ); + + this.eventHandler.setInputAction( + (event: ScreenSpaceEventHandler.MotionEvent) => { + if (this.floatingPoint) { + const ray = this.viewer.camera.getPickRay(event.endPosition); + const newPosition = this.viewer.scene.globe.show + ? this.viewer.scene.globe.pick(ray, this.viewer.scene) + : this.viewer.scene.pickPosition(event.endPosition); + if (newPosition) { + this.activePositions.pop(); + this.activePositions.push(newPosition); + } + } + }, + ScreenSpaceEventType.MOUSE_MOVE, + ); + this.eventHandler.setInputAction( + (event: ScreenSpaceEventHandler.PositionedEvent) => { + const position = this.pickPosition(event.position); + this.activePositions.push(position); + this.finishSlicing(); + }, + ScreenSpaceEventType.LEFT_DOUBLE_CLICK, + ); + this.eventHandler.setInputAction(() => { + if (this.activePositions.length > 1) { + this.activePositions.splice(this.activePositions.length - 2, 1); + this.slicingDataSource.entities.remove(this.points.pop()); + } + }, ScreenSpaceEventType.RIGHT_CLICK); + this.slicingActive = true; + } + + finishSlicing(): void { + this.eventHandler?.destroy(); + if (this.activePositions.length > 2) { + // replace callback property + this.activePolygon.polygon.hierarchy = new ConstantProperty( + new PolygonHierarchy(this.activePositions), + ); + this.activePolygon.show = false; + const clippingPolygon = new ClippingPolygon({ + positions: this.activePositions, + }); + if (this.editingClipping) { + this.editingClipping.clipping = clippingPolygon; + } else { + this.clippingPolygons.push({ + clipping: clippingPolygon, + entity: this.activePolygon, + }); + } + this.applyClipping(clippingPolygon); + } else { + this.slicingDataSource.entities.remove(this.activePolygon); + } + this.slicingDataSource.entities.remove(this.floatingPoint); + this.points.forEach((p) => this.slicingDataSource.entities.remove(p)); + this.activePositions = []; + this.points = []; + this.floatingPoint = undefined; + this.activePolygon = undefined; + this.slicingActive = false; + this.editingClipping = undefined; + } + + applyClipping(clippingPolygon: ClippingPolygon): void { + if (!this.viewer.scene.globe.clippingPolygons) { + this.viewer.scene.globe.clippingPolygons = + new ClippingPolygonCollection(); + } + this.viewer.scene.globe.clippingPolygons.add(clippingPolygon); + if (this.tiles3dCollection) { + for (let i = 0; i < this.tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = this.tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if (!tileset.clippingPolygons) { + tileset.clippingPolygons = new ClippingPolygonCollection(); + } + tileset.clippingPolygons.add(clippingPolygon); + } + } + } + + removeClipping(clippingPolygon: ClippingPolygon): void { + const globeClippingPolygons = this.viewer.scene.globe.clippingPolygons; + if ( + globeClippingPolygons && + globeClippingPolygons.contains(clippingPolygon) + ) { + globeClippingPolygons.remove(clippingPolygon); + } + + if (this.tiles3dCollection) { + for (let i = 0; i < this.tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = this.tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if ( + tileset.clippingPolygons && + tileset.clippingPolygons.contains(clippingPolygon) + ) { + tileset.clippingPolygons.remove(clippingPolygon); + } + } + } + } + + render(): HTMLTemplateResult | string { + return html`
+ + ${this.slicingActive + ? html` { + // todo + }} + @done="${() => { + this.finishSlicing(); + }}" + >` + : html` { + return {name: c.entity.name}; + })} + @remove=${(evt: {detail: number}) => { + const polygonToRemove = this.clippingPolygons[evt.detail]; + if (polygonToRemove) { + this.slicingDataSource.entities.removeById( + polygonToRemove.entity.id, + ); + this.removeClipping(polygonToRemove.clipping); + this.clippingPolygons.splice(evt.detail, 1); + this.requestUpdate(); + } + }} + @zoom=${(evt: {detail: number}) => { + const entToZoom = this.clippingPolygons[evt.detail]?.entity; + if (entToZoom) { + entToZoom.show = true; + this.viewer + .flyTo(entToZoom, { + duration: 0, + }) + .then(() => (entToZoom.show = false)) + .catch((e: Error) => console.error(e)); + } + }} + @edit=${(evt: {detail: number}) => { + const polToEdit = this.clippingPolygons[evt.detail]; + if (polToEdit) { + this.editingClipping = polToEdit; + this.removeClipping(polToEdit.clipping); + const positions = (<{positions: Cartesian3[]}>( + polToEdit.entity.polygon.hierarchy.getValue() + )).positions; + this.startDrawing(positions, polToEdit.entity); + this.addClippingPolygon(); + } + }} + @zoomEnter=${(e: {detail: number}) => { + const entToZoom = this.clippingPolygons[e.detail]?.entity; + if (entToZoom) entToZoom.show = true; + }} + @zoomOut=${(e: {detail: number}) => { + const entToZoom = this.clippingPolygons[e.detail]?.entity; + if (entToZoom) entToZoom.show = false; + }} + >`} +
`; + } +} diff --git a/src/plugins/ui/ngv-layers-list.ts b/src/plugins/ui/ngv-layers-list.ts index a5cfa40..2139f6e 100644 --- a/src/plugins/ui/ngv-layers-list.ts +++ b/src/plugins/ui/ngv-layers-list.ts @@ -10,6 +10,7 @@ export type LayerListOptions = { title?: string; showDeleteBtns?: boolean; showZoomBtns?: boolean; + showEditBtns?: boolean; }; @customElement('ngv-layers-list') @@ -37,8 +38,16 @@ export class NgvLayersList extends LitElement { .item { text-overflow: ellipsis; display: flex; - align-items: center; + flex-direction: column; column-gap: 10px; + row-gap: 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.16); + padding-bottom: 10px; + } + + .item:last-child { + border-bottom: none; + padding-bottom: 0; } .item span { @@ -46,6 +55,13 @@ export class NgvLayersList extends LitElement { text-overflow: ellipsis; } + .actions { + display: flex; + align-items: center; + justify-content: end; + column-gap: 5px; + } + button { border-radius: 4px; padding: 0 16px; @@ -65,27 +81,48 @@ export class NgvLayersList extends LitElement { ${this.layers.map( (l, i) => html`
- ${this.options?.showZoomBtns - ? html`` - : ''} ${l.name} - ${this.options?.showDeleteBtns - ? html`` + : ''} + ${this.options?.showEditBtns + ? html`` - : ''} + : ''} + ${this.options?.showDeleteBtns + ? html`` + : ''} +
`, )} `;