diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/Edge.tsx b/src/components/molecules/Visualizer/Engine/Cesium/Box/Edge.tsx new file mode 100644 index 000000000..285e4d696 --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/Edge.tsx @@ -0,0 +1,73 @@ +import { ArcType, Cartesian3, Color, TranslationRotationScale } from "cesium"; +import { FC, memo } from "react"; +import { Entity, PolylineGraphics } from "resium"; + +import { EventCallback } from "@reearth/util/event"; + +import { useHooks } from "./hooks/edge"; + +export type EdgeEventCallback = EventCallback< + [ + event: any, + edgeEvent: { + layerId: string; + index: number; + }, + ] +>; + +export type EdgeProperties = { start: Cartesian3; end: Cartesian3; isDraggable?: boolean }; + +type Props = { + id: string; + index: number; + edge: EdgeProperties; + trs: TranslationRotationScale; + isHovered: boolean; + fillColor?: Color; + hoverColor?: Color; + width?: number; + onMouseDown?: EdgeEventCallback; + onMouseMove?: EdgeEventCallback; + onMouseUp?: EdgeEventCallback; + onMouseEnter?: EdgeEventCallback; + onMouseLeave?: EdgeEventCallback; +}; + +export const Edge: FC = memo(function EdgePresenter({ + id, + index, + edge, + isHovered, + fillColor, + trs, + width, + hoverColor, + onMouseDown, + onMouseMove, + onMouseUp, +}) { + const { cbp, outlineColor } = useHooks({ + id, + index, + edge, + isHovered, + fillColor, + trs, + hoverColor, + onMouseDown, + onMouseMove, + onMouseUp, + }); + + return ( + + + + ); +}); diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/ScalePoints.tsx b/src/components/molecules/Visualizer/Engine/Cesium/Box/ScalePoints.tsx new file mode 100644 index 000000000..ab0f824bf --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/ScalePoints.tsx @@ -0,0 +1,126 @@ +import { ArcType, Cartesian3, Color, TranslationRotationScale } from "cesium"; +import { FC, memo } from "react"; +import { BoxGraphics, Entity, PolylineGraphics } from "resium"; + +import { EventCallback } from "@reearth/util/event"; + +import { useHooks } from "./hooks/scalePoint"; + +export type ScalePointProperties = { + point: Cartesian3; + oppositePoint: Cartesian3; +}; + +export type PointEventCallback = EventCallback< + [ + event: any, + pointEvent: { + index: number; + layerId: string; + opposite: boolean; + position?: Cartesian3; + oppositePosition?: Cartesian3; + pointLocal?: Cartesian3; + }, + ] +>; + +type Props = { + id: string; + index: number; + scalePoint: ScalePointProperties; + trs: TranslationRotationScale; + isHovered: boolean; + pointFillColor?: Color; + pointOutlineColor?: Color; + hoverPointOutlineColor?: Color; + pointOutlineWidth?: number; + axisLineColor?: Color; + axisLineWidth?: number; + dimensions?: { width: number; height: number; length: number }; + visiblePoint?: boolean; + visibleAxisLine?: boolean; + onPointMouseDown?: PointEventCallback; + onPointMouseMove?: PointEventCallback; + onPointMouseUp?: PointEventCallback; +}; + +export const ScalePoints: FC = memo(function ScalePointsPresenter({ + id, + index, + scalePoint, + isHovered, + pointFillColor, + pointOutlineColor, + hoverPointOutlineColor, + pointOutlineWidth, + axisLineColor, + axisLineWidth, + trs, + dimensions, + visiblePoint, + visibleAxisLine, + onPointMouseDown, + onPointMouseMove, + onPointMouseUp, +}) { + const { + entitiesPosition, + pointOutlineColorCb, + cesiumDimensionsCallbackProperty, + orientation, + axisColorProperty, + } = useHooks({ + id, + index, + scalePoint, + isHovered, + pointOutlineColor, + hoverPointOutlineColor, + axisLineColor, + trs, + dimensions, + onPointMouseDown, + onPointMouseMove, + onPointMouseUp, + }); + + return ( + <> + + + + + + + + + + + ); +}); diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/Side.tsx b/src/components/molecules/Visualizer/Engine/Cesium/Box/Side.tsx index 94f765114..b6f2a19b7 100644 --- a/src/components/molecules/Visualizer/Engine/Cesium/Box/Side.tsx +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/Side.tsx @@ -1,75 +1,46 @@ -import { - Axis, - CallbackProperty, - Cartesian2, - Cartesian3, - Color, - Matrix4, - Plane as CesiumPlane, - PositionProperty, - TranslationRotationScale, -} from "cesium"; -import { useMemo, FC, memo } from "react"; +import { Color, Plane as CesiumPlane, TranslationRotationScale } from "cesium"; +import { FC, memo } from "react"; import { Entity, PlaneGraphics } from "resium"; -// ref: https://github.com/TerriaJS/terriajs/blob/cad62a45cbee98c7561625458bec3a48510f6cbc/lib/Models/BoxDrawing.ts#L1446-L1461 -function setPlaneDimensions( - boxDimensions: Cartesian3, - planeNormalAxis: Axis, - planeDimensions: Cartesian2, -) { - if (planeNormalAxis === Axis.X) { - planeDimensions.x = boxDimensions.y; - planeDimensions.y = boxDimensions.z; - } else if (planeNormalAxis === Axis.Y) { - planeDimensions.x = boxDimensions.x; - planeDimensions.y = boxDimensions.z; - } else if (planeNormalAxis === Axis.Z) { - planeDimensions.x = boxDimensions.x; - planeDimensions.y = boxDimensions.y; - } -} +import { useHooks } from "./hooks/side"; export const Side: FC<{ id: string; planeLocal: CesiumPlane; + isActive: boolean; trs: TranslationRotationScale; - style: { - fillColor?: Color; - outlineColor?: Color; - outlineWidth?: number; - fill?: boolean; - outline?: boolean; - }; -}> = memo(function SidePresenter({ id, planeLocal, style, trs }) { - const normalAxis = planeLocal.normal.x ? Axis.X : planeLocal.normal.y ? Axis.Y : Axis.Z; - const cbRef = useMemo( - () => new CallbackProperty(() => trs.translation, false) as unknown as PositionProperty, - [trs], - ); - const [plane, dimension] = useMemo(() => { - const dimension = new Cartesian3(); - setPlaneDimensions(trs.scale, normalAxis, dimension); - const scratchScaleMatrix = new Matrix4(); - const scaleMatrix = Matrix4.fromScale(trs.scale, scratchScaleMatrix); - const plane = CesiumPlane.transform( - planeLocal, - scaleMatrix, - new CesiumPlane(Cartesian3.UNIT_Z, 0), - ); - return [plane, dimension]; - }, [trs, normalAxis, planeLocal]); + fillColor?: Color; + outlineColor?: Color; + activeOutlineColor?: Color; + fill?: boolean; +}> = memo(function SidePresenter({ + id, + planeLocal, + isActive, + fill, + fillColor, + outlineColor, + activeOutlineColor, + trs, +}) { + const { cbRef, plane, dimension, orientation, outlineColorCb } = useHooks({ + planeLocal, + isActive, + outlineColor, + activeOutlineColor, + trs, + }); return ( - + ); diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/constants.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/constants.ts new file mode 100644 index 000000000..0f8481b96 --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/constants.ts @@ -0,0 +1,63 @@ +// The 6 box sides defined as planes in local coordinate space. + +import { Cartesian3, Plane as CesiumPlane } from "cesium"; + +import { EdgeProperties } from "./Edge"; + +// ref: https://github.com/TerriaJS/terriajs/blob/cad62a45cbee98c7561625458bec3a48510f6cbc/lib/Models/BoxDrawing.ts#L161-L169 +export const SIDE_PLANES: readonly CesiumPlane[] = [ + new CesiumPlane(new Cartesian3(0, 0, 1), 0.5), + new CesiumPlane(new Cartesian3(0, 0, -1), 0.5), + new CesiumPlane(new Cartesian3(0, 1, 0), 0.5), + new CesiumPlane(new Cartesian3(0, -1, 0), 0.5), + new CesiumPlane(new Cartesian3(1, 0, 0), 0.5), + new CesiumPlane(new Cartesian3(-1, 0, 0), 0.5), +]; + +const CORNER_POINT_VECTORS = [ + new Cartesian3(0.5, 0.5, 0.5), + new Cartesian3(0.5, -0.5, 0.5), + new Cartesian3(-0.5, -0.5, 0.5), + new Cartesian3(-0.5, 0.5, 0.5), +]; + +const FACE_POINT_VECTORS = [ + new Cartesian3(0.5, 0.0, 0.0), + new Cartesian3(0.0, 0.5, 0.0), + new Cartesian3(0.0, 0.0, 0.5), +]; + +// The box has 8 corner points and 6 face points that act as scaling grips. +// Here we represent them as 7 vectors in local coordinates space. +// Each vector represents a point and its opposite points can be easily derived from it. +// @see https://github.com/TerriaJS/terriajs/blob/cad62a45cbee98c7561625458bec3a48510f6cbc/lib/Models/BoxDrawing.ts#L184-L187 +const SCALE_POINT_VECTORS = [...CORNER_POINT_VECTORS, ...FACE_POINT_VECTORS]; + +export const SCALE_POINTS = SCALE_POINT_VECTORS.map(vector => { + return { + point: vector, + oppositePoint: Cartesian3.multiplyByScalar(vector, -1, new Cartesian3()), + }; +}); + +// Calculate edge of box. The box has 12 edge. +// In this logic, calculate 3 edges(vertical edge, top edge, bottom edge) in each `CORNER_POINT_VECTORS`. +export const BOX_EDGES: EdgeProperties[] = CORNER_POINT_VECTORS.flatMap((vector, i) => { + const upPoint = vector; + const downPoint = Cartesian3.clone(upPoint, new Cartesian3()); + // Set point to bottom + downPoint.z *= -1; + + const nextUpPoint = CORNER_POINT_VECTORS[(i + 1) % 4]; + const nextDownPoint = Cartesian3.clone(nextUpPoint, new Cartesian3()); + nextDownPoint.z *= -1; + + const verticalEdge: EdgeProperties = { start: upPoint, end: downPoint, isDraggable: true }; + const topEdge: EdgeProperties = { start: nextUpPoint, end: upPoint }; + const bottomEdge: EdgeProperties = { start: nextDownPoint, end: downPoint }; + return [verticalEdge, topEdge, bottomEdge]; +}); + +// This is used for plane ID. +// We can use this name, for example you want to move the box when front plane is dragged. +export const SIDE_PLANE_NAMES = ["bottom", "top", "front", "back", "left", "right"]; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/box.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/box.ts new file mode 100644 index 000000000..7e14eef2a --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/box.ts @@ -0,0 +1,289 @@ +import { + Cartesian3, + HeadingPitchRoll, + Quaternion, + Math as CesiumMath, + TranslationRotationScale, + Cartesian2, + Cartographic, +} from "cesium"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCesium } from "resium"; + +import { useContext } from "@reearth/components/molecules/Visualizer/Plugin"; +import { toColor } from "@reearth/util/value"; + +import { Property } from ".."; +import { SceneProperty } from "../../.."; +import type { Props as PrimitiveProps } from "../../../../Primitive"; +import { sampleTerrainHeightFromCartesian } from "../../common"; +import { EdgeEventCallback } from "../Edge"; +import { PointEventCallback } from "../ScalePoints"; +import { computeMouseMoveAmount, updateTrs } from "../utils"; + +export const useHooks = ({ + layer, + sceneProperty, +}: PrimitiveProps) => { + const { viewer } = useCesium(); + const ctx = useContext(); + const { property } = layer ?? {}; + const { + fillColor, + outlineColor, + activeOutlineColor, + outlineWidth, + draggableOutlineColor, + activeDraggableOutlineColor, + draggableOutlineWidth, + pointFillColor, + pointOutlineColor, + activePointOutlineColor, + axisLineColor, + cursor, + } = property?.default ?? {}; + const { allowEnterGround } = sceneProperty?.default || {}; + + const [terrainHeightEstimate, setTerrainHeightEstimate] = useState(0); + const [trs] = useState(() => + updateTrs(new TranslationRotationScale(), property, terrainHeightEstimate, allowEnterGround), + ); + + const inProgressSamplingTerrainHeight = useRef(false); + const updateTerrainHeight = useCallback(() => { + if (inProgressSamplingTerrainHeight.current) { + return; + } + + if (!allowEnterGround) { + inProgressSamplingTerrainHeight.current = true; + sampleTerrainHeightFromCartesian(viewer.scene, trs.translation).then(v => { + setTerrainHeightEstimate(v ?? 0); + inProgressSamplingTerrainHeight.current = false; + }); + } + }, [allowEnterGround, viewer, trs]); + + useEffect(() => { + updateTrs(trs, property, terrainHeightEstimate, allowEnterGround); + updateTerrainHeight(); + }, [property, trs, viewer, terrainHeightEstimate, updateTerrainHeight, allowEnterGround]); + + const style = useMemo( + () => ({ + fillColor: toColor(fillColor), + outlineColor: toColor(outlineColor), + activeOutlineColor: toColor(activeOutlineColor), + draggableOutlineColor: toColor(draggableOutlineColor), + activeDraggableOutlineColor: toColor(activeDraggableOutlineColor), + outlineWidth, + draggableOutlineWidth, + fill: !!fillColor, + outline: !!outlineColor, + }), + [ + fillColor, + outlineColor, + activeOutlineColor, + outlineWidth, + draggableOutlineWidth, + draggableOutlineColor, + activeDraggableOutlineColor, + ], + ); + + const scalePointStyle = useMemo( + () => ({ + pointFillColor: toColor(pointFillColor) ?? style.fillColor, + pointOutlineColor: toColor(pointOutlineColor) ?? style.outlineColor, + activePointOutlineColor: toColor(activePointOutlineColor) ?? style.outlineColor, + axisLineColor: toColor(axisLineColor) ?? style.outlineColor, + }), + [pointFillColor, pointOutlineColor, axisLineColor, style, activePointOutlineColor], + ); + + // ScalePoint event handlers + const currentPointIndex = useRef(); + const handlePointMouseDown: PointEventCallback = useCallback((_, { index }) => { + currentPointIndex.current = index; + }, []); + const prevMousePosition2dForPoint = useRef(); + const handlePointMouseMove: PointEventCallback = useCallback( + (e, { position, oppositePosition, pointLocal, index, layerId }) => { + if (currentPointIndex.current !== index || !position || !oppositePosition || !pointLocal) { + return; + } + if (prevMousePosition2dForPoint.current === undefined) { + prevMousePosition2dForPoint.current = new Cartesian2(e.x, e.y); + return; + } + + const currentMousePosition2d = new Cartesian2(e.x, e.y); + + const axisVector = Cartesian3.subtract(position, oppositePosition, new Cartesian3()); + const length = Cartesian3.magnitude(axisVector); + const scaleDirection = Cartesian3.normalize(axisVector, new Cartesian3()); + + const { scaleAmount, pixelLengthAfterScaling } = computeMouseMoveAmount( + viewer.scene, + { + startPosition: prevMousePosition2dForPoint.current, + endPosition: currentMousePosition2d, + }, + position, + scaleDirection, + length, + ); + + prevMousePosition2dForPoint.current = currentMousePosition2d.clone(); + + const axisLocal = Cartesian3.normalize(pointLocal, new Cartesian3()); + const xDot = Math.abs(Cartesian3.dot(new Cartesian3(1, 0, 0), axisLocal)); + const yDot = Math.abs(Cartesian3.dot(new Cartesian3(0, 1, 0), axisLocal)); + const zDot = Math.abs(Cartesian3.dot(new Cartesian3(0, 0, 1), axisLocal)); + + const isProportionalScaling = xDot && yDot && zDot; + + // When downscaling, stop at 20px length. + if (scaleAmount < 0) { + const isDiagonal = axisLocal.x && axisLocal.y && axisLocal.y; + const pixelSideLengthAfterScaling = isDiagonal + ? pixelLengthAfterScaling / Math.sqrt(2) + : pixelLengthAfterScaling; + if (pixelSideLengthAfterScaling < 20) { + // Do nothing if scaling down will make the box smaller than 20px + return; + } + } + + // Compute scale components along xyz + const scaleStep = Cartesian3.multiplyByScalar( + // Taking abs because scaling step is independent of axis direction + // Scaling step is negative when scaling down and positive when scaling up + Cartesian3.abs( + // Extract scale components along the axis + Cartesian3.multiplyComponents( + trs.scale, + // For proportional scaling we scale equally along xyz + isProportionalScaling ? new Cartesian3(1, 1, 1) : axisLocal, + new Cartesian3(), + ), + new Cartesian3(), + ), + scaleAmount, + new Cartesian3(), + ); + + // Move the box by half the scale amount in the direction of scaling so + // that the opposite end remains stationary. + const moveStep = Cartesian3.multiplyByScalar(axisVector, scaleAmount / 2, new Cartesian3()); + + // Prevent scaling in Z axis if it will result in the box going underground. + const isDraggingBottomScalePoint = axisLocal.z < 0; + const isUpscaling = scaleAmount > 0; + if (!allowEnterGround && isUpscaling && isDraggingBottomScalePoint) { + const boxCenterHeight = Cartographic.fromCartesian( + trs.translation, + undefined, + new Cartographic(), + ).height; + const bottomHeight = boxCenterHeight - trs.scale.z / 2; + const bottomHeightAfterScaling = bottomHeight - Math.abs(moveStep.z); + if (bottomHeightAfterScaling < 0) { + scaleStep.z = 0; + } + } + + const nextScale = new Cartesian3(); + const nextTranslation = new Cartesian3(); + + Cartesian3.add(trs.scale, scaleStep, nextScale); + Cartesian3.add(trs.translation, moveStep, nextTranslation); + + const cartographic = viewer?.scene.globe.ellipsoid.cartesianToCartographic( + nextTranslation, + ) as Cartographic; + + ctx?.emit("layeredit", { + layerId, + scale: { + width: nextScale.x, + length: nextScale.y, + height: nextScale.z, + location: { + lat: CesiumMath.toDegrees(cartographic?.latitude), + lng: CesiumMath.toDegrees(cartographic?.longitude), + height: cartographic?.height, + }, + }, + }); + }, + [trs, viewer, allowEnterGround, ctx], + ); + const handlePointMouseUp: PointEventCallback = useCallback(() => { + currentPointIndex.current = undefined; + prevMousePosition2dForPoint.current = undefined; + }, []); + + // Edge event handlers + const currentEdgeIndex = useRef(); + const handleEdgeMouseDown: EdgeEventCallback = useCallback((_, { index }) => { + currentEdgeIndex.current = index; + }, []); + const prevMouseXAxisForEdge = useRef(); + const handleEdgeMouseMove: EdgeEventCallback = useCallback( + (e, { index, layerId }) => { + if (currentEdgeIndex.current !== index) { + return; + } + if (prevMouseXAxisForEdge.current === undefined) { + prevMouseXAxisForEdge.current = e.x; + return; + } + const dx = e.x - prevMouseXAxisForEdge.current; + prevMouseXAxisForEdge.current = e.x; + const sensitivity = 0.05; + const hpr = new HeadingPitchRoll(0, 0, 0); + // -dx because the screen coordinates is opposite to local coordinates space. + hpr.heading = -dx * sensitivity; + hpr.pitch = 0; + hpr.roll = 0; + + const nextRotation = new Quaternion(); + + Quaternion.multiply(trs.rotation, Quaternion.fromHeadingPitchRoll(hpr), nextRotation); + + const { heading, pitch, roll } = HeadingPitchRoll.fromQuaternion(nextRotation); + + ctx?.emit("layeredit", { + layerId, + rotate: { + heading, + pitch, + roll, + }, + }); + }, + [trs, ctx], + ); + const handleEdgeMouseUp: EdgeEventCallback = useCallback(() => { + currentEdgeIndex.current = undefined; + prevMouseXAxisForEdge.current = undefined; + }, []); + + useEffect(() => { + document.body.style.cursor = cursor || "default"; + }, [cursor]); + + return { + style, + trs, + scalePointStyle, + handlePointMouseDown, + handlePointMouseMove, + handlePointMouseUp, + handleEdgeMouseDown, + handleEdgeMouseMove, + handleEdgeMouseUp, + }; +}; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/edge.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/edge.ts new file mode 100644 index 000000000..0a86c18da --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/edge.ts @@ -0,0 +1,108 @@ +import { + CallbackProperty, + Cartesian3, + Color, + ColorMaterialProperty, + Matrix4, + TranslationRotationScale, +} from "cesium"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { useContext } from "@reearth/components/molecules/Visualizer/Plugin"; +import { EventCallback } from "@reearth/util/event"; + +import { EdgeEventCallback, EdgeProperties } from "../Edge"; + +export const useHooks = ({ + id, + index, + edge, + isHovered, + fillColor, + trs, + hoverColor, + onMouseDown, + onMouseMove, + onMouseUp, +}: { + id: string; + index: number; + edge: EdgeProperties; + trs: TranslationRotationScale; + isHovered: boolean; + fillColor?: Color; + hoverColor?: Color; + width?: number; + onMouseDown?: EdgeEventCallback; + onMouseMove?: EdgeEventCallback; + onMouseUp?: EdgeEventCallback; + onMouseEnter?: EdgeEventCallback; + onMouseLeave?: EdgeEventCallback; +}) => { + const ctx = useContext(); + const [cbp] = useState( + () => + new CallbackProperty(() => { + const position1 = new Cartesian3(); + const position2 = new Cartesian3(); + const matrix = Matrix4.fromTranslationRotationScale(trs); + Matrix4.multiplyByPoint(matrix, edge.start, position1); + Matrix4.multiplyByPoint(matrix, edge.end, position2); + return [position1, position2]; + }, false), + ); + + const isEdgeHovered = useRef(false); + const [outlineColor] = useState( + () => + new ColorMaterialProperty( + new CallbackProperty( + () => (isEdgeHovered.current ? hoverColor ?? fillColor : fillColor), + false, + ), + ), + ); + useEffect(() => { + isEdgeHovered.current = isHovered; + }, [isHovered]); + + const handleMouseDown: EventCallback = useCallback( + e => { + if (e.layerId !== id) { + return; + } + onMouseDown?.(e, { layerId: id, index }); + }, + [onMouseDown, id, index], + ); + + const handleMouseMove: EventCallback = useCallback( + e => { + onMouseMove?.(e, { layerId: id, index }); + }, + [onMouseMove, index, id], + ); + + const handleMouseUp: EventCallback = useCallback( + e => { + onMouseUp?.(e, { layerId: id, index }); + }, + [onMouseUp, index, id], + ); + + useEffect(() => { + ctx?.reearth.on("mousedown", handleMouseDown); + ctx?.reearth.on("mousemove", handleMouseMove); + ctx?.reearth.on("mouseup", handleMouseUp); + return () => { + ctx?.reearth.off("mousedown", handleMouseDown); + ctx?.reearth.off("mousemove", handleMouseMove); + ctx?.reearth.off("mouseup", handleMouseUp); + }; + }, [ctx, handleMouseDown, handleMouseMove, handleMouseUp]); + + return { + cbp, + outlineColor, + }; +}; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/scalePoint.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/scalePoint.ts new file mode 100644 index 000000000..683aba992 --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/scalePoint.ts @@ -0,0 +1,171 @@ +import { + CallbackProperty, + Cartesian3, + Color, + JulianDate, + Matrix4, + PolylineDashMaterialProperty, + PositionProperty, + Quaternion, + TranslationRotationScale, +} from "cesium"; +import { useCallback, useMemo, useState, useEffect, useRef } from "react"; + +import { useContext } from "@reearth/components/molecules/Visualizer/Plugin"; +import { EventCallback } from "@reearth/util/event"; + +import { PointEventCallback, ScalePointProperties } from "../ScalePoints"; + +export const useHooks = ({ + id, + index, + scalePoint, + isHovered, + pointOutlineColor, + hoverPointOutlineColor, + axisLineColor, + trs, + dimensions, + onPointMouseDown, + onPointMouseMove, + onPointMouseUp, +}: { + id: string; + index: number; + scalePoint: ScalePointProperties; + trs: TranslationRotationScale; + isHovered: boolean; + pointOutlineColor?: Color; + hoverPointOutlineColor?: Color; + axisLineColor?: Color; + dimensions?: { width: number; height: number; length: number }; + onPointMouseDown?: PointEventCallback; + onPointMouseMove?: PointEventCallback; + onPointMouseUp?: PointEventCallback; +}) => { + const layerId = `${id}-${index}`; + const oppositeLayerId = `${id}-opposite-${index}`; + + const ctx = useContext(); + + const [entitiesPosition] = useState(() => { + const point = new Cartesian3(); + const oppositePoint = new Cartesian3(); + + const pointProperty = new CallbackProperty(() => { + const matrix = Matrix4.fromTranslationRotationScale(trs); + Matrix4.multiplyByPoint(matrix, scalePoint.point, point); + return point; + }, false) as unknown as PositionProperty; + const oppositePointProperty = new CallbackProperty(() => { + const matrix = Matrix4.fromTranslationRotationScale(trs); + Matrix4.multiplyByPoint(matrix, scalePoint.oppositePoint, oppositePoint); + return oppositePoint; + }, false) as unknown as PositionProperty; + + return { + point: pointProperty, + oppositePoint: oppositePointProperty, + axisLine: new CallbackProperty( + () => [ + pointProperty.getValue(JulianDate.now()), + oppositePointProperty.getValue(JulianDate.now()), + ], + false, + ), + }; + }); + + const isScalePointHovered = useRef(false); + const [pointOutlineColorCb] = useState( + () => + new CallbackProperty( + () => (isScalePointHovered.current ? hoverPointOutlineColor : pointOutlineColor), + false, + ), + ); + useEffect(() => { + isScalePointHovered.current = isHovered; + }, [isHovered]); + + const [cesiumDimensions] = useState( + () => new Cartesian3(dimensions?.width, dimensions?.length, dimensions?.height), + ); + const [cesiumDimensionsCallbackProperty] = useState( + () => new CallbackProperty(() => cesiumDimensions, false), + ); + const [orientation] = useState(() => new CallbackProperty(() => Quaternion.IDENTITY, false)); + const axisColorProperty = useMemo( + () => new PolylineDashMaterialProperty({ color: axisLineColor, dashLength: 8 }), + [axisLineColor], + ); + + const isOppositePointClicked = useRef(false); + const handlePointMouseDown: EventCallback = useCallback( + e => { + isOppositePointClicked.current = e.layerId === oppositeLayerId; + if (e.layerId === layerId || e.layerId === oppositeLayerId) { + onPointMouseDown?.(e, { + layerId: isOppositePointClicked.current ? oppositeLayerId : layerId, + index, + opposite: isOppositePointClicked.current, + }); + } + }, + [onPointMouseDown, index, layerId, oppositeLayerId], + ); + const handlePointMouseMove: EventCallback = useCallback( + e => { + onPointMouseMove?.(e, { + layerId: isOppositePointClicked.current ? oppositeLayerId : layerId, + index, + opposite: isOppositePointClicked.current, + position: isOppositePointClicked.current + ? entitiesPosition.oppositePoint.getValue(JulianDate.now()) + : entitiesPosition.point.getValue(JulianDate.now()), + oppositePosition: isOppositePointClicked.current + ? entitiesPosition.point.getValue(JulianDate.now()) + : entitiesPosition.oppositePoint.getValue(JulianDate.now()), + pointLocal: isOppositePointClicked ? scalePoint.oppositePoint : scalePoint.point, + }); + }, + [onPointMouseMove, index, entitiesPosition, scalePoint, layerId, oppositeLayerId], + ); + const handlePointMouseUp: EventCallback = useCallback( + e => { + onPointMouseUp?.(e, { + layerId: isOppositePointClicked.current ? oppositeLayerId : layerId, + index, + opposite: isOppositePointClicked.current, + }); + }, + [onPointMouseUp, index, layerId, oppositeLayerId], + ); + + useEffect(() => { + Cartesian3.clone( + new Cartesian3(dimensions?.width, dimensions?.length, dimensions?.height), + cesiumDimensions, + ); + }, [dimensions, cesiumDimensions]); + + useEffect(() => { + ctx?.reearth.on("mousedown", handlePointMouseDown); + ctx?.reearth.on("mousemove", handlePointMouseMove); + ctx?.reearth.on("mouseup", handlePointMouseUp); + + return () => { + ctx?.reearth.off("mousedown", handlePointMouseDown); + ctx?.reearth.off("mousemove", handlePointMouseMove); + ctx?.reearth.off("mouseup", handlePointMouseUp); + }; + }, [ctx, handlePointMouseDown, handlePointMouseMove, handlePointMouseUp]); + + return { + entitiesPosition, + pointOutlineColorCb, + cesiumDimensionsCallbackProperty, + orientation, + axisColorProperty, + }; +}; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/side.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/side.ts new file mode 100644 index 000000000..a6cb3819f --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/hooks/side.ts @@ -0,0 +1,72 @@ +import { + Axis, + CallbackProperty, + Cartesian3, + Color, + Matrix4, + Plane as CesiumPlane, + PositionProperty, + TranslationRotationScale, +} from "cesium"; +import { useMemo, useRef, useState, useEffect } from "react"; + +import { setPlaneDimensions } from "../utils"; + +export const useHooks = ({ + planeLocal, + isActive, + outlineColor, + activeOutlineColor, + trs, +}: { + planeLocal: CesiumPlane; + isActive: boolean; + trs: TranslationRotationScale; + outlineColor?: Color; + activeOutlineColor?: Color; +}) => { + const cbRef = useMemo( + () => new CallbackProperty(() => trs.translation, false) as unknown as PositionProperty, + [trs], + ); + const [plane, dimension, orientation] = useMemo(() => { + return [ + new CallbackProperty(() => { + const scratchScaleMatrix = new Matrix4(); + const scaleMatrix = Matrix4.fromScale(trs.scale, scratchScaleMatrix); + return CesiumPlane.transform( + planeLocal, + scaleMatrix, + new CesiumPlane(Cartesian3.UNIT_Z, 0), + ); + }, false), + new CallbackProperty(() => { + const normalAxis = planeLocal.normal.x ? Axis.X : planeLocal.normal.y ? Axis.Y : Axis.Z; + const dimension = new Cartesian3(); + setPlaneDimensions(trs.scale, normalAxis, dimension); + return dimension; + }, false), + new CallbackProperty(() => trs.rotation, false), + ]; + }, [trs, planeLocal]); + + const isActiveRef = useRef(false); + const [outlineColorCb] = useState( + () => + new CallbackProperty( + () => (isActiveRef.current ? activeOutlineColor ?? outlineColor : outlineColor), + false, + ), + ); + useEffect(() => { + isActiveRef.current = isActive; + }, [isActive]); + + return { + cbRef, + plane, + dimension, + orientation, + outlineColorCb, + }; +}; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/index.tsx b/src/components/molecules/Visualizer/Engine/Cesium/Box/index.tsx index 391e550a1..b74e3f28a 100644 --- a/src/components/molecules/Visualizer/Engine/Cesium/Box/index.tsx +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/index.tsx @@ -1,10 +1,14 @@ -import { Cartesian3, Plane as CesiumPlane, TranslationRotationScale } from "cesium"; -import React, { useMemo, useEffect, memo, useState } from "react"; +import React, { memo } from "react"; -import { LatLngHeight, toColor } from "@reearth/util/value"; +import { LatLngHeight } from "@reearth/util/value"; +import { SceneProperty } from "../.."; import type { Props as PrimitiveProps } from "../../../Primitive"; +import { BOX_EDGES, SCALE_POINTS, SIDE_PLANES, SIDE_PLANE_NAMES } from "./constants"; +import { Edge } from "./Edge"; +import { useHooks } from "./hooks/box"; +import { ScalePoints } from "./ScalePoints"; import { Side } from "./Side"; export type Props = PrimitiveProps; @@ -20,63 +24,57 @@ export type Property = { roll?: number; fillColor?: string; outlineColor?: string; + activeOutlineColor?: string; outlineWidth?: number; - fill?: boolean; - outline?: boolean; + draggableOutlineColor?: string; + activeDraggableOutlineColor?: string; + draggableOutlineWidth?: number; + scalePoint?: boolean; + axisLine?: boolean; + pointFillColor?: string; + pointOutlineColor?: string; + activePointOutlineColor?: string; + pointOutlineWidth?: number; + axisLineColor?: string; + axisLineWidth?: number; + allowEnterGround?: boolean; + cursor?: string; + activeBox?: boolean; + activeScalePointIndex?: number; // 0 ~ 11 + activeEdgeIndex?: number; // 0 ~ 11 }; }; -// The 6 box sides defined as planes in local coordinate space. -// ref: https://github.com/TerriaJS/terriajs/blob/cad62a45cbee98c7561625458bec3a48510f6cbc/lib/Models/BoxDrawing.ts#L161-L169 -const SIDE_PLANES: readonly CesiumPlane[] = [ - new CesiumPlane(new Cartesian3(0, 0, 1), 0.5), - new CesiumPlane(new Cartesian3(0, 0, -1), 0.5), - new CesiumPlane(new Cartesian3(0, 1, 0), 0.5), - new CesiumPlane(new Cartesian3(0, -1, 0), 0.5), - new CesiumPlane(new Cartesian3(1, 0, 0), 0.5), - new CesiumPlane(new Cartesian3(-1, 0, 0), 0.5), -]; - -// This is used for plane ID. -// We can use this name, for example you want to move the box when front plane is dragged. -const SIDE_PLANE_NAMES = ["bottom", "top", "front", "back", "left", "right"]; - -const updateTrs = (trs: TranslationRotationScale, property: Property | undefined) => { - const { location, height, width, length } = property?.default ?? {}; - - const translation = location - ? Cartesian3.fromDegrees(location.lng, location.lat, location.height ?? 0) - : undefined; - if (translation) { - Cartesian3.clone(translation, trs.translation); - } - - // Quaternion.clone(trs.rotation, this.trs.rotation); - - Cartesian3.clone(new Cartesian3(width || 100, length || 100, height || 100), trs.scale); - - return trs; -}; - -const Box: React.FC> = memo(function BoxPresenter({ layer }) { +const Box: React.FC> = memo(function BoxPresenter({ + layer, + sceneProperty, +}) { const { id, isVisible, property } = layer ?? {}; - const { fillColor, outlineColor, outlineWidth, fill, outline } = property?.default ?? {}; - - const [trs] = useState(() => updateTrs(new TranslationRotationScale(), property)); - useEffect(() => { - updateTrs(trs, property); - }, [property, trs]); - - const style = useMemo( - () => ({ - fillColor: toColor(fillColor), - outlineColor: toColor(outlineColor), - outlineWidth, - fill, - outline, - }), - [fillColor, outlineColor, outlineWidth, fill, outline], - ); + const { + height = 100, + width = 100, + length = 100, + pointOutlineWidth, + axisLineWidth, + scalePoint = true, + axisLine = true, + activeBox, + activeEdgeIndex, + activeScalePointIndex, + } = property?.default ?? {}; + const { + style, + trs, + scalePointStyle, + handlePointMouseDown, + handlePointMouseMove, + handlePointMouseUp, + handleEdgeMouseDown, + handleEdgeMouseMove, + handleEdgeMouseUp, + } = useHooks({ layer, sceneProperty }); + + const scalePointDimension = ((width + height + length) / 3) * 0.05; return !isVisible ? null : ( <> @@ -85,10 +83,61 @@ const Box: React.FC> = memo(function BoxPresenter({ lay key={`${id}-plane-${SIDE_PLANE_NAMES[i]}`} id={`${id}-plane-${SIDE_PLANE_NAMES[i]}`} planeLocal={plane} - style={style} + fill={style.fill} + fillColor={style.fillColor} + outlineColor={style.outlineColor} + isActive={!!activeBox} + activeOutlineColor={style.activeOutlineColor} trs={trs} /> ))} + {BOX_EDGES.map((edge, i) => { + return ( + + ); + })} + {scalePoint && + SCALE_POINTS.map((vector, i) => ( + + ))} ); }); diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Box/utils.ts b/src/components/molecules/Visualizer/Engine/Cesium/Box/utils.ts new file mode 100644 index 000000000..77312e1a0 --- /dev/null +++ b/src/components/molecules/Visualizer/Engine/Cesium/Box/utils.ts @@ -0,0 +1,131 @@ +import { + Axis, + Cartesian2, + Cartesian3, + HeadingPitchRoll, + Matrix3, + Matrix4, + Quaternion, + Ray, + Scene, + Transforms, + TranslationRotationScale, +} from "cesium"; + +import { translationWithClamping } from "../utils"; + +import { Property } from "."; + +export const updateTrs = ( + trs: TranslationRotationScale, + property: Property | undefined, + terrainHeightEstimation: number, + allowEnterGround?: boolean, +) => { + const { location, height, width, length, heading, pitch, roll } = property?.default ?? {}; + + const translation = location + ? Cartesian3.fromDegrees(location.lng, location.lat, location.height ?? 0) + : undefined; + if (translation) { + Cartesian3.clone(translation, trs.translation); + } + if (!allowEnterGround) { + translationWithClamping(trs, !!allowEnterGround, terrainHeightEstimation); + } + + const rotation = + heading && pitch && roll + ? Matrix3.fromHeadingPitchRoll(new HeadingPitchRoll(heading, pitch, roll)) + : Matrix3.getRotation( + Matrix4.getMatrix3(Transforms.eastNorthUpToFixedFrame(trs.translation), new Matrix3()), + new Matrix3(), + ); + Quaternion.clone(Quaternion.fromRotationMatrix(rotation), trs.rotation); + + Cartesian3.clone(new Cartesian3(width || 100, length || 100, height || 100), trs.scale); + + return trs; +}; + +function screenProjectVector( + scene: Scene, + position: Cartesian3, + direction: Cartesian3, + length: number, + result: Cartesian2, +): Cartesian2 { + const ray = new Ray(); + ray.origin = position; + ray.direction = direction; + const nearPoint2d = scene.cartesianToCanvasCoordinates(Ray.getPoint(ray, 0), new Cartesian2()); + + const farPoint2d = scene.cartesianToCanvasCoordinates( + Ray.getPoint(ray, length), + new Cartesian2(), + ); + const screenVector2d = Cartesian2.subtract(farPoint2d, nearPoint2d, result); + return screenVector2d; +} + +const dotMousePosition = ( + scene: Scene, + mouseMove: { + startPosition: Cartesian2; + endPosition: Cartesian2; + }, + position: Cartesian3, + direction: Cartesian3, +) => { + const mouseVector2d = Cartesian2.subtract( + mouseMove.endPosition, + mouseMove.startPosition, + new Cartesian2(), + ); + + // Project the vector of unit length to the screen + const screenVector2d = screenProjectVector(scene, position, direction, 1, new Cartesian2()); + const screenNormal2d = Cartesian2.normalize(screenVector2d, new Cartesian2()); + + const pixelsPerStep = Cartesian2.magnitude(screenVector2d); + const moveAmountPixels = Cartesian2.dot(mouseVector2d, screenNormal2d); + + return { + pixelsPerStep, + moveAmount: moveAmountPixels / pixelsPerStep, + }; +}; + +export const computeMouseMoveAmount = ( + scene: Scene, + mouseMove: { + startPosition: Cartesian2; + endPosition: Cartesian2; + }, + position: Cartesian3, + direction: Cartesian3, + length: number, +) => { + const { moveAmount, pixelsPerStep } = dotMousePosition(scene, mouseMove, position, direction); + const scaleAmount = moveAmount / length; + const pixelLengthAfterScaling = pixelsPerStep * length + pixelsPerStep * length * scaleAmount; + return { pixelLengthAfterScaling, scaleAmount }; +}; + +// ref: https://github.com/TerriaJS/terriajs/blob/cad62a45cbee98c7561625458bec3a48510f6cbc/lib/Models/BoxDrawing.ts#L1446-L1461 +export function setPlaneDimensions( + boxDimensions: Cartesian3, + planeNormalAxis: Axis, + planeDimensions: Cartesian2, +) { + if (planeNormalAxis === Axis.X) { + planeDimensions.x = boxDimensions.y; + planeDimensions.y = boxDimensions.z; + } else if (planeNormalAxis === Axis.Y) { + planeDimensions.x = boxDimensions.x; + planeDimensions.y = boxDimensions.z; + } else if (planeNormalAxis === Axis.Z) { + planeDimensions.x = boxDimensions.x; + planeDimensions.y = boxDimensions.y; + } +} diff --git a/src/components/molecules/Visualizer/Engine/Cesium/Tileset/index.tsx b/src/components/molecules/Visualizer/Engine/Cesium/Tileset/index.tsx index 959760f7d..7f4714771 100644 --- a/src/components/molecules/Visualizer/Engine/Cesium/Tileset/index.tsx +++ b/src/components/molecules/Visualizer/Engine/Cesium/Tileset/index.tsx @@ -8,6 +8,7 @@ import { Matrix3, Matrix4, Transforms, + TranslationRotationScale, } from "cesium"; import ClippingPlane from "cesium/Source/Scene/ClippingPlane"; import { FC, useCallback, useEffect, useMemo, useRef, useState, memo } from "react"; @@ -15,8 +16,10 @@ import { Cesium3DTileset, CesiumComponentRef, useCesium } from "resium"; import { EXPERIMENTAL_clipping, toColor } from "@reearth/util/value"; +import { SceneProperty } from "../.."; import type { Props as PrimitiveProps } from "../../../Primitive"; -import { shadowMode, layerIdField } from "../common"; +import { shadowMode, layerIdField, sampleTerrainHeightFromCartesian } from "../common"; +import { translationWithClamping } from "../utils"; export type Props = PrimitiveProps; @@ -32,7 +35,10 @@ export type Property = { }; }; -const Tileset: FC> = memo(function TilesetPresenter({ layer }) { +const Tileset: FC> = memo(function TilesetPresenter({ + layer, + sceneProperty, +}) { const { viewer } = useCesium(); const { isVisible, property } = layer ?? {}; const { sourceType, tileset, styleUrl, shadows, edgeColor, edgeWidth, experimental_clipping } = @@ -47,6 +53,7 @@ const Tileset: FC> = memo(function TilesetPresenter({ l pitch, planes: _planes, } = experimental_clipping || {}; + const { allowEnterGround } = sceneProperty?.default || {}; const [style, setStyle] = useState(); const prevPlanes = useRef(_planes); const planes = useMemo(() => { @@ -91,6 +98,25 @@ const Tileset: FC> = memo(function TilesetPresenter({ l [layer?.id], ); + const [terrainHeightEstimate, setTerrainHeightEstimate] = useState(0); + const inProgressSamplingTerrainHeight = useRef(false); + const updateTerrainHeight = useCallback( + (translation: Cartesian3) => { + if (inProgressSamplingTerrainHeight.current) { + return; + } + + if (!allowEnterGround) { + inProgressSamplingTerrainHeight.current = true; + sampleTerrainHeightFromCartesian(viewer.scene, translation).then(v => { + setTerrainHeightEstimate(v ?? 0); + inProgressSamplingTerrainHeight.current = false; + }); + } + }, + [allowEnterGround, viewer], + ); + useEffect(() => { const prepareClippingPlanes = async () => { if (!tilesetRef.current) { @@ -111,8 +137,15 @@ const Tileset: FC> = memo(function TilesetPresenter({ l location?.height, ); - const hpr = - heading && pitch && roll ? HeadingPitchRoll.fromDegrees(heading, pitch, roll) : undefined; + if (!allowEnterGround) { + translationWithClamping( + new TranslationRotationScale(position, undefined, dimensions), + !!allowEnterGround, + terrainHeightEstimate, + ); + } + + const hpr = heading && pitch && roll ? new HeadingPitchRoll(heading, pitch, roll) : undefined; const boxTransform = Matrix4.multiply( hpr ? Matrix4.fromRotationTranslation(Matrix3.fromHeadingPitchRoll(hpr), position) @@ -126,8 +159,25 @@ const Tileset: FC> = memo(function TilesetPresenter({ l Matrix4.multiply(inverseOriginalModelMatrix, boxTransform, clippingPlanes.modelMatrix); }; + if (!allowEnterGround) { + updateTerrainHeight(Matrix4.getTranslation(clippingPlanes.modelMatrix, new Cartesian3())); + } prepareClippingPlanes(); - }, [width, length, height, location?.lng, location?.lat, location?.height, heading, pitch, roll, clippingPlanes.modelMatrix]); + }, [ + width, + length, + height, + location?.lng, + location?.lat, + location?.height, + heading, + pitch, + roll, + clippingPlanes.modelMatrix, + updateTerrainHeight, + allowEnterGround, + terrainHeightEstimate, + ]); useEffect(() => { if (!styleUrl) { @@ -161,6 +211,6 @@ const Tileset: FC> = memo(function TilesetPresenter({ l ); }); -const _debugFlight = true; +const _debugFlight = false; export default Tileset; diff --git a/src/components/molecules/Visualizer/Engine/Cesium/common.ts b/src/components/molecules/Visualizer/Engine/Cesium/common.ts index 98034517e..71fde68b1 100644 --- a/src/components/molecules/Visualizer/Engine/Cesium/common.ts +++ b/src/components/molecules/Visualizer/Engine/Cesium/common.ts @@ -681,7 +681,7 @@ function rotateVectorAboutAxis(vector: Cartesian3, rotateAxis: Cartesian3, rotat return rotatedVector; } -async function sampleTerrainHeight( +export async function sampleTerrainHeight( scene: Scene, lng: number, lat: number, @@ -694,3 +694,15 @@ async function sampleTerrainHeight( ]); return sample.height; } + +export async function sampleTerrainHeightFromCartesian(scene: Scene, translation: Cartesian3) { + const cart = Cartographic.fromCartesian(translation); + const [lng, lat] = [ + CesiumMath.toDegrees(cart?.longitude || 0), + CesiumMath.toDegrees(cart?.latitude || 0), + ]; + if (!lng || !lat) { + return; + } + return await sampleTerrainHeight(scene, lng, lat); +} diff --git a/src/components/molecules/Visualizer/Engine/Cesium/utils.ts b/src/components/molecules/Visualizer/Engine/Cesium/utils.ts index 5cc224228..9b41d4843 100644 --- a/src/components/molecules/Visualizer/Engine/Cesium/utils.ts +++ b/src/components/molecules/Visualizer/Engine/Cesium/utils.ts @@ -1,4 +1,10 @@ -import { Cartesian3, Viewer as CesiumViewer, Math as CesiumMath } from "cesium"; +import { + Cartesian3, + Viewer as CesiumViewer, + Math as CesiumMath, + TranslationRotationScale, + Cartographic, +} from "cesium"; export const convertCartesian3ToPosition = ( cesium?: CesiumViewer, @@ -13,3 +19,19 @@ export const convertCartesian3ToPosition = ( height: cartographic.height, }; }; + +export const translationWithClamping = ( + trs: TranslationRotationScale, + allowEnterGround: boolean, + terrainHeightEstimate: number, +) => { + if (!allowEnterGround) { + const cartographic = Cartographic.fromCartesian(trs.translation, undefined, new Cartographic()); + const boxBottomHeight = cartographic.height - trs.scale.z / 2; + const floorHeight = terrainHeightEstimate; + if (boxBottomHeight < floorHeight) { + cartographic.height += floorHeight - boxBottomHeight; + Cartographic.toCartesian(cartographic, undefined, trs.translation); + } + } +}; diff --git a/src/components/molecules/Visualizer/Plugin/context.tsx b/src/components/molecules/Visualizer/Plugin/context.tsx index becdde049..9012ec245 100644 --- a/src/components/molecules/Visualizer/Plugin/context.tsx +++ b/src/components/molecules/Visualizer/Plugin/context.tsx @@ -7,7 +7,7 @@ import { useMemo, } from "react"; -import events from "@reearth/util/event"; +import events, { EventEmitter } from "@reearth/util/event"; import { Rect } from "@reearth/util/value"; import { MouseEvents, MouseEventHandles } from "../Engine/ref"; @@ -85,10 +85,16 @@ export type Props = { moveWidget: (widgetId: string, options: WidgetLocationOptions) => void; }; +type SelectedReearthEventType = Pick< + ReearthEventType, + "cameramove" | "select" | "tick" | "resize" | keyof MouseEvents | "layeredit" +>; + export type Context = { reearth: CommonReearth; engine: EngineContext; overrideSceneProperty: (id: string, property: any) => void; + emit: EventEmitter; moveWidget: (widgetId: string, options: WidgetLocationOptions) => void; }; @@ -147,10 +153,7 @@ export function Provider({ children, }: Props): JSX.Element { const [ev, emit] = useMemo( - () => - events< - Pick - >(), + () => events(), // eslint-disable-next-line react-hooks/exhaustive-deps [engineName], ); @@ -222,6 +225,7 @@ export function Provider({ flyToGround, }), overrideSceneProperty, + emit, moveWidget, }), [ @@ -268,11 +272,12 @@ export function Provider({ moveOverTerrain, flyToGround, overrideSceneProperty, + emit, moveWidget, ], ); - useEmit>( + useEmit( { select: useMemo<[layerId: string | undefined]>( () => (selectedLayer ? [selectedLayer.id] : [undefined]), diff --git a/src/components/molecules/Visualizer/Plugin/hooks.ts b/src/components/molecules/Visualizer/Plugin/hooks.ts index b4bf3ac73..65ffd3db7 100644 --- a/src/components/molecules/Visualizer/Plugin/hooks.ts +++ b/src/components/molecules/Visualizer/Plugin/hooks.ts @@ -236,6 +236,7 @@ export function useAPI({ "wheel", "tick", "resize", + "layeredit", ]); } diff --git a/src/components/molecules/Visualizer/Plugin/types.ts b/src/components/molecules/Visualizer/Plugin/types.ts index cb7331e9b..bad42df46 100644 --- a/src/components/molecules/Visualizer/Plugin/types.ts +++ b/src/components/molecules/Visualizer/Plugin/types.ts @@ -50,10 +50,17 @@ export type MouseEvent = { delta?: number; }; +export type LayerEditEvent = { + layerId: string | undefined; + scale?: { width: number; length: number; height: number; location: LatLngHeight }; + rotate?: { heading: number; pitch: number; roll: number }; +}; + export type ReearthEventType = { update: []; close: []; cameramove: [camera: CameraPosition]; + layeredit: [e: LayerEditEvent]; select: [layerId: string | undefined]; message: [message: any]; click: [props: MouseEvent];