ModelEditor.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. class ModelEditor {
  2. constructor(viewer, entity) {
  3. this.viewer = viewer;
  4. this.entity = entity;
  5. this.handler = null;
  6. this.billboardCollection = null;
  7. this.polylineCollection = null;
  8. this.hoverBillboard = null;
  9. this.isDragging = false;
  10. this.dragType = null;
  11. this.dragAxis = null;
  12. this.startMousePos = null;
  13. this.startScreenPos = null;
  14. this.startWorldPos = null;
  15. this.dragWorldAxis = null;
  16. this.startModelMatrix = null;
  17. this.startCenter = null;
  18. this.center = null;
  19. this.radius = 2;
  20. this.init();
  21. }
  22. init() {
  23. this.cleanup();
  24. const scene = this.viewer.scene;
  25. const canvas = scene.canvas;
  26. this.handler = new Cesium.ScreenSpaceEventHandler(canvas);
  27. this.billboardCollection = scene.primitives.add(new Cesium.BillboardCollection());
  28. this.polylineCollection = scene.primitives.add(new Cesium.PolylineCollection());
  29. this.center = this.entity.position.getValue(this.viewer.clock.currentTime);
  30. this.createAllControls();
  31. this.setupEvents();
  32. }
  33. createAllControls() {
  34. const axisLength = this.radius * 0.8;
  35. const rotateRadius = this.radius;
  36. const axes = [
  37. { key: 'X', color: Cesium.Color.RED, dir: new Cesium.Cartesian3(1, 0, 0) },
  38. { key: 'Y', color: Cesium.Color.GREEN, dir: new Cesium.Cartesian3(0, 1, 0) },
  39. { key: 'Z', color: Cesium.Color.BLUE, dir: new Cesium.Cartesian3(0, 0, 1) },
  40. ];
  41. // 平移轴
  42. axes.forEach(({ key, color, dir }) => {
  43. const end = this.localToWorld(new Cesium.Cartesian3(dir.x * axisLength, dir.y * axisLength, dir.z * axisLength));
  44. this.billboardCollection.add({
  45. position: end,
  46. image: this.makeArrow(color),
  47. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  48. id: { type: 'translate', axis: key }
  49. });
  50. this.polylineCollection.add({
  51. positions: [this.center, end],
  52. width: 3,
  53. material: Cesium.Material.fromType('Color', { color })
  54. });
  55. });
  56. // 旋转圆环
  57. axes.forEach(({ key, color }) => {
  58. const points = [];
  59. for (let i = 0; i <= 64; i++) {
  60. const ang = i / 64 * Math.PI * 2;
  61. let p;
  62. if (key === 'X') p = new Cesium.Cartesian3(0, Math.cos(ang) * rotateRadius, Math.sin(ang) * rotateRadius);
  63. else if (key === 'Y') p = new Cesium.Cartesian3(Math.cos(ang) * rotateRadius, 0, Math.sin(ang) * rotateRadius);
  64. else p = new Cesium.Cartesian3(Math.cos(ang) * rotateRadius, Math.sin(ang) * rotateRadius, 0);
  65. points.push(this.localToWorld(p));
  66. }
  67. this.polylineCollection.add({
  68. positions: points,
  69. width: 2,
  70. material: Cesium.Material.fromType('Color', { color })
  71. });
  72. let rotPos;
  73. if (key === 'X') rotPos = this.localToWorld(new Cesium.Cartesian3(0, rotateRadius, 0));
  74. else if (key === 'Y') rotPos = this.localToWorld(new Cesium.Cartesian3(rotateRadius, 0, 0));
  75. else rotPos = this.localToWorld(new Cesium.Cartesian3(rotateRadius, 0, 0));
  76. this.billboardCollection.add({
  77. position: rotPos,
  78. image: this.makeRing(color),
  79. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  80. id: { type: 'rotate', axis: key }
  81. });
  82. });
  83. // 缩放方块
  84. axes.forEach(({ key, color, dir }) => {
  85. const end = this.localToWorld(new Cesium.Cartesian3(dir.x * axisLength, dir.y * axisLength, dir.z * axisLength));
  86. this.billboardCollection.add({
  87. position: end,
  88. image: this.makeBox(color),
  89. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  90. id: { type: 'scale', axis: key }
  91. });
  92. });
  93. }
  94. setupEvents() {
  95. // 鼠标移动 - 悬停
  96. this.handler.setInputAction((e) => {
  97. if (this.isDragging) return;
  98. const picked = this.pick(e.endPosition);
  99. if (picked !== this.hoverBillboard) {
  100. if (this.hoverBillboard) {
  101. this.hoverBillboard.scale = 1.0;
  102. this.hoverBillboard.color = Cesium.Color.WHITE;
  103. }
  104. this.hoverBillboard = picked;
  105. if (this.hoverBillboard) {
  106. this.hoverBillboard.scale = 1.5;
  107. this.hoverBillboard.color = Cesium.Color.YELLOW;
  108. }
  109. }
  110. this.viewer.canvas.style.cursor = picked ? 'pointer' : 'default';
  111. }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
  112. // 左键按下
  113. this.handler.setInputAction((e) => {
  114. const bb = this.pick(e.position);
  115. if (!bb) return;
  116. this.isDragging = true;
  117. this.dragType = bb.id.type;
  118. this.dragAxis = bb.id.axis;
  119. this.startMousePos = { x: e.position.x, y: e.position.y };
  120. this.startScreenPos = { x: e.position.x, y: e.position.y };
  121. const localAxis = this.getAxis(this.dragAxis);
  122. this.dragWorldAxis = this.localToWorld(localAxis);
  123. Cesium.Cartesian3.normalize(this.dragWorldAxis, this.dragWorldAxis);
  124. this.startCenter = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position;
  125. this.center = Cesium.Cartesian3.clone(this.startCenter);
  126. if (this.entity.model) {
  127. this.startModelMatrix = Cesium.Matrix4.clone(this.entity.model.modelMatrix);
  128. } else {
  129. this.startModelMatrix = Cesium.Matrix4.IDENTITY.clone();
  130. }
  131. this.viewer.scene.screenSpaceCameraController.enableInputs = false;
  132. }, Cesium.ScreenSpaceEventType.LEFT_DOWN);
  133. // 拖拽
  134. this.handler.setInputAction((e) => {
  135. if (!this.isDragging || !this.startScreenPos) return;
  136. const dx = e.endPosition.x - this.startScreenPos.x;
  137. const dy = e.endPosition.y - this.startScreenPos.y;
  138. if (this.dragType === 'rotate') {
  139. const rotateDx = e.endPosition.x - this.startMousePos.x;
  140. this.doRotate(rotateDx);
  141. this.startMousePos = { x: e.endPosition.x, y: e.endPosition.y };
  142. } else {
  143. this.startScreenPos = { x: e.endPosition.x, y: e.endPosition.y };
  144. if (this.dragType === 'translate') this.doTranslate(dx, dy);
  145. if (this.dragType === 'scale') this.doScale(dx, dy);
  146. }
  147. }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
  148. // 左键抬起
  149. this.handler.setInputAction(() => {
  150. this.isDragging = false;
  151. this.dragType = null;
  152. this.startScreenPos = null;
  153. this.dragWorldAxis = null;
  154. this.viewer.scene.screenSpaceCameraController.enableInputs = true;
  155. }, Cesium.ScreenSpaceEventType.LEFT_UP);
  156. }
  157. // 平移
  158. doTranslate(dx, dy) {
  159. const camera = this.viewer.camera;
  160. const center = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position;
  161. const dist = Cesium.Cartesian3.distance(camera.position, center);
  162. const sensitivity = dist * 0.002;
  163. const right = camera.right;
  164. const up = camera.up;
  165. const mat = Cesium.Transforms.eastNorthUpToFixedFrame(center);
  166. const xAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(1, 0, 0), new Cesium.Cartesian3());
  167. const yAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(0, 1, 0), new Cesium.Cartesian3());
  168. const zAxis = Cesium.Matrix4.multiplyByPoint(mat, new Cesium.Cartesian3(0, 0, 1), new Cesium.Cartesian3());
  169. Cesium.Cartesian3.subtract(xAxis, center, xAxis);
  170. Cesium.Cartesian3.subtract(yAxis, center, yAxis);
  171. Cesium.Cartesian3.subtract(zAxis, center, zAxis);
  172. let moveDir;
  173. if (this.dragAxis === 'X') {
  174. moveDir = xAxis;
  175. } else if (this.dragAxis === 'Y') {
  176. moveDir = yAxis;
  177. } else {
  178. moveDir = zAxis;
  179. }
  180. Cesium.Cartesian3.normalize(moveDir, moveDir);
  181. const screenDirX = right.x * dx + up.x * (-dy);
  182. const screenDirY = right.y * dx + up.y * (-dy);
  183. const screenDirZ = right.z * dx + up.z * (-dy);
  184. const screenMove = new Cesium.Cartesian3(screenDirX, screenDirY, screenDirZ);
  185. const dot = Cesium.Cartesian3.dot(screenMove, moveDir);
  186. const moveAmount = dot * sensitivity;
  187. const offset = Cesium.Cartesian3.multiplyByScalar(moveDir, moveAmount, new Cesium.Cartesian3());
  188. this.center = Cesium.Cartesian3.add(this.startCenter, offset, new Cesium.Cartesian3());
  189. this.entity.position = Cesium.Cartesian3.clone(this.center);
  190. this.startCenter = Cesium.Cartesian3.clone(this.center);
  191. this.updateControls();
  192. this.viewer.scene.requestRender();
  193. }
  194. updateControls() {
  195. if (this.billboardCollection) {
  196. this.viewer.scene.primitives.remove(this.billboardCollection);
  197. this.viewer.scene.primitives.remove(this.polylineCollection);
  198. this.billboardCollection = this.viewer.scene.primitives.add(new Cesium.BillboardCollection());
  199. this.polylineCollection = this.viewer.scene.primitives.add(new Cesium.PolylineCollection());
  200. this.createAllControls();
  201. }
  202. }
  203. // 缩放
  204. doScale(dx, dy) {
  205. const scaleDelta = (dx + dy) * 0.01;
  206. const scale = Math.max(0.1, 1 + scaleDelta);
  207. const modelMatrix = this.entity.model.modelMatrix;
  208. const scaleVec = new Cesium.Cartesian3(1, 1, 1);
  209. if (this.dragAxis === 'X') scaleVec.x = scale;
  210. if (this.dragAxis === 'Y') scaleVec.y = scale;
  211. if (this.dragAxis === 'Z') scaleVec.z = scale;
  212. const currentScale = Cesium.Matrix4.getScale(modelMatrix, new Cesium.Cartesian3());
  213. const newScale = new Cesium.Cartesian3(
  214. currentScale.x * scaleVec.x,
  215. currentScale.y * scaleVec.y,
  216. currentScale.z * scaleVec.z
  217. );
  218. const translation = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());
  219. const rotation = Cesium.Matrix4.getRotation(modelMatrix, new Cesium.Matrix3());
  220. const newModelMatrix = Cesium.Matrix4.fromTranslationRotationScale(translation, rotation, newScale);
  221. this.entity.model.modelMatrix = newModelMatrix;
  222. this.viewer.scene.requestRender();
  223. }
  224. // 旋转
  225. doRotate(dx) {
  226. if (!this.entity.model) return;
  227. const center = this.entity.position.getValue ? this.entity.position.getValue(this.viewer.clock.currentTime) : this.entity.position;
  228. const angleSensitivity = 0.005;
  229. const angle = dx * angleSensitivity;
  230. let rotateAxis;
  231. if (this.dragAxis === 'X') {
  232. rotateAxis = new Cesium.Cartesian3(1, 0, 0);
  233. } else if (this.dragAxis === 'Y') {
  234. rotateAxis = new Cesium.Cartesian3(0, 1, 0);
  235. } else {
  236. rotateAxis = new Cesium.Cartesian3(0, 0, 1);
  237. }
  238. const mat = Cesium.Transforms.eastNorthUpToFixedFrame(center);
  239. const worldAxis = Cesium.Matrix4.multiplyByPoint(mat, rotateAxis, new Cesium.Cartesian3());
  240. Cesium.Cartesian3.subtract(worldAxis, center, worldAxis);
  241. Cesium.Cartesian3.normalize(worldAxis, worldAxis);
  242. const quat = Cesium.Quaternion.fromAxisAngle(worldAxis, angle);
  243. const rotationMatrix = Cesium.Matrix3.fromQuaternion(quat);
  244. const modelMatrix = this.entity.model.modelMatrix;
  245. const translation = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());
  246. const scale = Cesium.Matrix4.getScale(modelMatrix, new Cesium.Cartesian3());
  247. const currentRotMatrix = Cesium.Matrix4.getRotation(modelMatrix, new Cesium.Matrix3());
  248. const newRotMatrix = Cesium.Matrix3.multiply(currentRotMatrix, rotationMatrix, new Cesium.Matrix3());
  249. const newModelMatrix = Cesium.Matrix4.fromTranslationRotationScale(translation, newRotMatrix, scale);
  250. this.entity.model.modelMatrix = newModelMatrix;
  251. this.viewer.scene.requestRender();
  252. }
  253. // 拾取
  254. pick(windowPos) {
  255. const bbs = this.billboardCollection._billboards || [];
  256. for (const bb of bbs) {
  257. if (!bb?.position || !bb.id) continue;
  258. const screenPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, bb.position);
  259. if (!screenPos) continue;
  260. const dist = Math.hypot(screenPos.x - windowPos.x, screenPos.y - windowPos.y);
  261. if (dist < 50) return bb;
  262. }
  263. return null;
  264. }
  265. getAxis(axis) {
  266. switch (axis) {
  267. case 'X': return new Cesium.Cartesian3(1, 0, 0);
  268. case 'Y': return new Cesium.Cartesian3(0, 1, 0);
  269. case 'Z': return new Cesium.Cartesian3(0, 0, 1);
  270. default: return new Cesium.Cartesian3();
  271. }
  272. }
  273. localToWorld(v) {
  274. const mat = Cesium.Transforms.eastNorthUpToFixedFrame(this.center);
  275. return Cesium.Matrix4.multiplyByPoint(mat, v, new Cesium.Cartesian3());
  276. }
  277. makeArrow(color) {
  278. const c = document.createElement('canvas');
  279. c.width = 32; c.height = 32;
  280. const ctx = c.getContext('2d');
  281. ctx.fillStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`;
  282. ctx.beginPath();
  283. ctx.moveTo(4, 16); ctx.lineTo(24, 16); ctx.lineTo(24, 6); ctx.lineTo(30, 16); ctx.lineTo(24, 26); ctx.closePath();
  284. ctx.fill();
  285. return c.toDataURL();
  286. }
  287. makeBox(color) {
  288. const c = document.createElement('canvas');
  289. c.width = 24; c.height = 24;
  290. const ctx = c.getContext('2d');
  291. ctx.fillStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`;
  292. ctx.fillRect(6, 6, 12, 12);
  293. return c.toDataURL();
  294. }
  295. makeRing(color) {
  296. const c = document.createElement('canvas');
  297. c.width = 24; c.height = 24;
  298. const ctx = c.getContext('2d');
  299. ctx.strokeStyle = `rgb(${color.red * 255},${color.green * 255},${color.blue * 255})`;
  300. ctx.lineWidth = 3;
  301. ctx.beginPath();
  302. ctx.arc(12, 12, 8, 0, Math.PI * 2);
  303. ctx.stroke();
  304. return c.toDataURL();
  305. }
  306. cleanup() {
  307. if (this.handler) this.handler.destroy();
  308. if (this.billboardCollection) this.viewer.scene.primitives.remove(this.billboardCollection);
  309. if (this.polylineCollection) this.viewer.scene.primitives.remove(this.polylineCollection);
  310. this.viewer.scene.screenSpaceCameraController.enableInputs = true;
  311. }
  312. destroy() { this.cleanup(); }
  313. }
  314. export default ModelEditor;