class ModelEditor { constructor(viewer, entity) { this.viewer = viewer; this.entity = entity; this.handler = null; this.billboardCollection = null; this.polylineCollection = null; this.hoverBillboard = null; this.isDragging = false; this.dragType = null; this.dragAxis = null; this.startMousePos = null; this.startScreenPos = null; this.startWorldPos = null; this.dragWorldAxis = null; this.startModelMatrix = null; this.startCenter = null; this.center = null; this.radius = 2; this.init(); } init() { this.cleanup(); const scene = this.viewer.scene; const canvas = scene.canvas; this.handler = new Cesium.ScreenSpaceEventHandler(canvas); this.billboardCollection = scene.primitives.add(new Cesium.BillboardCollection()); this.polylineCollection = scene.primitives.add(new Cesium.PolylineCollection()); this.center = this.entity.position.getValue(this.viewer.clock.currentTime); this.createAllControls(); this.setupEvents(); } createAllControls() { const axisLength = this.radius * 0.8; const rotateRadius = this.radius; const axes = [ { key: 'X', color: Cesium.Color.RED, dir: new Cesium.Cartesian3(1, 0, 0) }, { key: 'Y', color: Cesium.Color.GREEN, dir: new Cesium.Cartesian3(0, 1, 0) }, { key: 'Z', color: Cesium.Color.BLUE, dir: new Cesium.Cartesian3(0, 0, 1) }, ]; // 平移轴 axes.forEach(({ key, color, dir }) => { const end = this.localToWorld(new Cesium.Cartesian3(dir.x * axisLength, dir.y * axisLength, dir.z * axisLength)); this.billboardCollection.add({ position: end, image: this.makeArrow(color), disableDepthTestDistance: Number.POSITIVE_INFINITY, id: { type: 'translate', axis: key } }); this.polylineCollection.add({ positions: [this.center, end], width: 3, material: Cesium.Material.fromType('Color', { color }) }); }); // 旋转圆环 axes.forEach(({ key, color }) => { const points = []; for (let i = 0; i <= 64; i++) { const ang = i / 64 * Math.PI * 2; let p; if (key === 'X') p = new Cesium.Cartesian3(0, Math.cos(ang) * rotateRadius, Math.sin(ang) * rotateRadius); else if (key === 'Y') p = new Cesium.Cartesian3(Math.cos(ang) * rotateRadius, 0, Math.sin(ang) * rotateRadius); else p = new Cesium.Cartesian3(Math.cos(ang) * rotateRadius, Math.sin(ang) * rotateRadius, 0); points.push(this.localToWorld(p)); } this.polylineCollection.add({ positions: points, width: 2, material: Cesium.Material.fromType('Color', { color }) }); let rotPos; if (key === 'X') rotPos = this.localToWorld(new Cesium.Cartesian3(0, rotateRadius, 0)); else if (key === 'Y') rotPos = this.localToWorld(new Cesium.Cartesian3(rotateRadius, 0, 0)); else rotPos = this.localToWorld(new Cesium.Cartesian3(rotateRadius, 0, 0)); this.billboardCollection.add({ position: rotPos, image: this.makeRing(color), disableDepthTestDistance: Number.POSITIVE_INFINITY, id: { type: 'rotate', axis: key } }); }); // 缩放方块 axes.forEach(({ key, color, dir }) => { const end = this.localToWorld(new Cesium.Cartesian3(dir.x * axisLength, dir.y * axisLength, dir.z * axisLength)); this.billboardCollection.add({ position: end, image: this.makeBox(color), disableDepthTestDistance: Number.POSITIVE_INFINITY, id: { type: 'scale', axis: key } }); }); } setupEvents() { // 鼠标移动 - 悬停 this.handler.setInputAction((e) => { if (this.isDragging) return; const picked = this.pick(e.endPosition); if (picked !== this.hoverBillboard) { if (this.hoverBillboard) { this.hoverBillboard.scale = 1.0; this.hoverBillboard.color = Cesium.Color.WHITE; } this.hoverBillboard = picked; if (this.hoverBillboard) { this.hoverBillboard.scale = 1.5; this.hoverBillboard.color = Cesium.Color.YELLOW; } } this.viewer.canvas.style.cursor = picked ? 'pointer' : 'default'; }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // 左键按下 this.handler.setInputAction((e) => { const bb = this.pick(e.position); if (!bb) return; this.isDragging = true; this.dragType = bb.id.type; this.dragAxis = bb.id.axis; this.startMousePos = { x: e.position.x, y: e.position.y }; this.startScreenPos = { x: e.position.x, y: e.position.y }; const localAxis = this.getAxis(this.dragAxis); this.dragWorldAxis = this.localToWorld(localAxis); Cesium.Cartesian3.normalize(this.dragWorldAxis, this.dragWorldAxis); this.startCenter = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position; this.center = Cesium.Cartesian3.clone(this.startCenter); if (this.entity.model) { this.startModelMatrix = Cesium.Matrix4.clone(this.entity.model.modelMatrix); } else { this.startModelMatrix = Cesium.Matrix4.IDENTITY.clone(); } this.viewer.scene.screenSpaceCameraController.enableInputs = false; }, Cesium.ScreenSpaceEventType.LEFT_DOWN); // 拖拽 this.handler.setInputAction((e) => { if (!this.isDragging || !this.startScreenPos) return; const dx = e.endPosition.x - this.startScreenPos.x; const dy = e.endPosition.y - this.startScreenPos.y; if (this.dragType === 'rotate') { const rotateDx = e.endPosition.x - this.startMousePos.x; this.doRotate(rotateDx); this.startMousePos = { x: e.endPosition.x, y: e.endPosition.y }; } else { this.startScreenPos = { x: e.endPosition.x, y: e.endPosition.y }; if (this.dragType === 'translate') this.doTranslate(dx, dy); if (this.dragType === 'scale') this.doScale(dx, dy); } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // 左键抬起 this.handler.setInputAction(() => { this.isDragging = false; this.dragType = null; this.startScreenPos = null; this.dragWorldAxis = null; this.viewer.scene.screenSpaceCameraController.enableInputs = true; }, Cesium.ScreenSpaceEventType.LEFT_UP); } // 平移 doTranslate(dx, dy) { const camera = this.viewer.camera; const center = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position; const dist = Cesium.Cartesian3.distance(camera.position, center); const sensitivity = dist * 0.002; const right = camera.right; const up = camera.up; const mat = Cesium.Transforms.eastNorthUpToFixedFrame(center); const xAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(1, 0, 0), new Cesium.Cartesian3()); const yAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(0, 1, 0), new Cesium.Cartesian3()); const zAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(0, 0, 1), new Cesium.Cartesian3()); Cesium.Cartesian3.subtract(xAxis, center, xAxis); Cesium.Cartesian3.subtract(yAxis, center, yAxis); Cesium.Cartesian3.subtract(zAxis, center, zAxis); let moveDir; if (this.dragAxis === 'X') { moveDir = xAxis; } else if (this.dragAxis === 'Y') { moveDir = yAxis; } else { moveDir = zAxis; } Cesium.Cartesian3.normalize(moveDir, moveDir); const screenDirX = right.x * dx + up.x * (-dy); const screenDirY = right.y * dx + up.y * (-dy); const screenDirZ = right.z * dx + up.z * (-dy); const screenMove = new Cesium.Cartesian3(screenDirX, screenDirY, screenDirZ); const dot = Cesium.Cartesian3.dot(screenMove, moveDir); const moveAmount = dot * sensitivity; const offset = Cesium.Cartesian3.multiplyByScalar(moveDir, moveAmount, new Cesium.Cartesian3()); this.center = Cesium.Cartesian3.add(this.startCenter, offset, new Cesium.Cartesian3()); this.entity.position = Cesium.Cartesian3.clone(this.center); this.startCenter = Cesium.Cartesian3.clone(this.center); this.updateControls(); this.viewer.scene.requestRender(); } updateControls() { if (this.billboardCollection) { this.viewer.scene.primitives.remove(this.billboardCollection); this.viewer.scene.primitives.remove(this.polylineCollection); this.billboardCollection = this.viewer.scene.primitives.add(new Cesium.BillboardCollection()); this.polylineCollection = this.viewer.scene.primitives.add(new Cesium.PolylineCollection()); this.createAllControls(); } } // 缩放 doScale(dx, dy) { const scaleDelta = (dx + dy) * 0.01; const scale = Math.max(0.1, 1 + scaleDelta); const modelMatrix = this.entity.model.modelMatrix; const scaleVec = new Cesium.Cartesian3(1, 1, 1); if (this.dragAxis === 'X') scaleVec.x = scale; if (this.dragAxis === 'Y') scaleVec.y = scale; if (this.dragAxis === 'Z') scaleVec.z = scale; const currentScale = Cesium.Matrix4.getScale(modelMatrix, new Cesium.Cartesian3()); const newScale = new Cesium.Cartesian3( currentScale.x * scaleVec.x, currentScale.y * scaleVec.y, currentScale.z * scaleVec.z ); const translation = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3()); const rotation = Cesium.Matrix4.getRotation(modelMatrix, new Cesium.Matrix3()); const newModelMatrix = Cesium.Matrix4.fromTranslationRotationScale(translation, rotation, newScale); this.entity.model.modelMatrix = newModelMatrix; this.viewer.scene.requestRender(); } // 旋转 doRotate(dx) { if (!this.entity.model) return; const center = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position; const angleSensitivity = 0.005; const angle = dx * angleSensitivity; let rotateAxis; if (this.dragAxis === 'X') { rotateAxis = new Cesium.Cartesian3(1, 0, 0); } else if (this.dragAxis === 'Y') { rotateAxis = new Cesium.Cartesian3(0, 1, 0); } else { rotateAxis = new Cesium.Cartesian3(0, 0, 1); } const mat = Cesium.Transforms.eastNorthUpToFixedFrame(center); const worldAxis = Cesium.Matrix4.multiplyByPoint(mat, rotateAxis, new Cesium.Cartesian3()); Cesium.Cartesian3.subtract(worldAxis, center, worldAxis); Cesium.Cartesian3.normalize(worldAxis, worldAxis); const quat = Cesium.Quaternion.fromAxisAngle(worldAxis, angle); const rotationMatrix = Cesium.Matrix3.fromQuaternion(quat); const modelMatrix = this.entity.model.modelMatrix; const translation = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3()); const scale = Cesium.Matrix4.getScale(modelMatrix, new Cesium.Cartesian3()); const currentRotMatrix = Cesium.Matrix4.getRotation(modelMatrix, new Cesium.Matrix3()); const newRotMatrix = Cesium.Matrix3.multiply(currentRotMatrix, rotationMatrix, new Cesium.Matrix3()); const newModelMatrix = Cesium.Matrix4.fromTranslationRotationScale(translation, newRotMatrix, scale); this.entity.model.modelMatrix = newModelMatrix; this.viewer.scene.requestRender(); } // 拾取 pick(windowPos) { const bbs = this.billboardCollection._billboards || []; for (const bb of bbs) { if (!bb?.position || !bb.id) continue; const screenPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, bb.position); if (!screenPos) continue; const dist = Math.hypot(screenPos.x - windowPos.x, screenPos.y - windowPos.y); if (dist < 50) return bb; } return null; } getAxis(axis) { switch (axis) { case 'X': return new Cesium.Cartesian3(1, 0, 0); case 'Y': return new Cesium.Cartesian3(0, 1, 0); case 'Z': return new Cesium.Cartesian3(0, 0, 1); default: return new Cesium.Cartesian3(); } } localToWorld(v) { const mat = Cesium.Transforms.eastNorthUpToFixedFrame(this.center); return Cesium.Matrix4.multiplyByPoint(mat, v, new Cesium.Cartesian3()); } makeArrow(color) { const c = document.createElement('canvas'); c.width = 32; c.height = 32; const ctx = c.getContext('2d'); ctx.fillStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`; ctx.beginPath(); ctx.moveTo(4, 16); ctx.lineTo(24, 16); ctx.lineTo(24, 6); ctx.lineTo(30, 16); ctx.lineTo(24, 26); ctx.closePath(); ctx.fill(); return c.toDataURL(); } makeBox(color) { const c = document.createElement('canvas'); c.width = 24; c.height = 24; const ctx = c.getContext('2d'); ctx.fillStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`; ctx.fillRect(6, 6, 12, 12); return c.toDataURL(); } makeRing(color) { const c = document.createElement('canvas'); c.width = 24; c.height = 24; const ctx = c.getContext('2d'); ctx.strokeStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(12, 12, 8, 0, Math.PI * 2); ctx.stroke(); return c.toDataURL(); } cleanup() { if (this.handler) this.handler.destroy(); if (this.billboardCollection) this.viewer.scene.primitives.remove(this.billboardCollection); if (this.polylineCollection) this.viewer.scene.primitives.remove(this.polylineCollection); this.viewer.scene.screenSpaceCameraController.enableInputs = true; } destroy() { this.cleanup(); } } export default ModelEditor;