import { computed, ref, watch, watchEffect } from "vue"; import { installThreeGlobalVar, useCamera, useContainer, useRender, useStageProps, } from "./use-stage"; import { OrbitControls } from "three/examples/jsm/Addons.js"; import { listener } from "@/utils/event"; import { Intersection, Matrix3, Vector3 } from "three"; import { mergeFuns } from "@/utils/shared"; import { useMouseEventRegister } from "./use-event"; import { useCameraAnimation } from "./use-animation"; import { getMoveDirectrionByKeys, useFigureMoveCollision } from "./use-move"; import { useGetIntersectObject } from "./use-getter"; import { subgroupName } from "../container"; const useModelControls = () => { const container = useContainer(); const camera = useCamera(); const _render = useRender(); const render = () => { camera.bus.emit("change"); _render(); }; const controls = new OrbitControls(camera); controls.target.set(0, 5, 0); controls.enabled = false; const unListener = listener(controls as any, "change", render); let prevOrigin: Vector3 | null = null; let prevDire: Vector3 | null = null; watch( container, (container, _, onCleanup) => { if (container) { controls.domElement = container; controls.connect(); onCleanup(() => { controls.disconnect(); }); } }, { immediate: true } ); return { controls, onDestory() { controls.domElement && controls.dispose(); unListener(); }, syncCamera() { controls.update(); }, disable() { prevOrigin = camera.position.clone(); prevDire = camera.getWorldDirection(new Vector3()); controls.enabled = false; }, enable() { controls.enabled = true; }, get current() { return { prevOrigin, prevDire, }; }, }; }; const roamingEysHeight = 120; const useRoamingControls = () => { const container = useContainer(); const camera = useCamera(); const _render = useRender(); const render = () => { camera.bus.emit("change"); _render(); }; const enabled = ref(false); const syncCamera = (direction = camera.getWorldDirection(new Vector3())) => { controls.target.copy(direction.add(camera.position)); controls.update(); }; const { direction, onDestory: onDownDestory } = getMoveDirectrionByKeys(); const move = useFigureMoveCollision(camera, direction, syncCamera); move.pause(); const controls = new OrbitControls(camera); controls.rotateSpeed = -0.3; controls.enableZoom = false; let prevOrigin: Vector3 | null = null; let prevDire: Vector3 | null = null; return { controls, onDestory: mergeFuns( watchEffect(() => (controls.enabled = enabled.value)), watch( container, (container, _, onCleanup) => { if (container) { controls.domElement = container; controls.connect(); onCleanup(() => { controls.disconnect(); }); } }, { immediate: true } ), move.destory, () => { controls.domElement && controls.dispose(); }, listener(controls as any, "change", render), onDownDestory ), syncCamera, disable() { prevOrigin = camera.position.clone(); prevDire = camera.getWorldDirection(new Vector3()); controls.enabled = false; move.pause(); }, enable() { controls.enabled = true; move.continue(); render(); }, get current() { return { prevOrigin, prevDire, }; }, }; }; const controlsFactory = { model: useModelControls, roaming: useRoamingControls, }; export type ControlsType = keyof typeof controlsFactory; export type Controls = ReturnType<(typeof controlsFactory)[ControlsType]>; export const useControls = installThreeGlobalVar(() => { const container = useContainer(); const type = ref(); const controls = ref(); const controlsMap = {} as Record; for (const [type, factory] of Object.entries(controlsFactory)) { controlsMap[type as ControlsType] = factory(); } const stopWatch = watch( [container, type], ([container, type], _, onCleanup) => { if (!(type && container)) return; const ct = controlsMap[type]; ct.enable(); controls.value = ct; onCleanup(() => { ct.disable(); controls.value = undefined; }); }, { immediate: true, flush: "sync" } ); return { var: { type, value: controls }, onDestroy: () => { stopWatch(); for (const controls of Object.values(controlsMap)) { controls.onDestory(); } }, }; }); export const useFlyRoaming = installThreeGlobalVar(() => { const camera = useCamera(); const { type, value: controls } = useControls(); const cameraAnimation = useCameraAnimation(); const getIntersectObject = useGetIntersectObject(); const normalMatrix = new Matrix3(); const bottom = new Vector3(0, -1, 0); return async (point: Intersection | Vector3) => { let intersect: Intersection; if ("point" in point) { intersect = point; } else { point = point.clone(); const objects = getIntersectObject(point, bottom); if (!objects.length) { throw "当前位置无法漫游"; } intersect = objects[0]; } const normal = intersect.face?.normal.clone(); if (!normal) { throw "当前位置无法漫游"; } normalMatrix.getNormalMatrix(intersect.object.matrixWorld); normal.applyMatrix3(normalMatrix).normalize(); if (normal.y < 0.8) { throw "当前位置无法漫游"; } if (type.value !== 'roaming') { type.value = undefined } else { controls.value?.disable() } const position = intersect.point .clone() .add({ x: 0, y: roamingEysHeight, z: 0 }); const direction = camera.getWorldDirection(new Vector3()); const target = position.clone().add(direction); await cameraAnimation(position, target); type.value = "roaming"; controls.value?.enable() controls.value?.syncCamera(); }; }); export const useFlyModel = installThreeGlobalVar(() => { const camera = useCamera(); const { type, value: controls } = useControls(); const cameraAnimation = useCameraAnimation(); return async (set: { point?: Vector3; direction?: Vector3 } = {}) => { type.value = "model"; const prev = controls.value?.current; if (!set.point) { set.point = prev?.prevOrigin || camera.position.clone() || new Vector3(400, 400, 400); } if (!set.direction) { set.direction = prev?.prevDire || camera.getWorldDirection(new Vector3()) || camera.getWorldDirection(new Vector3()); } const direction = set.direction; const target = set.point.clone().add(direction); await cameraAnimation(set.point, target); controls.value?.syncCamera(); }; }); export const installAutoSwitchControls = installThreeGlobalVar(() => { const { type } = useControls(); const mouseRegister = useMouseEventRegister(); const flyRoaming = useFlyRoaming(); const flyModel = useFlyModel(); const sProps = useStageProps(); const canFlyTypes = ["line", "icon"] as const; const canFlyNames = computed(() => { const names = canFlyTypes.flatMap((type) => sProps.value.draw.store.getTypeItems(type).map((item) => item.id) ) names.push(subgroupName) return names }); const onDestroy = mergeFuns( listener(document.documentElement, "keydown", (ev) => { if (ev.key === "Escape" && type.value !== "model") { flyModel(); } }), watchEffect((onCleanup) => { const cleanups = canFlyNames.value.map((name) => mouseRegister(name, "dblclick", (object) => { flyRoaming(object); }) ); onCleanup(mergeFuns(cleanups)) }) ); type.value = "model"; return { onDestroy }; });