feat(all): 拖入花线迁移
This commit is contained in:
parent
2605656dc4
commit
986da8ce42
@ -122,6 +122,9 @@ export default {
|
||||
world: '世界坐标',
|
||||
"Status monitoring":"状态监控",
|
||||
},
|
||||
path: {
|
||||
"Path drawing: Left-click to add a point, and double-click to end the drawing": "路径绘制:左键单击添加点,双击结束绘制",
|
||||
},
|
||||
viewportInfo: {
|
||||
Objects: '物体',
|
||||
Vertices: '顶点',
|
||||
@ -754,6 +757,7 @@ export default {
|
||||
'Query failed': "查询失败",
|
||||
'Related document': "相关文档",
|
||||
Copy: "复制",
|
||||
Finish: "完成",
|
||||
Focus: '聚焦',
|
||||
Support: '支持',
|
||||
Upload: '上传',
|
||||
|
||||
60
packages/editor/src/store/modules/pathDrawing.ts
Normal file
60
packages/editor/src/store/modules/pathDrawing.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { store } from "@/store";
|
||||
|
||||
interface IPathDrawingState {
|
||||
active: boolean;
|
||||
template: Record<string, any> | null;
|
||||
submit: ((payload: IPathDrawingResult) => void) | null;
|
||||
}
|
||||
|
||||
export interface IPathDrawingResult {
|
||||
worldPoints: Array<{ x: number; y: number; z: number }>;
|
||||
origin: { x: number; y: number; z: number };
|
||||
options: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IPathDrawingRequest {
|
||||
template?: Record<string, any> | null;
|
||||
submit?: (payload: IPathDrawingResult) => void;
|
||||
}
|
||||
|
||||
function isPathDrawingRequest(payload: Record<string, any> | IPathDrawingRequest) {
|
||||
return Object.prototype.hasOwnProperty.call(payload, "template") || Object.prototype.hasOwnProperty.call(payload, "submit");
|
||||
}
|
||||
|
||||
export const usePathDrawingStore = defineStore({
|
||||
id: "pathDrawing",
|
||||
state: (): IPathDrawingState => ({
|
||||
active: false,
|
||||
template: null,
|
||||
submit: null,
|
||||
}),
|
||||
getters: {
|
||||
isActive: state => state.active,
|
||||
getTemplate: state => state.template,
|
||||
getSubmit: state => state.submit,
|
||||
},
|
||||
actions: {
|
||||
start(payload: Record<string, any> | IPathDrawingRequest) {
|
||||
const request = isPathDrawingRequest(payload) ? payload : { template: payload };
|
||||
|
||||
this.template = request.template ? JSON.parse(JSON.stringify(request.template)) : null;
|
||||
this.submit = typeof request.submit === "function" ? request.submit : null;
|
||||
this.active = true;
|
||||
},
|
||||
cancel() {
|
||||
this.active = false;
|
||||
this.template = null;
|
||||
this.submit = null;
|
||||
},
|
||||
finish() {
|
||||
this.active = false;
|
||||
this.template = null;
|
||||
this.submit = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function usePathDrawingStoreWithOut() {
|
||||
return usePathDrawingStore(store);
|
||||
}
|
||||
@ -46,6 +46,7 @@ import type { Ref } from "vue";
|
||||
import { Box3, Vector3, type Object3D } from "three";
|
||||
import { cpt } from "@/language";
|
||||
import { useDragStore } from "@/store/modules/drag";
|
||||
import { usePathDrawingStore } from "@/store/modules/pathDrawing";
|
||||
import { screenToWorld } from "@/utils/common/scenes";
|
||||
import { App, AddObjectCommand, Heatmap, Path, UIPanel, Water, type Preview } from "@astral3d/engine";
|
||||
|
||||
@ -59,6 +60,7 @@ interface ExpansionItem {
|
||||
const searchText = inject("searchText") as Ref<string>;
|
||||
const previewInfo = inject("previewInfo") as any;
|
||||
const previewRef = inject("previewRef") as any;
|
||||
const pathDrawingStore = usePathDrawingStore();
|
||||
|
||||
const activeSubCategory = ref("heatmap");
|
||||
const subCategories = ref([
|
||||
@ -514,6 +516,17 @@ function selectSubCategory(key: string) {
|
||||
activeSubCategory.value = key;
|
||||
}
|
||||
|
||||
function startPathDrawing(item: ExpansionItem) {
|
||||
const options = JSON.parse(JSON.stringify(item.options || {}));
|
||||
if (!options.name) {
|
||||
options.name = getItemName(item);
|
||||
}
|
||||
if (pathDrawingStore.isActive) {
|
||||
pathDrawingStore.cancel();
|
||||
}
|
||||
pathDrawingStore.start(options);
|
||||
}
|
||||
|
||||
function getDefaultAddPosition(): number[] | undefined {
|
||||
const container = window.viewer?.container;
|
||||
if (!container) return undefined;
|
||||
@ -580,8 +593,7 @@ function addToScene(item: ExpansionItem, position?: number[]) {
|
||||
break;
|
||||
}
|
||||
case "path": {
|
||||
const path = new Path(options);
|
||||
App.execute(new AddObjectCommand(path), `Add Path: ${options.name}`);
|
||||
startPathDrawing(item);
|
||||
break;
|
||||
}
|
||||
case "uipanel": {
|
||||
@ -603,6 +615,12 @@ function dragStart(item: ExpansionItem) {
|
||||
function dragEnd() {
|
||||
if (dragStore.getActionTarget !== "addToScene" || dragStore.endArea !== "Scene") return;
|
||||
|
||||
if (activeSubCategory.value === "path") {
|
||||
startPathDrawing(dragStore.getData);
|
||||
dragStore.setActionTarget("");
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToWorld(dragStore.endPosition.x, dragStore.endPosition.y).toArray();
|
||||
addToScene(dragStore.getData, position);
|
||||
|
||||
|
||||
@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, watch } from "vue";
|
||||
import * as THREE from "three";
|
||||
import { AddObjectCommand, App, Hooks, Path, Utils, getDefaultPathOptions } from "@astral3d/engine";
|
||||
import { t } from "@/language";
|
||||
import type { IPathDrawingResult } from "@/store/modules/pathDrawing";
|
||||
import { usePathDrawingStore } from "@/store/modules/pathDrawing";
|
||||
|
||||
const pathDrawingStore = usePathDrawingStore();
|
||||
const isDrawing = computed(() => pathDrawingStore.isActive);
|
||||
const submit = computed(() => pathDrawingStore.getSubmit);
|
||||
let templateOptions: Record<string, any> | null = null;
|
||||
let points: THREE.Vector3[] = [];
|
||||
let origin: THREE.Vector3 | null = null;
|
||||
let previewPath: Path | null = null;
|
||||
let viewerRef: any = null;
|
||||
let prevCursor = "";
|
||||
const previewFlag = "__pathPreview";
|
||||
|
||||
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||
const tempNdc = new THREE.Vector2();
|
||||
const onDownPosition = new THREE.Vector2();
|
||||
const onUpPosition = new THREE.Vector2();
|
||||
|
||||
let isPointerDown = false;
|
||||
let lastClickTime = 0;
|
||||
const signal = Hooks.useSignal();
|
||||
|
||||
function haltSelectionOnIntersect() {
|
||||
if (!isDrawing.value) return;
|
||||
signal.halt("intersectionsDetected");
|
||||
}
|
||||
|
||||
function setViewer(viewer: any) {
|
||||
viewerRef = viewer;
|
||||
}
|
||||
|
||||
function cloneTemplate() {
|
||||
const current = pathDrawingStore.getTemplate;
|
||||
templateOptions = current ? JSON.parse(JSON.stringify(current)) : null;
|
||||
}
|
||||
|
||||
function cleanupPreview() {
|
||||
let removed = false;
|
||||
|
||||
if (previewPath) {
|
||||
previewPath.parent?.remove(previewPath);
|
||||
previewPath.dispose?.();
|
||||
previewPath = null;
|
||||
removed = true;
|
||||
}
|
||||
|
||||
const orphans: THREE.Object3D[] = [];
|
||||
App.scene.traverse(child => {
|
||||
if (child.userData?.[previewFlag]) {
|
||||
orphans.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
if (orphans.length > 0) {
|
||||
orphans.forEach(child => {
|
||||
child.parent?.remove(child);
|
||||
(child as any).dispose?.();
|
||||
});
|
||||
removed = true;
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
Hooks.useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
points = [];
|
||||
origin = null;
|
||||
isPointerDown = false;
|
||||
lastClickTime = 0;
|
||||
cleanupPreview();
|
||||
}
|
||||
|
||||
function buildOptions(worldPoints: THREE.Vector3[], baseOrigin: THREE.Vector3) {
|
||||
const options = getDefaultPathOptions();
|
||||
if (templateOptions) {
|
||||
Utils.deepAssign(options, templateOptions);
|
||||
}
|
||||
options.position = baseOrigin.toArray();
|
||||
options.points = worldPoints.map(point => ({
|
||||
x: point.x - baseOrigin.x,
|
||||
y: point.y - baseOrigin.y,
|
||||
z: point.z - baseOrigin.z,
|
||||
}));
|
||||
if (!options.name) options.name = "Path";
|
||||
return options;
|
||||
}
|
||||
|
||||
function buildResult(worldPoints: THREE.Vector3[], baseOrigin: THREE.Vector3): IPathDrawingResult {
|
||||
return {
|
||||
worldPoints: worldPoints.map(point => ({ x: point.x, y: point.y, z: point.z })),
|
||||
origin: {
|
||||
x: baseOrigin.x,
|
||||
y: baseOrigin.y,
|
||||
z: baseOrigin.z,
|
||||
},
|
||||
options: buildOptions(worldPoints, baseOrigin),
|
||||
};
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
if (!templateOptions) return;
|
||||
if (points.length === 0) return;
|
||||
|
||||
if (!origin) {
|
||||
origin = points[0].clone();
|
||||
}
|
||||
|
||||
const options = buildOptions(points, origin);
|
||||
if (!previewPath) {
|
||||
const path = new Path(options);
|
||||
path.ignore = true;
|
||||
path.userData[previewFlag] = true;
|
||||
previewPath = path;
|
||||
App.scene.add(path);
|
||||
} else {
|
||||
previewPath.updateOptions({
|
||||
mode: options.mode,
|
||||
closed: options.closed,
|
||||
cornerRadius: options.cornerRadius,
|
||||
cornerSplit: options.cornerSplit,
|
||||
path: options.path,
|
||||
tube: options.tube,
|
||||
position: options.position,
|
||||
points: options.points,
|
||||
});
|
||||
}
|
||||
|
||||
Hooks.useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
|
||||
function getPickPoint(event: PointerEvent) {
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (!viewer) return null;
|
||||
|
||||
const mouse = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||
const intersects = viewer.getIntersects(new THREE.Vector2(mouse[0], mouse[1]));
|
||||
if (intersects.length > 0) {
|
||||
return intersects[0].point.clone();
|
||||
}
|
||||
|
||||
tempNdc.set(mouse[0] * 2 - 1, -(mouse[1] * 2) + 1);
|
||||
viewer.raycaster.setFromCamera(tempNdc, viewer.camera);
|
||||
const hit = new THREE.Vector3();
|
||||
return viewer.raycaster.ray.intersectPlane(groundPlane, hit) ? hit.clone() : null;
|
||||
}
|
||||
|
||||
function addPoint(point: THREE.Vector3) {
|
||||
points.push(point);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function finishDrawing() {
|
||||
if (!isDrawing.value) return;
|
||||
if (points.length <= 2) {
|
||||
cleanupPreview();
|
||||
pathDrawingStore.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const baseOrigin = origin || points[0];
|
||||
const result = buildResult(points, baseOrigin);
|
||||
if (submit.value) {
|
||||
try {
|
||||
submit.value(result);
|
||||
} catch (error) {
|
||||
console.error("[PathDrawingOverlay] path drawing submit failed", error);
|
||||
}
|
||||
} else {
|
||||
const path = new Path(result.options);
|
||||
App.execute(new AddObjectCommand(path), `Add Path: ${result.options.name}`);
|
||||
}
|
||||
cleanupPreview();
|
||||
pathDrawingStore.finish();
|
||||
}
|
||||
|
||||
function cancelDrawing() {
|
||||
if (!isDrawing.value) return;
|
||||
cleanupPreview();
|
||||
pathDrawingStore.cancel();
|
||||
}
|
||||
|
||||
function stopDrawing() {
|
||||
unbindEvents();
|
||||
resetState();
|
||||
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (viewer) {
|
||||
viewer.container.style.cursor = prevCursor || "";
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (!viewer) return;
|
||||
|
||||
prevCursor = viewer.container.style.cursor || "";
|
||||
viewer.container.style.cursor = "crosshair";
|
||||
|
||||
viewer.container.addEventListener("pointerdown", onPointerDown, true);
|
||||
viewer.container.addEventListener("pointerup", onPointerUp, true);
|
||||
viewer.container.addEventListener("dblclick", onDoubleClick, true);
|
||||
}
|
||||
|
||||
function unbindEvents() {
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (!viewer) return;
|
||||
viewer.container.removeEventListener("pointerdown", onPointerDown, true);
|
||||
viewer.container.removeEventListener("pointerup", onPointerUp, true);
|
||||
viewer.container.removeEventListener("dblclick", onDoubleClick, true);
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
if (!isDrawing.value) return;
|
||||
if (event.button !== 0) return;
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (!viewer) return;
|
||||
const array = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||
onDownPosition.fromArray(array);
|
||||
isPointerDown = true;
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
if (!isDrawing.value) return;
|
||||
if (event.button !== 0) return;
|
||||
if (!isPointerDown) return;
|
||||
const viewer = viewerRef || (window as any).viewer;
|
||||
if (!viewer) return;
|
||||
const array = Utils.getMousePosition(viewer.container, event.clientX, event.clientY);
|
||||
onUpPosition.fromArray(array);
|
||||
isPointerDown = false;
|
||||
|
||||
if (onDownPosition.distanceTo(onUpPosition) !== 0) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastClickTime < 200) return;
|
||||
lastClickTime = now;
|
||||
|
||||
const point = getPickPoint(event);
|
||||
if (!point) return;
|
||||
addPoint(point);
|
||||
}
|
||||
|
||||
function onDoubleClick(event: MouseEvent) {
|
||||
if (!isDrawing.value) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
finishDrawing();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isDrawing.value,
|
||||
active => {
|
||||
if (active) {
|
||||
resetState();
|
||||
cloneTemplate();
|
||||
bindEvents();
|
||||
} else {
|
||||
stopDrawing();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
Hooks.useAddSignal("intersectionsDetected", haltSelectionOnIntersect, undefined, 999);
|
||||
Hooks.useAddSignal("viewerInitCompleted", setViewer);
|
||||
if ((window as any).viewer) {
|
||||
setViewer((window as any).viewer);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Hooks.useRemoveSignal("intersectionsDetected", haltSelectionOnIntersect);
|
||||
Hooks.useRemoveSignal("viewerInitCompleted", setViewer);
|
||||
stopDrawing();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card v-if="isDrawing" size="small" class="path-drawing-overlay">
|
||||
<n-space size="small" align="center" @pointerdown.stop>
|
||||
<n-text strong>
|
||||
{{ t("layout.scene.path['Path drawing: Left-click to add a point, and double-click to end the drawing']") }}
|
||||
</n-text>
|
||||
<n-button size="small" type="primary" @click="finishDrawing">{{ t("other.Finish") }}</n-button>
|
||||
<n-button size="small" @click="cancelDrawing">{{ t("other.Cancel") }}</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.path-drawing-overlay {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
transform: translateX(-50%);
|
||||
z-index: 20;
|
||||
}
|
||||
</style>
|
||||
@ -11,6 +11,8 @@
|
||||
|
||||
<!-- IFC BIM 构件信息悬浮框 -->
|
||||
<IFCProperties/>
|
||||
|
||||
<PathDrawingOverlay/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -18,6 +20,7 @@
|
||||
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
||||
import {App,Viewer,Hooks} from "@astral3d/engine";
|
||||
import Toolbar from "./Toolbar.vue";
|
||||
import PathDrawingOverlay from "./PathDrawingOverlay.vue";
|
||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||
import {usePluginStore} from "@/store/modules/plugin";
|
||||
import {installBuiltinPlugin} from "@/plugin";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user