diff --git a/packages/dev/core/src/Gizmos/cameraGizmo.ts b/packages/dev/core/src/Gizmos/cameraGizmo.ts index 4d185d17e81..1f956a307e6 100644 --- a/packages/dev/core/src/Gizmos/cameraGizmo.ts +++ b/packages/dev/core/src/Gizmos/cameraGizmo.ts @@ -67,10 +67,10 @@ export class CameraGizmo extends Gizmo implements ICameraGizmo { } this._isHovered = !!(pointerInfo.pickInfo && this._rootMesh.getChildMeshes().indexOf(pointerInfo.pickInfo.pickedMesh) != -1); - if (this._isHovered && pointerInfo.event.button === 0) { + if (this._isHovered && pointerInfo.type === PointerEventTypes.POINTERDOWN && pointerInfo.event.button === 0) { this.onClickedObservable.notifyObservers(this._camera); } - }, PointerEventTypes.POINTERDOWN); + }); } protected _camera: Nullable = null; diff --git a/packages/dev/core/src/Gizmos/lightGizmo.ts b/packages/dev/core/src/Gizmos/lightGizmo.ts index bff25498e9c..ba36d4ad943 100644 --- a/packages/dev/core/src/Gizmos/lightGizmo.ts +++ b/packages/dev/core/src/Gizmos/lightGizmo.ts @@ -70,10 +70,10 @@ export class LightGizmo extends Gizmo implements ILightGizmo { } this._isHovered = !!(pointerInfo.pickInfo && this._rootMesh.getChildMeshes().indexOf(pointerInfo.pickInfo.pickedMesh) != -1); - if (this._isHovered && pointerInfo.event.button === 0) { + if (this._isHovered && pointerInfo.type === PointerEventTypes.POINTERDOWN && pointerInfo.event.button === 0) { this.onClickedObservable.notifyObservers(this._light); } - }, PointerEventTypes.POINTERDOWN); + }); } protected _light: Nullable = null; diff --git a/packages/dev/inspector-v2/src/components/pickingToolbar.tsx b/packages/dev/inspector-v2/src/components/pickingToolbar.tsx new file mode 100644 index 00000000000..8c104ea86d9 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/pickingToolbar.tsx @@ -0,0 +1,128 @@ +import type { FunctionComponent } from "react"; + +import type { AbstractMesh, IMeshDataCache, Scene } from "core/index"; +import type { IGizmoService } from "../services/gizmoService"; + +import { TargetRegular } from "@fluentui/react-icons"; +import { useEffect, useMemo, useState } from "react"; + +import { PointerEventTypes } from "core/Events/pointerEvents"; +import { TmpVectors, Vector3 } from "core/Maths/math.vector"; +import { ToggleButton } from "shared-ui-components/fluent/primitives/toggleButton"; + +export const PickingToolbar: FunctionComponent<{ + scene: Scene; + selectEntity: (entity: unknown) => void; + gizmoService: IGizmoService; + ignoreBackfaces?: boolean; +}> = (props) => { + const { scene, selectEntity, gizmoService, ignoreBackfaces } = props; + + const meshDataCache = useMemo(() => new WeakMap(), [scene]); + // Not sure why changing the cursor on the canvas itself doesn't work, so change it on the parent. + const sceneElement = scene.getEngine().getRenderingCanvas()?.parentElement; + + const [pickingEnabled, setPickingEnabled] = useState(false); + + useEffect(() => { + if (pickingEnabled && sceneElement) { + const originalCursor = getComputedStyle(sceneElement).cursor; + sceneElement.style.cursor = "crosshair"; + + const pointerObserver = scene.onPrePointerObservable.add(() => { + let pickedEntity: unknown = null; + + // Check camera gizmos. + if (!pickedEntity) { + for (const cameraGizmo of gizmoService.getCameraGizmos(scene)) { + if (cameraGizmo.isHovered) { + pickedEntity = cameraGizmo.camera; + } + } + } + + // Check light gizmos. + if (!pickedEntity) { + for (const lightGizmo of gizmoService.getLightGizmos(scene)) { + if (lightGizmo.isHovered) { + pickedEntity = lightGizmo.light; + } + } + } + + // Check the main scene. + if (!pickedEntity) { + // Refresh bounding info to ensure morph target and skeletal animations are taken into account. + for (const mesh of scene.meshes) { + let cache = meshDataCache.get(mesh); + if (!cache) { + cache = {}; + meshDataCache.set(mesh, cache); + } + mesh.refreshBoundingInfo({ applyMorph: true, applySkeleton: true, cache }); + } + + const pickingInfo = scene.pick( + scene.unTranslatedPointer.x, + scene.unTranslatedPointer.y, + (mesh) => mesh.isEnabled() && mesh.isVisible && mesh.getTotalVertices() > 0, + false, + undefined, + (p0, p1, p2, ray) => { + if (!ignoreBackfaces) { + return true; + } + + const p0p1 = TmpVectors.Vector3[0]; + const p1p2 = TmpVectors.Vector3[1]; + let normal = TmpVectors.Vector3[2]; + + p1.subtractToRef(p0, p0p1); + p2.subtractToRef(p1, p1p2); + + normal = Vector3.Cross(p0p1, p1p2); + + return Vector3.Dot(normal, ray.direction) < 0; + } + ); + + pickedEntity = pickingInfo.pickedMesh; + } + + // If an entity was picked, select it. + if (pickedEntity) { + selectEntity(pickedEntity); + } + }, PointerEventTypes.POINTERTAP); + + // Exit picking mode if the escape key is pressed. + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setPickingEnabled(false); + } + }; + document.addEventListener("keydown", handleKeyDown); + + return () => { + sceneElement.style.cursor = originalCursor; + pointerObserver.remove(); + document.removeEventListener("keydown", handleKeyDown); + }; + } + + return () => { + /* No-op */ + }; + }, [pickingEnabled, sceneElement, ignoreBackfaces]); + + return ( + sceneElement && ( + setPickingEnabled((prev) => !prev)} + /> + ) + ); +}; diff --git a/packages/dev/inspector-v2/src/inspector.tsx b/packages/dev/inspector-v2/src/inspector.tsx index 07478eb1e47..4dbe8fd99d9 100644 --- a/packages/dev/inspector-v2/src/inspector.tsx +++ b/packages/dev/inspector-v2/src/inspector.tsx @@ -51,6 +51,7 @@ import { TextureExplorerServiceDefinition } from "./services/panes/scene/texture import { SettingsServiceDefinition } from "./services/panes/settingsService"; import { StatsServiceDefinition } from "./services/panes/statsService"; import { ToolsServiceDefinition } from "./services/panes/toolsService"; +import { PickingServiceDefinition } from "./services/pickingService"; import { SceneContextIdentity } from "./services/sceneContext"; import { SelectionServiceDefinition } from "./services/selectionService"; import { ShellServiceIdentity } from "./services/shellService"; @@ -250,6 +251,9 @@ function _ShowInspector(scene: Nullable, options: Partial { getUtilityLayer(scene: Scene, layer?: string): Reference; getCameraGizmo(camera: Camera): Reference; getLightGizmo(light: Light): Reference; + getCameraGizmos(scene: Scene): readonly CameraGizmo[]; + getLightGizmos(scene: Scene): readonly LightGizmo[]; } export const GizmoServiceDefinition: ServiceDefinition<[IGizmoService], []> = { @@ -110,6 +112,8 @@ export const GizmoServiceDefinition: ServiceDefinition<[IGizmoService], []> = { getUtilityLayer, getCameraGizmo, getLightGizmo, + getCameraGizmos: (scene) => scene.cameras.map((camera) => cameraGizmos.get(camera)?.gizmo).filter(Boolean) as readonly CameraGizmo[], + getLightGizmos: (scene) => scene.lights.map((light) => lightGizmos.get(light)?.gizmo).filter(Boolean) as readonly LightGizmo[], }; }, }; diff --git a/packages/dev/inspector-v2/src/services/pickingService.tsx b/packages/dev/inspector-v2/src/services/pickingService.tsx new file mode 100644 index 00000000000..a8c7b360412 --- /dev/null +++ b/packages/dev/inspector-v2/src/services/pickingService.tsx @@ -0,0 +1,34 @@ +import type { ServiceDefinition } from "../modularity/serviceDefinition"; +import type { IGizmoService } from "./gizmoService"; +import type { ISceneContext } from "./sceneContext"; +import type { ISelectionService } from "./selectionService"; +import type { ISettingsContext } from "./settingsContext"; +import type { IShellService } from "./shellService"; + +import { useCallback } from "react"; +import { PickingToolbar } from "../components/pickingToolbar"; +import { useObservableState } from "../hooks/observableHooks"; +import { GizmoServiceIdentity } from "./gizmoService"; +import { SceneContextIdentity } from "./sceneContext"; +import { SelectionServiceIdentity } from "./selectionService"; +import { SettingsContextIdentity } from "./settingsContext"; +import { ShellServiceIdentity } from "./shellService"; + +export const PickingServiceDefinition: ServiceDefinition<[], [ISceneContext, IShellService, ISelectionService, IGizmoService, ISettingsContext]> = { + friendlyName: "Picking Service", + consumes: [SceneContextIdentity, ShellServiceIdentity, SelectionServiceIdentity, GizmoServiceIdentity, SettingsContextIdentity], + factory: (sceneContext, shellService, selectionService, gizmoService, settingsContext) => { + shellService.addToolbarItem({ + key: "Picking Service", + verticalLocation: "top", + horizontalLocation: "left", + suppressTeachingMoment: true, + component: () => { + const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable); + const selectEntity = useCallback((entity: unknown) => (selectionService.selectedEntity = entity), []); + const ignoreBackfacesForPicking = useObservableState(() => settingsContext.ignoreBackfacesForPicking, settingsContext.settingsChangedObservable); + return scene ? : null; + }, + }); + }, +};