feat(all): 拖入花线迁移
This commit is contained in:
parent
2605656dc4
commit
986da8ce42
@ -122,6 +122,9 @@ export default {
|
|||||||
world: '世界坐标',
|
world: '世界坐标',
|
||||||
"Status monitoring":"状态监控",
|
"Status monitoring":"状态监控",
|
||||||
},
|
},
|
||||||
|
path: {
|
||||||
|
"Path drawing: Left-click to add a point, and double-click to end the drawing": "路径绘制:左键单击添加点,双击结束绘制",
|
||||||
|
},
|
||||||
viewportInfo: {
|
viewportInfo: {
|
||||||
Objects: '物体',
|
Objects: '物体',
|
||||||
Vertices: '顶点',
|
Vertices: '顶点',
|
||||||
@ -754,6 +757,7 @@ export default {
|
|||||||
'Query failed': "查询失败",
|
'Query failed': "查询失败",
|
||||||
'Related document': "相关文档",
|
'Related document': "相关文档",
|
||||||
Copy: "复制",
|
Copy: "复制",
|
||||||
|
Finish: "完成",
|
||||||
Focus: '聚焦',
|
Focus: '聚焦',
|
||||||
Support: '支持',
|
Support: '支持',
|
||||||
Upload: '上传',
|
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 { Box3, Vector3, type Object3D } from "three";
|
||||||
import { cpt } from "@/language";
|
import { cpt } from "@/language";
|
||||||
import { useDragStore } from "@/store/modules/drag";
|
import { useDragStore } from "@/store/modules/drag";
|
||||||
|
import { usePathDrawingStore } from "@/store/modules/pathDrawing";
|
||||||
import { screenToWorld } from "@/utils/common/scenes";
|
import { screenToWorld } from "@/utils/common/scenes";
|
||||||
import { App, AddObjectCommand, Heatmap, Path, UIPanel, Water, type Preview } from "@astral3d/engine";
|
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 searchText = inject("searchText") as Ref<string>;
|
||||||
const previewInfo = inject("previewInfo") as any;
|
const previewInfo = inject("previewInfo") as any;
|
||||||
const previewRef = inject("previewRef") as any;
|
const previewRef = inject("previewRef") as any;
|
||||||
|
const pathDrawingStore = usePathDrawingStore();
|
||||||
|
|
||||||
const activeSubCategory = ref("heatmap");
|
const activeSubCategory = ref("heatmap");
|
||||||
const subCategories = ref([
|
const subCategories = ref([
|
||||||
@ -514,6 +516,17 @@ function selectSubCategory(key: string) {
|
|||||||
activeSubCategory.value = key;
|
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 {
|
function getDefaultAddPosition(): number[] | undefined {
|
||||||
const container = window.viewer?.container;
|
const container = window.viewer?.container;
|
||||||
if (!container) return undefined;
|
if (!container) return undefined;
|
||||||
@ -580,8 +593,7 @@ function addToScene(item: ExpansionItem, position?: number[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "path": {
|
case "path": {
|
||||||
const path = new Path(options);
|
startPathDrawing(item);
|
||||||
App.execute(new AddObjectCommand(path), `Add Path: ${options.name}`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "uipanel": {
|
case "uipanel": {
|
||||||
@ -603,6 +615,12 @@ function dragStart(item: ExpansionItem) {
|
|||||||
function dragEnd() {
|
function dragEnd() {
|
||||||
if (dragStore.getActionTarget !== "addToScene" || dragStore.endArea !== "Scene") return;
|
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();
|
const position = screenToWorld(dragStore.endPosition.x, dragStore.endPosition.y).toArray();
|
||||||
addToScene(dragStore.getData, position);
|
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 构件信息悬浮框 -->
|
<!-- IFC BIM 构件信息悬浮框 -->
|
||||||
<IFCProperties/>
|
<IFCProperties/>
|
||||||
|
|
||||||
|
<PathDrawingOverlay/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -18,6 +20,7 @@
|
|||||||
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
import {onMounted, ref, nextTick, onBeforeUnmount} from 'vue';
|
||||||
import {App,Viewer,Hooks} from "@astral3d/engine";
|
import {App,Viewer,Hooks} from "@astral3d/engine";
|
||||||
import Toolbar from "./Toolbar.vue";
|
import Toolbar from "./Toolbar.vue";
|
||||||
|
import PathDrawingOverlay from "./PathDrawingOverlay.vue";
|
||||||
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
import {useGlobalConfigStore} from "@/store/modules/globalConfig";
|
||||||
import {usePluginStore} from "@/store/modules/plugin";
|
import {usePluginStore} from "@/store/modules/plugin";
|
||||||
import {installBuiltinPlugin} from "@/plugin";
|
import {installBuiltinPlugin} from "@/plugin";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user