diff --git a/web/src/beta/lib/core/Crust/Plugins/api.ts b/web/src/beta/lib/core/Crust/Plugins/api.ts index ef0bff915..1152534f8 100644 --- a/web/src/beta/lib/core/Crust/Plugins/api.ts +++ b/web/src/beta/lib/core/Crust/Plugins/api.ts @@ -431,6 +431,10 @@ export function commonReearth({ computeGlobeHeight, toXYZ, toLngLatHeight, + convertScreenToPositionOffset, + isPositionVisible, + setView, + toWindowPosition, enableScreenSpaceCameraController, lookHorizontal, lookVertical, @@ -481,6 +485,10 @@ export function commonReearth({ computeGlobeHeight: GlobalThis["reearth"]["scene"]["computeGlobeHeight"]; toXYZ: GlobalThis["reearth"]["scene"]["toXYZ"]; toLngLatHeight: GlobalThis["reearth"]["scene"]["toLngLatHeight"]; + convertScreenToPositionOffset: GlobalThis["reearth"]["scene"]["convertScreenToPositionOffset"]; + isPositionVisible: GlobalThis["reearth"]["scene"]["isPositionVisible"]; + setView: GlobalThis["reearth"]["camera"]["setView"]; + toWindowPosition: GlobalThis["reearth"]["scene"]["toWindowPosition"]; inEditor: () => GlobalThis["reearth"]["scene"]["inEditor"]; built: () => GlobalThis["reearth"]["scene"]["built"]; enableScreenSpaceCameraController: GlobalThis["reearth"]["camera"]["enableScreenSpaceController"]; @@ -528,6 +536,7 @@ export function commonReearth({ moveRight, moveOverTerrain, flyToGround, + setView, }, get property() { return sceneProperty?.(); @@ -557,6 +566,9 @@ export function commonReearth({ computeGlobeHeight, toXYZ, toLngLatHeight, + convertScreenToPositionOffset, + isPositionVisible, + toWindowPosition, pickManyFromViewport, }, get viewport() { @@ -588,6 +600,7 @@ export function commonReearth({ moveRight, moveOverTerrain, flyToGround, + setView, }, layers: { get layersInViewport() { diff --git a/web/src/beta/lib/core/Crust/Plugins/hooks.ts b/web/src/beta/lib/core/Crust/Plugins/hooks.ts index 44ece12f5..69e1704f2 100644 --- a/web/src/beta/lib/core/Crust/Plugins/hooks.ts +++ b/web/src/beta/lib/core/Crust/Plugins/hooks.ts @@ -267,6 +267,34 @@ export default function ({ [engineRef], ); + const convertScreenToPositionOffset = useCallback( + (rawPosition: [x: number, y: number, z: number], screenOffset: [x: number, y: number]) => { + return engineRef?.convertScreenToPositionOffset(rawPosition, screenOffset); + }, + [engineRef], + ); + + const isPositionVisible = useCallback( + (position: [x: number, y: number, z: number]) => { + return !!engineRef?.isPositionVisible(position); + }, + [engineRef], + ); + + const setView = useCallback( + (camera: CameraPosition) => { + return engineRef?.setView(camera); + }, + [engineRef], + ); + + const toWindowPosition = useCallback( + (position: [x: number, y: number, z: number]) => { + return engineRef?.toWindowPosition(position); + }, + [engineRef], + ); + const enableScreenSpaceCameraController = useCallback( (enabled: boolean) => engineRef?.enableScreenSpaceCameraController(enabled), [engineRef], @@ -446,6 +474,10 @@ export default function ({ computeGlobeHeight, toXYZ, toLngLatHeight, + convertScreenToPositionOffset, + isPositionVisible, + setView, + toWindowPosition, rotateRight, orbit, captureScreen, @@ -511,6 +543,10 @@ export default function ({ computeGlobeHeight, toXYZ, toLngLatHeight, + convertScreenToPositionOffset, + isPositionVisible, + setView, + toWindowPosition, lookHorizontal, lookVertical, moveForward, diff --git a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts index ef6f70fbd..a6357012e 100644 --- a/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts +++ b/web/src/beta/lib/core/Crust/Plugins/plugin_types.ts @@ -141,6 +141,14 @@ export type Scene = { z: number, options?: { useGlobeEllipsoid?: boolean }, ) => [lng: number, lat: number, height: number] | undefined; + readonly convertScreenToPositionOffset: ( + rawPosition: [x: number, y: number, z: number], + screenOffset: [x: number, y: number], + ) => [x: number, y: number, z: number] | undefined; + readonly isPositionVisible: (position: [x: number, y: number, z: number]) => boolean; + readonly toWindowPosition: ( + position: [x: number, y: number, z: number], + ) => [x: number, y: number] | undefined; readonly pickManyFromViewport: ( windowPosition: [x: number, y: number], windowWidth: number, @@ -185,6 +193,7 @@ export type Camera = { options?: CameraOptions, offset?: number, ) => void; + readonly setView: (camera: CameraPosition) => void; }; export type Clock = { diff --git a/web/src/beta/lib/core/Crust/Plugins/storybook.tsx b/web/src/beta/lib/core/Crust/Plugins/storybook.tsx index 59aaa85a3..41aa1d63a 100644 --- a/web/src/beta/lib/core/Crust/Plugins/storybook.tsx +++ b/web/src/beta/lib/core/Crust/Plugins/storybook.tsx @@ -97,6 +97,9 @@ export const context: Context = { computeGlobeHeight: act("computeGlobeHeight"), toXYZ: act("toXYZ"), toLngLatHeight: act("toLngLatHeight"), + convertScreenToPositionOffset: act("convertScreenToPositionOffset"), + isPositionVisible: act("isPositionVisible"), + toWindowPosition: act("toWindowPosition"), pickManyFromViewport: act("pickManyFromViewport"), }, layers: { @@ -148,6 +151,7 @@ export const context: Context = { moveRight: act("moveRight"), moveOverTerrain: act("moveOverTerrain"), flyToGround: act("flyToGround"), + setView: act("setView"), }, clock: { startTime: new Date("2022-06-01"), diff --git a/web/src/beta/lib/core/Map/ref.ts b/web/src/beta/lib/core/Map/ref.ts index bfc115f4d..939c8f260 100644 --- a/web/src/beta/lib/core/Map/ref.ts +++ b/web/src/beta/lib/core/Map/ref.ts @@ -26,6 +26,10 @@ const engineRefKeys: FunctionKeys = { computeGlobeHeight: 1, toXYZ: 1, toLngLatHeight: 1, + convertScreenToPositionOffset: 1, + isPositionVisible: 1, + setView: 1, + toWindowPosition: 1, getViewport: 1, lookAt: 1, lookAtLayer: 1, diff --git a/web/src/beta/lib/core/Map/types/index.ts b/web/src/beta/lib/core/Map/types/index.ts index 41bb422df..467b526fc 100644 --- a/web/src/beta/lib/core/Map/types/index.ts +++ b/web/src/beta/lib/core/Map/types/index.ts @@ -18,6 +18,7 @@ import type { SelectedFeatureInfo, Feature, ComputedFeature, + CameraPosition, } from "../../mantle"; import type { CameraOptions, FlyTo, FlyToDestination, LookAtDestination } from "../../types"; import type { @@ -86,6 +87,15 @@ export type EngineRef = { z: number, options?: { useGlobeEllipsoid?: boolean }, ) => [lng: number, lat: number, height: number] | undefined; + convertScreenToPositionOffset: ( + rawPosition: [x: number, y: number, z: number], + screenOffset: [x: number, y: number], + ) => [x: number, y: number, z: number] | undefined; + isPositionVisible: (position: [x: number, y: number, z: number]) => boolean; + setView: (camera: CameraPosition) => void; + toWindowPosition: ( + position: [x: number, y: number, z: number], + ) => [x: number, y: number] | undefined; flyTo: FlyTo; lookAt: (destination: LookAtDestination, options?: CameraOptions) => void; lookAtLayer: (layerId: string) => void; diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.stories.tsx b/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.stories.tsx new file mode 100644 index 000000000..b9e35be51 --- /dev/null +++ b/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.stories.tsx @@ -0,0 +1,48 @@ +import { Meta, Story } from "@storybook/react"; + +import { engine } from "../.."; +import Component, { Props } from "../../../../Map"; + +export default { + component: Component, + parameters: { actions: { argTypesRegex: "^on.*" } }, +} as Meta; + +const Template: Story = args => ; + +export const Default = Template.bind([]); +Default.args = { + engine: "cesium", + engines: { + cesium: engine, + }, + ready: true, + layers: [ + { + id: "l", + type: "simple", + data: { + type: "geojson", + value: { + type: "Feature", + geometry: { + type: "Point", + coordinates: [0, 0, 1000], + }, + }, + }, + ellipse: { + radius: 1000, + fillColor: "#FF0000", + }, + }, + ], + property: { + tiles: [ + { + id: "default", + tile_type: "default", + }, + ], + }, +}; diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.tsx b/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.tsx new file mode 100644 index 000000000..d4df9abf8 --- /dev/null +++ b/web/src/beta/lib/core/engines/Cesium/Feature/Ellipse/index.tsx @@ -0,0 +1,94 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { CallbackProperty, Cartesian3, PositionProperty } from "cesium"; +import { useMemo } from "react"; +import { EllipseGraphics } from "resium"; + +import { toColor } from "@reearth/beta/utils/value"; + +import type { EllipseAppearance } from "../../.."; +import { classificationType, heightReference, shadowMode } from "../../common"; +import { + EntityExt, + toDistanceDisplayCondition, + toTimeInterval, + type FeatureComponentConfig, + type FeatureProps, +} from "../utils"; + +export type Props = FeatureProps; + +export type Property = EllipseAppearance; + +export default function Ellipse({ id, isVisible, property, geometry, layer, feature }: Props) { + const { show = true } = property ?? {}; + const coordinates = useMemo( + () => (geometry?.type === "Point" ? geometry.coordinates : undefined), + [geometry?.coordinates, geometry?.type], + ); + + const { + heightReference: hr, + shadows, + radius = 1000, + fillColor, + fill, + classificationType: ct, + } = property ?? {}; + const { useTransition, translate } = layer?.transition ?? {}; + + console.log(translate, radius); + + const pos = useMemo( + () => + coordinates + ? Cartesian3.fromDegrees(coordinates[0], coordinates[1], coordinates[2]) + : undefined, + [coordinates], + ); + const translateCallbackProperty = useMemo( + () => + useTransition && translate + ? new CallbackProperty(() => Cartesian3.fromDegrees(...translate), false) + : undefined, + [useTransition, translate], + ); + + const semiAxisProperty = useMemo( + () => (useTransition ? new CallbackProperty(() => radius, false) : radius), + [radius], + ); + + const material = useMemo(() => toColor(fillColor), [fillColor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); + const distanceDisplayCondition = useMemo( + () => toDistanceDisplayCondition(property?.near, property?.far), + [property?.near, property?.far], + ); + + return !isVisible || !pos || !show ? null : ( + + + + ); +} + +export const config: FeatureComponentConfig = { + noLayer: true, +}; diff --git a/web/src/beta/lib/core/engines/Cesium/Feature/index.tsx b/web/src/beta/lib/core/engines/Cesium/Feature/index.tsx index a92e0a85b..2524c2ea5 100644 --- a/web/src/beta/lib/core/engines/Cesium/Feature/index.tsx +++ b/web/src/beta/lib/core/engines/Cesium/Feature/index.tsx @@ -6,6 +6,7 @@ import { ComputedFeature, DataType, guessType } from "@reearth/beta/lib/core/man import type { AppearanceTypes, FeatureComponentProps, ComputedLayer } from "../.."; import Box, { config as boxConfig } from "./Box"; +import Ellipse, { config as ellipseConfig } from "./Ellipse"; import Ellipsoid, { config as ellipsoidConfig } from "./Ellipsoid"; import Frustum, { config as frustumConfig } from "./Frustum"; import Marker, { config as markerConfig } from "./Marker"; @@ -41,6 +42,7 @@ const components: Record< polyline: [Polyline, polylineConfig], polygon: [Polygon, polygonConfig], ellipsoid: [Ellipsoid, ellipsoidConfig], + ellipse: [Ellipse, ellipseConfig], model: [Model, modelConfig], "3dtiles": [Tileset, tilesetConfig], box: [Box, boxConfig], diff --git a/web/src/beta/lib/core/engines/Cesium/useEngineRef.ts b/web/src/beta/lib/core/engines/Cesium/useEngineRef.ts index 9e11b71c7..48f3ebf5f 100644 --- a/web/src/beta/lib/core/engines/Cesium/useEngineRef.ts +++ b/web/src/beta/lib/core/engines/Cesium/useEngineRef.ts @@ -1,3 +1,4 @@ +import { EllipsoidalOccluder } from "@cesium/engine"; import * as Cesium from "cesium"; import { ClockStep, JulianDate, Math as CesiumMath } from "cesium"; import { useImperativeHandle, Ref, RefObject, useMemo, useRef } from "react"; @@ -150,6 +151,99 @@ export default function useEngineRef( cart.height, ]; }, + // Calculate window position from WGS coordinates. + // TODO: We might need to support other WGS, but it's only WGS84 for now. + toWindowPosition: (position: [x: number, y: number, z: number]) => { + const viewer = cesium.current?.cesiumElement; + if (!viewer || viewer.isDestroyed()) return; + const result = Cesium.SceneTransforms.wgs84ToWindowCoordinates( + viewer.scene, + Cesium.Cartesian3.fromElements(...position), + ); + return [result.x, result.y]; + }, + // Calculate next positino from screen(window) offset. + // Ref: https://github.com/takram-design-engineering/plateau-view/blob/6c8225d626cd8085e5d10ffe8980837814c333b0/libs/pedestrian/src/convertScreenToPositionOffset.ts + convertScreenToPositionOffset: (rawPosition, screenOffset) => { + const viewer = cesium.current?.cesiumElement; + if (!viewer || viewer.isDestroyed()) return; + + const position = Cesium.Cartesian3.fromElements(...rawPosition); + + const resultScratch = new Cesium.Cartesian3(); + const cartographicScratch = new Cesium.Cartographic(); + const radiiScratch = new Cesium.Cartesian3(); + const ellipsoidScratch = new Cesium.Ellipsoid(); + const rayScratch = new Cesium.Ray(); + const projectionScratch = new Cesium.Cartesian3(); + + const scene = viewer.scene; + const ellipsoid = scene.globe.ellipsoid; + let cartographic; + try { + cartographic = Cesium.Cartographic.fromCartesian( + position, + ellipsoid, + cartographicScratch, + ); + } catch (error) { + return; + } + radiiScratch.x = ellipsoid.radii.x + cartographic.height; + radiiScratch.y = ellipsoid.radii.y + cartographic.height; + radiiScratch.z = ellipsoid.radii.z + cartographic.height; + const offsetEllipsoid = Cesium.Ellipsoid.fromCartesian3(radiiScratch, ellipsoidScratch); + const windowPosition = new Cesium.Cartesian2(); + try { + [windowPosition.x, windowPosition.y] = e.toWindowPosition(rawPosition) ?? [0, 0]; + } catch (error) { + return; + } + windowPosition.x += screenOffset[0]; + windowPosition.y += screenOffset[1]; + const ray = scene.camera.getPickRay(windowPosition, rayScratch); + if (ray == null) { + return; + } + const intersection = Cesium.IntersectionTests.rayEllipsoid(ray, offsetEllipsoid); + if (intersection == null) { + return; + } + const projection = Cesium.Ray.getPoint(ray, intersection.start, projectionScratch); + const result = Cesium.Cartesian3.subtract(projection, position, resultScratch); + return [result.x, result.y, result.z]; + }, + // Check if the position is visible on globe. + isPositionVisible: position => { + const viewer = cesium.current?.cesiumElement; + if (!viewer || viewer.isDestroyed()) return false; + const occluder = new EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, Cesium.Cartesian3.ZERO); + occluder.cameraPosition = viewer.scene.camera.position; + return occluder.isPointVisible(Cesium.Cartesian3.fromElements(...position)); + }, + setView: camera => { + const viewer = cesium.current?.cesiumElement; + if (!viewer || viewer.isDestroyed()) return false; + const scene = viewer.scene; + if (camera.lng || camera.lat || camera.height) { + const xyz = Cesium.Cartesian3.fromDegrees(camera.lng, camera.lat, camera.height); + scene.camera.position.x = xyz.x; + scene.camera.position.y = xyz.y; + scene.camera.position.z = xyz.z; + } + if (camera.heading || camera.pitch) { + scene.camera.setView({ + orientation: { + heading: camera.heading, + pitch: camera.pitch, + }, + }); + } + if (camera.fov && scene.camera.frustum instanceof Cesium.PerspectiveFrustum) { + scene.camera.frustum.fov = camera.fov; + } + return; + }, flyTo: (target, options) => { if (target && typeof target === "object") { const viewer = cesium.current?.cesiumElement; diff --git a/web/src/beta/lib/core/engines/index.ts b/web/src/beta/lib/core/engines/index.ts index b86499d9c..a7663a1bc 100644 --- a/web/src/beta/lib/core/engines/index.ts +++ b/web/src/beta/lib/core/engines/index.ts @@ -22,6 +22,7 @@ export type { export type { Cesium3DTilesAppearance, EllipsoidAppearance, + EllipseAppearance, PolygonAppearance, PolylineAppearance, MarkerAppearance, diff --git a/web/src/beta/lib/core/mantle/types/appearance.ts b/web/src/beta/lib/core/mantle/types/appearance.ts index 76d9153d7..7dfeace70 100644 --- a/web/src/beta/lib/core/mantle/types/appearance.ts +++ b/web/src/beta/lib/core/mantle/types/appearance.ts @@ -24,6 +24,7 @@ export type AppearanceTypes = { model: ModelAppearance; "3dtiles": Cesium3DTilesAppearance; ellipsoid: EllipsoidAppearance; + ellipse: EllipseAppearance; box: BoxAppearance; photooverlay: LegacyPhotooverlayAppearance; resource: ResourceAppearance; @@ -113,6 +114,18 @@ export type EllipsoidAppearance = { far?: number; }; +export type EllipseAppearance = { + show?: boolean; + heightReference?: "none" | "clamp" | "relative"; + classificationType?: ClassificationType; + shadows?: "disabled" | "enabled" | "cast_only" | "receive_only"; + radius?: number; + fill?: boolean; + fillColor?: string; + near?: number; + far?: number; +}; + export type ModelAppearance = { show?: boolean; model?: string; // For compat @@ -257,6 +270,7 @@ export const appearanceKeyObj: { [k in keyof AppearanceTypes]: 1 } = { polyline: 1, polygon: 1, ellipsoid: 1, + ellipse: 1, model: 1, "3dtiles": 1, box: 1,