ThreeJS(javascript 3D库) 一些常用的功能
1 "use strict"; 2 3 var __emptyPoint = null, __emptyContext = null, __emptyPointA = null; 4 5 const ColorRefTable = { 6 "aliceblue": "#f0f8ff", 7 "antiquewhite": "#faebd7", 8 "aqua": "#00ffff", 9 "aquamarine": "#7fffd4", 10 "azure": "#f0ffff", 11 "beige": "#f5f5dc", 12 "bisque": "#ffe4c4", 13 "black": "#000000", 14 "blanchedalmond": "#ffebcd", 15 "blue": "#0000ff", 16 "blueviolet": "#8a2be2", 17 "brown": "#a52a2a", 18 "burlywood": "#deb887", 19 "cadetblue": "#5f9ea0", 20 "chartreuse": "#7fff00", 21 "chocolate": "#d2691e", 22 "coral": "#ff7f50", 23 "cornsilk": "#fff8dc", 24 "crimson": "#dc143c", 25 "cyan": "#00ffff", 26 "darkblue": "#00008b", 27 "darkcyan": "#008b8b", 28 "darkgoldenrod": "#b8860b", 29 "darkgray": "#a9a9a9", 30 "darkgreen": "#006400", 31 "darkgrey": "#a9a9a9", 32 "darkkhaki": "#bdb76b", 33 "darkmagenta": "#8b008b", 34 "firebrick": "#b22222", 35 "darkolivegreen": "#556b2f", 36 "darkorange": "#ff8c00", 37 "darkorchid": "#9932cc", 38 "darkred": "#8b0000", 39 "darksalmon": "#e9967a", 40 "darkseagreen": "#8fbc8f", 41 "darkslateblue": "#483d8b", 42 "darkslategray": "#2f4f4f", 43 "darkslategrey": "#2f4f4f", 44 "darkturquoise": "#00ced1", 45 "darkviolet": "#9400d3", 46 "deeppink": "#ff1493", 47 "deepskyblue": "#00bfff", 48 "dimgray": "#696969", 49 "dimgrey": "#696969", 50 "dodgerblue": "#1e90ff", 51 "floralwhite": "#fffaf0", 52 "forestgreen": "#228b22", 53 "fuchsia": "#ff00ff", 54 "gainsboro": "#dcdcdc", 55 "ghostwhite": "#f8f8ff", 56 "gold": "#ffd700", 57 "goldenrod": "#daa520", 58 "gray": "#808080", 59 "green": "#008000", 60 "greenyellow": "#adff2f", 61 "grey": "#808080", 62 "honeydew": "#f0fff0", 63 "hotpink": "#ff69b4", 64 "indianred": "#cd5c5c", 65 "indigo": "#4b0082", 66 "ivory": "#fffff0", 67 "khaki": "#f0e68c", 68 "lavender": "#e6e6fa", 69 "lavenderblush": "#fff0f5", 70 "lawngreen": "#7cfc00", 71 "lemonchiffon": "#fffacd", 72 "lightblue": "#add8e6", 73 "lightcoral": "#f08080", 74 "lightcyan": "#e0ffff", 75 "lightgoldenrodyellow": "#fafad2", 76 "lightgray": "#d3d3d3", 77 "lightgreen": "#90ee90", 78 "lightgrey": "#d3d3d3", 79 "lightpink": "#ffb6c1", 80 "lightsalmon": "#ffa07a", 81 "lightseagreen": "#20b2aa", 82 "lightskyblue": "#87cefa", 83 "lightslategray": "#778899", 84 "lightslategrey": "#778899", 85 "lightsteelblue": "#b0c4de", 86 "lightyellow": "#ffffe0", 87 "lime": "#00ff00", 88 "limegreen": "#32cd32", 89 "linen": "#faf0e6", 90 "magenta": "#ff00ff", 91 "maroon": "#800000", 92 "mediumaquamarine": "#66cdaa", 93 "mediumblue": "#0000cd", 94 "mediumorchid": "#ba55d3", 95 "mediumpurple": "#9370db", 96 "mediumseagreen": "#3cb371", 97 "mediumslateblue": "#7b68ee", 98 "mediumspringgreen": "#00fa9a", 99 "mediumturquoise": "#48d1cc", 100 "mediumvioletred": "#c71585", 101 "midnightblue": "#191970", 102 "mintcream": "#f5fffa", 103 "mistyrose": "#ffe4e1", 104 "moccasin": "#ffe4b5", 105 "navajowhite": "#ffdead", 106 "navy": "#000080", 107 "oldlace": "#fdf5e6", 108 "olive": "#808000", 109 "olivedrab": "#6b8e23", 110 "orange": "#ffa500", 111 "orangered": "#ff4500", 112 "orchid": "#da70d6", 113 "palegoldenrod": "#eee8aa", 114 "palegreen": "#98fb98", 115 "paleturquoise": "#afeeee", 116 "palevioletred": "#db7093", 117 "papayawhip": "#ffefd5", 118 "peachpuff": "#ffdab9", 119 "peru": "#cd853f", 120 "pink": "#ffc0cb", 121 "plum": "#dda0dd", 122 "powderblue": "#b0e0e6", 123 "purple": "#800080", 124 "red": "#ff0000", 125 "rosybrown": "#bc8f8f", 126 "royalblue": "#4169e1", 127 "saddlebrown": "#8b4513", 128 "salmon": "#fa8072", 129 "sandybrown": "#f4a460", 130 "seagreen": "#2e8b57", 131 "seashell": "#fff5ee", 132 "sienna": "#a0522d", 133 "silver": "#c0c0c0", 134 "skyblue": "#87ceeb", 135 "slateblue": "#6a5acd", 136 "slategray": "#708090", 137 "slategrey": "#708090", 138 "snow": "#fffafa", 139 "springgreen": "#00ff7f", 140 "steelblue": "#4682b4", 141 "tan": "#d2b48c", 142 "teal": "#008080", 143 "thistle": "#d8bfd8", 144 "tomato": "#ff6347", 145 "turquoise": "#40e0d0", 146 "violet": "#ee82ee", 147 "wheat": "#f5deb3", 148 "white": "#ffffff", 149 "whitesmoke": "#f5f5f5", 150 "yellow": "#ffff00", 151 "yellowgreen": "#9acd32" 152 }, 153 154 UTILS = { 155 156 get isMobile(){ 157 return /Android|webOS|iPhone|iPod|BlackBerry|SymbianOS|Windows Phone|iPad/i.test(navigator.userAgent); 158 }, 159 160 emptyArray(arr){ 161 return !Array.isArray(arr) || arr.length === 0; 162 }, 163 164 createContext(w = 1, h = 1){ 165 const canvas = document.createElement("canvas"), 166 context = canvas.getContext("2d"); 167 canvas.width = w; 168 canvas.height = h; 169 return context; 170 }, 171 172 createElem(tagName, className = "", textContent = ""){ 173 const elem = document.createElement(tagName); 174 if(className !== "") elem.className = className; 175 if(textContent !== "") elem.textContent = textContent; 176 return elem; 177 }, 178 179 createInput(type, className = ""){ 180 const input = document.createElement("input"); 181 input.type = type; 182 if(className !== "") input.className = className; 183 return input; 184 }, 185 186 appendChilds(parentElem, ...nodes){ 187 const msgContainer = document.createDocumentFragment(); 188 for(let k = 0, len = nodes.length; k < len; k++) msgContainer.appendChild(nodes[k]); 189 parentElem.appendChild(msgContainer); 190 }, 191 192 isObject(obj){ 193 194 return obj !== null && typeof obj === "object" && Array.isArray(obj) === false; 195 196 }, 197 198 isNumber(num){ 199 200 return typeof num === "number" && isNaN(num) === false; 201 202 }, 203 204 //获取最后一个点后面的字符 205 getFileType(string){ 206 let type = "", str = string.split('').reverse().join(''); 207 for(let k = 0, len = str.length; k < len; k++){ 208 if(str[k] === ".") break; 209 type += str[k]; 210 } 211 return type.split('').reverse().join(''); 212 }, 213 214 //删除 string 所有的空格 215 deleteSpaceAll(str){ 216 const len = str.length; 217 var result = ''; 218 for(let i = 0; i < len; i++){ 219 if(str[i] !== '') result += str[i] 220 } 221 222 return result 223 }, 224 225 //删除 string 两边空格 226 removeSpaceSides(string){ 227 228 return string.replace(/(^\s*)|(\s*$)/g, ""); 229 230 }, 231 232 //返回 num 与 num1 之间的随机数 233 random(num, num1){ 234 235 if(num < num1) return Math.random() * (num1 - num) + num; 236 237 else if(num > num1) return Math.random() * (num - num1) + num1; 238 239 else return num; 240 241 }, 242 243 //生成 UUID 244 generateUUID: function (){ 245 const _lut = []; 246 247 for ( let i = 0; i < 256; i ++ ) { 248 249 _lut[ i ] = ( i < 16 ? '0' : '' ) + ( i ).toString( 16 ); 250 251 } 252 253 return function (){ 254 const d0 = Math.random() * 0xffffffff | 0; 255 const d1 = Math.random() * 0xffffffff | 0; 256 const d2 = Math.random() * 0xffffffff | 0; 257 const d3 = Math.random() * 0xffffffff | 0; 258 const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' + 259 _lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' + 260 _lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] + 261 _lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ]; 262 263 return uuid.toLowerCase(); //toLowerCase() 这里展平连接的字符串以节省堆内存空间 264 } 265 }(), 266 267 //欧几里得距离(两点的直线距离) 268 distance(x, y, x1, y1){ 269 270 return Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2)); 271 272 }, 273 274 downloadFile(blob, fileName){ 275 const link = document.createElement("a"); 276 link.href = URL.createObjectURL(blob); 277 link.download = fileName; 278 link.click(); 279 }, 280 281 loadFileJSON(callback){ 282 const input = document.createElement("input"); 283 input.type = "file"; 284 input.accept = ".json"; 285 286 input.onchange = a => { 287 if(a.target.files.length === 0) return; 288 const fr = new FileReader(); 289 fr.onloadend = b => callback(b.target.result); 290 fr.readAsText(a.target.files[0]); //fr.readAsDataURL(a.target.files[0]); 291 } 292 293 input.click(); 294 }, 295 296 get emptyPoint(){ 297 if(__emptyPoint === null) __emptyPoint = new Point(); 298 return __emptyPoint; 299 }, 300 301 get emptyPointA(){ 302 if(__emptyPointA === null) __emptyPointA = new Point(); 303 return __emptyPointA; 304 }, 305 306 get emptyContext(){ 307 if(__emptyContext === null) __emptyContext = document.createElement("canvas").getContext('2d') 308 return __emptyContext; 309 }, 310 311 /* 把 target 以相等的比例缩放至 result 大小 312 target, result: Object{width, height}; //也可以是img元素 313 */ 314 setSizeToSameScale(target, result = {width: 100, height: 100}){ 315 if(!target) return result; 316 317 const width = target.width, 318 height = target.height, 319 ratio = width / height, 320 scale = ratio < 1 ? ratio * result.width / width : result.width / width; 321 322 result.width = width * scale; 323 result.height = height * scale; 324 325 return result; 326 }, 327 328 //小数保留 count 位 329 floatKeep(float, count = 3){ 330 count = Math.pow(10, count); 331 return Math.ceil(float * count) / count; 332 }, 333 334 } 335 336 337 338 339 /* SecurityDoor 安检门 340 作用: 有时候我们要给一个功能加一个启禁用锁, 就比如用一个.enable属性表示此功能是否启用, 341 a, b是两个使用此功能的人; a 需要把此功能禁用(.enable = false), 342 过了一会b也要禁用此功能(.enable = false), 又过了一会a又要在次启用此功能, 343 此时a让此功能启用了(.enable = true), 很显然这违背了b, 对于b来说此功能还在禁用状态, 344 实际并非如b所想, 所以当多个人使用此功能时一个.enable满足不了它们; 345 或许还有其它解决方法就比如让所有使用此功能的人(a,b)加一个属性表示自己是否可以使用此功能, 346 但我更希望用一个专门的数组去装载它们, 而不是在某个使用者身上都添加一个属性; 347 348 parameter: 349 list: Array[]; 350 351 attribute: 352 list: Array; //私有, 默认 null 353 empty: Bool; //只读, 列表是否为空 354 355 method: 356 clear() 357 equals(v) 358 add(v) 359 remove(v) 360 */ 361 class SecurityDoor{ 362 363 #list = null; 364 #onchange = null; 365 366 get empty(){ 367 return this.#list === null; 368 } 369 370 constructor(list, onchange){ 371 if(UTILS.emptyArray(list) === false) this.#list = list; 372 if(typeof onchange === "function") this.#onchange = onchange; 373 } 374 375 add(sign){ 376 if(this.#list !== null){ 377 if(this.#list.includes(sign) === false) this.#list.push(sign); 378 } 379 else{ 380 this.#list = [sign]; 381 if(this.#onchange !== null) this.#onchange(false); 382 } 383 } 384 385 clear(){ 386 this.#list = null; 387 } 388 389 equals(sign){ 390 return this.#list !== null && this.#list.includes(sign); 391 } 392 393 remove(sign){ 394 if(this.#list !== null){ 395 sign = this.#list.indexOf(sign); 396 if(sign !== -1){ 397 if(this.#list.length > 1) this.#list.splice(sign, 1); 398 else{ 399 this.#list = null; 400 if(this.#onchange !== null) this.#onchange(true); 401 } 402 } 403 } 404 } 405 406 } 407 408 409 410 411 /* RunningList 412 413 */ 414 class RunningList{ 415 416 #runName = ""; 417 #running = false; 418 #list = []; 419 #disabls = []; 420 421 constructor(runName = 'update'){ 422 this.#runName = runName; 423 } 424 425 clear(){ 426 this.#list.length = 0; 427 this.#disabls.length = 0; 428 } 429 430 add(v){ 431 if(this.#list.includes(v) === false) this.#list.push(v); 432 else{ 433 const i = this.#disabls.indexOf(v); 434 if(i !== -1) this.#disabls.splice(i, 1); 435 } 436 } 437 438 remove(v){ 439 if(this.#running) this.#disabls.push(v); 440 else{ 441 const i = this.#list.indexOf(v); 442 if(i !== -1) this.#list.splice(i, 1); 443 } 444 } 445 446 update(){ 447 var len = this.#list.length; 448 if(len === 0) return; 449 450 var k; 451 this.#running = true; 452 if(this.#runName !== ''){ 453 for(k = 0; k < len; k++) this.#list[k][this.#runName](); 454 }else{ 455 for(k = 0; k < len; k++) this.#list[k](); 456 } 457 this.#running = false; 458 459 var i; 460 len = this.#disabls.length; 461 for(k = 0; k < len; k++){ 462 i = this.#list.indexOf(this.#disabls[k]); 463 if(i !== -1) this.#list.splice(i, 1); 464 } 465 this.#disabls.length = 0; 466 } 467 468 } 469 470 471 472 473 /* AnimateLoop 动画循环 474 attributes: 475 onupdate: Func; 476 //只读: 477 running: Bool; 478 479 method: 480 play(onupdate: Func): this; //onupdate 可选 481 stop(): this; 482 update(): undefined; //使用前必须已定义.onupdate 属性 483 */ 484 class AnimateLoop{ 485 486 #id = -1; 487 #animate = null; 488 489 get running(){ 490 return this.#id !== -1; 491 } 492 493 constructor(onupdate = null){ 494 const animate = () => { 495 this.#id = requestAnimationFrame(animate); 496 this.onupdate(); 497 } 498 499 this.#animate = animate; 500 this.onupdate = onupdate; 501 } 502 503 stop(){ 504 if(this.#id !== -1){ 505 cancelAnimationFrame(this.#id); 506 this.#id = -1; 507 } 508 return this; 509 } 510 511 play(onupdate){ 512 if(typeof onupdate === "function") this.onupdate = onupdate; 513 if(this.onupdate !== null){ 514 this.stop(); 515 this.#animate(); 516 } 517 return this; 518 } 519 520 update(){ 521 if(this.#id === -1){ 522 this.onupdate(); 523 } 524 } 525 526 } 527 528 529 530 531 /** Ajax 532 parameter: 533 option = { 534 url: 可选, 默认 '' 535 method: 可选, post 或 get请求, 默认 post 536 asy: 可选, 是否异步执行, 默认 true 537 success: 可选, 成功回调, 默认 null 538 error: 可选, 超时或失败调用, 默认 null 539 change: 可选, 请求状态改变时调用, 默认 null 540 data: 可选, 如果定义则在初始化时自动执行.send(data)方法 541 } 542 543 demo: 544 const data = `email=${email}&password=${password}`, 545 546 //默认 post 请求: 547 ajax = new Ajax({ 548 url: './login', 549 data: data, 550 success: mes => console.log(mes), 551 }); 552 553 //get 请求: 554 ajax.method = "get"; 555 ajax.send(data); 556 */ 557 class Ajax{ 558 559 constructor(option = {}){ 560 this.url = option.url || ""; 561 this.method = option.method || "post"; 562 this.asy = typeof option.asy === "boolean" ? option.asy : true; 563 this.success = option.success || null; 564 this.error = option.error || null; 565 this.change = option.change || null; 566 567 //init XML 568 this.xhr = new XMLHttpRequest(); 569 570 this.xhr.onerror = this.xhr.ontimeout = option.error || null; 571 572 this.xhr.onreadystatechange = event => { 573 574 if(event.target.readyState === 4 && event.target.status === 200){ 575 576 if(this.success !== null) this.success(event.target.responseText, event); 577 578 } 579 580 else if(this.change !== null) this.change(event); 581 582 } 583 584 if(option.data) this.send(option.data); 585 } 586 587 send(data = ""){ 588 if(this.method === "get"){ 589 this.xhr.open(this.method, this.url+"?"+data, this.asy); 590 this.xhr.send(); 591 } 592 593 else if(this.method === "post"){ 594 this.xhr.open(this.method, this.url, this.asy); 595 this.xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 596 this.xhr.send(data); 597 } 598 } 599 600 } 601 602 603 604 605 /* IndexedDB 本地数据库 606 607 parameter: 608 name: String; //需要打开的数据库名称(如果不存在则会新建一个) 必须 609 done: Function(IndexedDB); //链接数据库成功时的回调 默认 null 610 version: Number; //数据库版本(高版本数据库将覆盖低版本的数据库) 默认 1 611 612 attribute: 613 database: IndexedDB; //链接完成的数据库对象 614 transaction: IDBTransaction; //事务管理(读和写) 615 objectStore: IDBObjectStore; //当前的事务 616 617 method: 618 set(data, key, callback) //添加或更新 619 get(key, callback) //获取 620 delete(key, callback) //删除 621 622 traverse(callback) //遍历 623 getAll(callback) //获取全部 624 clear(callback) //清理所以数据 625 close() //关闭数据库链接 626 627 readOnly: 628 629 static: 630 indexedDB: Object; 631 632 demo: 633 634 new IndexedDB('TEST', db => { 635 636 conosle.log(db); 637 638 }); 639 640 */ 641 class IndexedDB{ 642 643 static indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 644 645 get objectStore(){ //每个事务只能使用一次, 所以每次都需要重新创建 646 return this.database.transaction(this.name, 'readwrite').objectStore(this.name); 647 } 648 649 constructor(name, done = null, version = 1){ 650 651 if(IndexedDB.indexedDB === undefined) return console.error("IndexedDB: 不支持IndexedDB"); 652 653 if(typeof name !== 'string') return console.warn('IndexedDB: 参数错误'); 654 655 this.name = name; 656 this.database = null; 657 658 const request = IndexedDB.indexedDB.open(name, version); 659 660 request.onupgradeneeded = (e)=>{ //数据库不存在 或 版本号不同时 触发 661 if(!this.database) this.database = e.target.result; 662 if(this.database.objectStoreNames.contains(name) === false) this.database.createObjectStore(name); 663 } 664 665 request.onsuccess = (e)=>{ 666 this.database = e.target.result; 667 if(typeof done === 'function') done(this); 668 } 669 670 request.onerror = (e)=>{ 671 console.error(e); 672 } 673 674 } 675 676 close(){ 677 678 return this.database.close(); 679 680 } 681 682 clear(callback){ 683 684 this.objectStore.clear().onsuccess = callback; 685 686 } 687 688 traverse(callback){ 689 690 this.objectStore.openCursor().onsuccess = callback; 691 692 } 693 694 set(data, key = 0, callback){ 695 696 this.objectStore.put(data, key).onsuccess = callback; 697 698 } 699 700 get(key = 0, callback){ 701 702 this.objectStore.get(key).onsuccess = callback; 703 704 } 705 706 del(key = 0, callback){ 707 708 this.objectStore.delete(key).onsuccess = callback; 709 710 } 711 712 getAll(callback){ 713 714 this.objectStore.getAll().onsuccess = callback; 715 716 } 717 718 } 719 720 721 722 723 /* TreeStruct 树结构基类 724 725 attribute: 726 parent: TreeStruct; 727 children: Array[TreeStruct]; 728 729 method: 730 add(v: TreeStruct): v; //v添加到自己的子集 731 remove(v: TreeStruct): v; //删除v, 前提v必须是自己的子集 732 export(): Array[Object]; //TreeStruct 转为 可导出的结构, 包括其所有的后代 733 734 getPath(v: TreeStruct): Array[TreeStruct]; //获取自己到v的路径 735 736 traverse(callback: Function): undefined; //迭代自己的每一个后代, 包括自己 737 callback(value: TreeStruct); //如返回 "continue" 则不在迭代其后代(不是结束迭代, 而是只结束当前节点的后代); 738 739 traverseUp(callback): undefined; //向上遍历每一个父, 包括自己 740 callback(value: TreeStruct); //如返回 "break" 立即停止遍历; 741 742 static: 743 import(arr: Array[Object]): TreeStruct; //.export() 返回的 arr 转为 TreeStruct 744 745 */ 746 class TreeStruct{ 747 748 static import(arr){ 749 750 //json = JSON.parse(json); 751 const len = arr.length; 752 753 for(let k = 0, v; k < len; k++){ 754 v = Object.assign(new TreeStruct(), arr[k]); 755 v.parent = arr[arr[k].parent] || null; 756 if(v.parent !== null) v.parent.add(v); 757 arr[k] = v; 758 } 759 760 return arr[0]; 761 762 } 763 764 constructor(){ 765 this.parent = null; 766 this.children = []; 767 } 768 769 getPath(v){ 770 771 var path; 772 773 const pathA = []; 774 this.traverseUp(tar => { 775 if(v === tar){ 776 path = pathA; 777 return "break"; 778 } 779 pathA.push(tar); 780 }); 781 782 if(path) return path; 783 784 const pathB = []; 785 v.traverseUp(tar => { 786 if(this === tar){ 787 path = pathB.reverse(); 788 return "break"; 789 } 790 else{ 791 let i = pathA.indexOf(tar); 792 if(i !== -1){ 793 pathA.splice(i); 794 pathA.push(tar); 795 path = pathA.concat(pathB.reverse()); 796 return "break"; 797 } 798 } 799 pathB.push(tar); 800 }); 801 802 return path; 803 804 } 805 806 add(v){ 807 v.parent = this; 808 if(this.children.includes(v) === false) this.children.push(v); 809 810 return v; 811 } 812 813 remove(v){ 814 const i = this.children.indexOf(v); 815 if(i !== -1) this.children.splice(i, 1); 816 v.parent = null; 817 818 return v; 819 } 820 821 traverse(callback, key = 0){ 822 823 if(callback(this, key) !== "continue"){ 824 825 for(let k = 0, len = this.children.length; k < len; k++){ 826 827 this.children[k].traverse(callback, k); 828 829 } 830 831 } 832 833 } 834 835 traverseUp(callback){ 836 837 var par = this.parent; 838 839 while(par !== null){ 840 if(callback(par) === "break") return; 841 par = par.parent; 842 } 843 844 } 845 846 export(){ 847 848 const result = [], arr = []; 849 var obj = null; 850 851 this.traverse(v => { 852 obj = Object.assign({}, v); 853 obj.parent = arr.indexOf(v.parent); 854 delete obj.children; 855 result.push(obj); 856 arr.push(v); 857 }); 858 859 return result; //JSON.stringify(result); 860 861 } 862 863 } 864 865 866 867 868 /* Point 869 parameter: 870 x = 0, y = 0; 871 872 attribute 873 x, y: Number; 874 875 method: 876 set(x, y): this; 877 angle(origin): Number; 878 copy(point): this; 879 clone(): Point; 880 distance(point): Number; //获取欧几里得距离 881 distanceMHD(point): Number; //获取曼哈顿距离 882 distanceCompare(point): Number; //获取用于比较的距离(相对于.distance() 效率更高) 883 equals(point): Bool; //是否恒等 884 reverse(): this; //取反值 885 rotate(origin: Object{x,y}, angle): this; //旋转点 886 normalize(): this; //归一 887 */ 888 class Point{ 889 890 constructor(x = 0, y = 0){ 891 this.x = x; 892 this.y = y; 893 } 894 895 set(x = 0, y = 0){ 896 this.x = x; 897 this.y = y; 898 899 return this; 900 } 901 902 angle(origin){ 903 904 return Math.atan2(this.y - origin.y, this.x - origin.x); 905 906 } 907 908 copy(point){ 909 910 this.x = point.x; 911 this.y = point.y; 912 return this; 913 //return Object.assign(this, point); 914 915 } 916 917 clone(){ 918 919 return new this.constructor().copy(this); 920 //return Object.assign(new this.constructor(), this); 921 922 } 923 924 distance(point){ 925 926 return Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2)); 927 928 } 929 930 distanceMHD(point){ 931 932 return Math.abs(this.x - point.x) + Math.abs(this.y - point.y); 933 934 } 935 936 distanceCompare(point){ 937 938 return Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2); 939 940 } 941 942 equals(point){ 943 944 return point.x === this.x && point.y === this.y; 945 946 } 947 948 reverse(){ 949 this.x = -this.x; 950 this.y = -this.y; 951 952 return this; 953 } 954 955 rotate(origin, angle){ 956 const c = Math.cos(angle), s = Math.sin(angle), 957 x = this.x - origin.x, y = this.y - origin.y; 958 959 this.x = x * c - y * s + origin.x; 960 this.y = x * s + y * c + origin.y; 961 962 return this; 963 } 964 965 normalize(){ 966 const len = 1 / (Math.sqrt(this.x * this.x + this.y * this.y) || 1); 967 this.x *= len; 968 this.y *= len; 969 970 return this; 971 } 972 973 /* add(point){ 974 this.x += point.x; 975 this.y += point.y; 976 return this; 977 } 978 979 sub(point){ 980 this.x -= point.x; 981 this.y -= point.y; 982 return this; 983 } 984 985 multiply(point){ 986 this.x *= point.x; 987 this.y *= point.y; 988 return this; 989 } 990 991 divide(point){ 992 this.x /= point.x; 993 this.y /= point.y; 994 return this; 995 } */ 996 997 } 998 999 1000 1001 1002 /* Rotate 1003 parameter: 1004 x, y, a = 0 1005 1006 attribute: 1007 angle: Number; 1008 origin: Point; 1009 1010 method: 1011 set(x, y, a): this; 1012 */ 1013 class Rotate{ 1014 1015 constructor(x, y, a = 0){ 1016 this.angle = a; 1017 this.origin = new Point(x, y); 1018 } 1019 1020 set(x, y, a){ 1021 this.origin.x = x; 1022 this.origin.y = y; 1023 this.angle = a; 1024 return this; 1025 } 1026 1027 pos(x, y){ 1028 this.origin.x = x; 1029 this.origin.y = y; 1030 return this; 1031 } 1032 1033 copy(rotate){ 1034 this.angle = rotate.angle; 1035 this.origin.copy(rotate.origin); 1036 return this; 1037 } 1038 1039 clone(){ 1040 return new this.constructor().copy(this); 1041 } 1042 1043 toAngle(v){ 1044 this.angle = v / 180 * Math.PI; 1045 return this; 1046 } 1047 1048 } 1049 1050 1051 1052 1053 /* Line 1054 parameter: x, y, x1, y1: Number; 1055 attribute: x, y, x1, y1: Number; 1056 method: 1057 set(x, y, x1, y1): this; 1058 copy(line): this; 1059 clone(): Line; 1060 containsPoint(x, y): Bool; //点是否在线上 1061 intersectPoint(line: Line, point: Point): Point; //如果不相交则返回null, 否则返回交点Point 1062 isIntersect(line): Bool; //this与line是否相交 1063 */ 1064 class Line{ 1065 1066 constructor(x = 0, y = 0, x1 = 0, y1 = 0){ 1067 this.x = x; 1068 this.y = y; 1069 this.x1 = x1; 1070 this.y1 = y1; 1071 } 1072 1073 set(x = 0, y = 0, x1 = 0, y1 = 0){ 1074 this.x = x; 1075 this.y = y; 1076 this.x1 = x1; 1077 this.y1 = y1; 1078 return this; 1079 } 1080 1081 copy(line){ 1082 this.x = line.x; 1083 this.y = line.y; 1084 this.x1 = line.x1; 1085 this.y1 = line.y1; 1086 return this; 1087 //return Object.assign(this, line); 1088 } 1089 1090 clone(){ 1091 return new this.constructor().copy(this); 1092 //return Object.assign(new this.constructor(), this); 1093 } 1094 1095 containsPoint(x, y){ 1096 return (x - this.x) * (x - this.x1) <= 0 && (y - this.y) * (y - this.y1) <= 0; 1097 } 1098 1099 intersectPoint(line, point){ 1100 //解线性方程组, 求线段交点 1101 //如果分母为0则平行或共线, 不相交 1102 var denominator = (this.y1 - this.y) * (line.x1 - line.x) - (this.x - this.x1) * (line.y - line.y1); 1103 if(denominator === 0) return null; 1104 1105 //线段所在直线的交点坐标 (x , y) 1106 const x = ((this.x1 - this.x) * (line.x1 - line.x) * (line.y - this.y) 1107 + (this.y1 - this.y) * (line.x1 - line.x) * this.x 1108 - (line.y1 - line.y) * (this.x1 - this.x) * line.x) / denominator; 1109 1110 const y = -((this.y1 - this.y) * (line.y1 - line.y) * (line.x - this.x) 1111 + (this.x1 - this.x) * (line.y1 - line.y) * this.y 1112 - (line.x1 - line.x) * (this.y1 - this.y) * line.y) / denominator; 1113 1114 //判断交点是否在两条线段上 1115 if(this.containsPoint(x, y) && line.containsPoint(x, y)){ 1116 point.x = x; 1117 point.y = y; 1118 return point; 1119 } 1120 1121 return null; 1122 } 1123 1124 isIntersect(line){ 1125 //快速排斥: 1126 //两个线段为对角线组成的矩形,如果这两个矩形没有重叠的部分,那么两条线段是不可能出现重叠的 1127 1128 //这里的确如此,这一步是判定两矩形是否相交 1129 //1.线段ab的低点低于cd的最高点(可能重合) 1130 //2.cd的最左端小于ab的最右端(可能重合) 1131 //3.cd的最低点低于ab的最高点(加上条件1,两线段在竖直方向上重合) 1132 //4.ab的最左端小于cd的最右端(加上条件2,两直线在水平方向上重合) 1133 //综上4个条件,两条线段组成的矩形是重合的 1134 //特别要注意一个矩形含于另一个矩形之内的情况 1135 if(!(Math.min(this.x,this.x1)<=Math.max(line.x,line.x1) && Math.min(line.y,line.y1)<=Math.max(this.y,this.y1) && Math.min(line.x,line.x1)<=Math.max(this.x,this.x1) && Math.min(this.y,this.y1)<=Math.max(line.y,line.y1))) return false; 1136 1137 //跨立实验: 1138 //如果两条线段相交,那么必须跨立,就是以一条线段为标准,另一条线段的两端点一定在这条线段的两段 1139 //也就是说a b两点在线段cd的两端,c d两点在线段ab的两端 1140 var u=(line.x-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y-this.y), 1141 v = (line.x1-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y1-this.y), 1142 w = (this.x-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y-line.y), 1143 z = (this.x1-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y1-line.y); 1144 1145 return u*v <= 0.00000001 && w*z <= 0.00000001; 1146 } 1147 1148 } 1149 1150 1151 1152 1153 /* Box 矩形 1154 parameter: 1155 x = 0, y = 0, w = 0, h = 0; 1156 1157 attribute: 1158 x,y: Number; 位置 1159 w,h: Number; 大小 1160 1161 只读 1162 mx, my: Number; // 1163 1164 method: 1165 set(x, y, w, h): this; 1166 pos(x, y): this; //设置位置 1167 size(w, h): this; //设置大小 1168 setFromRotate(rotate: Rotate): this; //根据 rotate 旋转自己 1169 setFromShapeRect(shapeRect): this; //ShapeRect 转为 Box (忽略 ShapeRect 的旋转和缩放) 1170 setFromCircle(circle, inner: Bool): this; // 1171 setFromPolygon(polygon, inner = true): this;// 1172 toArray(array: Array, index: Integer): this; 1173 copy(box): this; //复制 1174 clone(): Box; //克隆 1175 center(box): this; //设置位置在box居中 1176 distance(x, y): Number; //左上角原点 与 x,y 的直线距离 1177 isEmpty(): Boolean; //.w.h是否小于等于零 1178 maxX(): Number; //返回 max x(this.x+this.w); 1179 maxY(): Number; //返回 max y(this.y+this.h); 1180 expand(box): undefined; //扩容; 把box合并到this 1181 equals(box): Boolean; //this与box是否恒等 1182 intersectsBox(box): Boolean; //box与this是否相交(box在this内部也会返回true) 1183 containsPoint(x, y): Boolean; //x,y点是否在this内 1184 containsBox(box): Boolean; //box是否在this内(只是相交返回fasle) 1185 computeOverflow(b: Box, r: Box): undefined; //this相对b超出的部分赋值到r; r的size小于或等于0的话说明完全超出 1186 */ 1187 class Box{ 1188 1189 get mx(){ 1190 return this.x + this.w; 1191 } 1192 1193 get my(){ 1194 return this.y + this.h; 1195 } 1196 1197 get cx(){ 1198 return this.w / 2 + this.x; 1199 } 1200 1201 get cy(){ 1202 return this.h / 2 + this.y; 1203 } 1204 1205 constructor(x = 0, y = 0, w = 0, h = 0){ 1206 //this.set(x, y, w, h); 1207 this.x = x; 1208 this.y = y; 1209 this.w = w; 1210 this.h = h; 1211 } 1212 1213 set(x, y, w, h){ 1214 this.x = x; 1215 this.y = y; 1216 this.w = w; 1217 this.h = h; 1218 return this; 1219 } 1220 1221 pos(x, y){ 1222 this.x = x; 1223 this.y = y; 1224 return this; 1225 } 1226 1227 size(w, h){ 1228 this.w = w; 1229 this.h = h; 1230 return this; 1231 } 1232 1233 setFromRotate(rotate){ 1234 var minX = this.x, minY = this.y, maxX = 0, maxY = 0; 1235 const point = UTILS.emptyPoint, 1236 run = function (){ 1237 if(point.x < minX) minX = point.x; 1238 else if(point.x > maxX) maxX = point.x; 1239 if(point.y < minY) minY = point.y; 1240 else if(point.y > maxY) maxY = point.y; 1241 } 1242 1243 point.set(this.x, this.y).rotate(rotate.origin, rotate.angle); run(); 1244 point.set(this.mx, this.y).rotate(rotate.origin, rotate.angle); run(); 1245 point.set(this.mx, this.my).rotate(rotate.origin, rotate.angle); run(); 1246 point.set(this.x, this.my).rotate(rotate.origin, rotate.angle); run(); 1247 1248 this.x = minX; 1249 this.y = minY; 1250 this.w = maxX - minX; 1251 this.h = maxY - minY; 1252 1253 return this; 1254 } 1255 1256 setFromShapeRect(shapeRect){ 1257 this.width = shapeRect.width; 1258 this.height = shapeRect.height; 1259 this.x = shapeRect.position.x - this.width / 2; 1260 this.y = shapeRect.position.y - this.height / 2; 1261 return this; 1262 } 1263 1264 setFromCircle(circle, inner = true){ 1265 if(inner === true){ 1266 this.x = Math.sin(-135 / 180 * Math.PI) * circle.r + circle.x; 1267 this.y = Math.cos(-135 / 180 * Math.PI) * circle.r + circle.y; 1268 this.w = this.h = Math.sin(135 / 180 * Math.PI) * circle.r + circle.x - this.x; 1269 } 1270 1271 else{ 1272 this.x = circle.x - circle.r; 1273 this.y = circle.y - circle.r; 1274 this.w = this.h = circle.r * 2; 1275 } 1276 return this; 1277 } 1278 1279 setFromPolygon(polygon, inner = true){ 1280 if(inner === true){ 1281 console.warn('Box: 暂不支持第二个参数为true'); 1282 } 1283 1284 else{ 1285 const len = polygon.path.length; 1286 let x = Infinity, y = Infinity, mx = 0, my = 0; 1287 for(let k = 0, v; k < len; k+=2){ 1288 v = polygon.path[k]; 1289 if(v < x) x = v; 1290 else if(v > mx) mx = v; 1291 1292 v = polygon.path[k+1]; 1293 if(v < y) y = v; 1294 else if(v > my) my = v; 1295 1296 } 1297 1298 this.set(x, y, mx - x, my - y); 1299 1300 } 1301 return this; 1302 } 1303 1304 toArray(array, index){ 1305 array[index] = this.x; 1306 array[index+1] = this.y; 1307 array[index+2] = this.w; 1308 array[index+3] = this.h; 1309 1310 return this; 1311 } 1312 1313 copy(box){ 1314 this.x = box.x; 1315 this.y = box.y; 1316 this.w = box.w; 1317 this.h = box.h; 1318 return this; 1319 //return Object.assign(this, box); //this.set(box.x, box.y, box.w, box.h); 1320 } 1321 1322 clone(){ 1323 return new this.constructor().copy(this); 1324 //return Object.assign(new this.constructor(), this); 1325 } 1326 1327 center(box){ 1328 this.x = (box.w - this.w) / 2 + box.x; 1329 this.y = (box.h - this.h) / 2 + box.y; 1330 return this; 1331 } 1332 1333 distance(x, y){ 1334 return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); 1335 } 1336 1337 isEmpty(){ 1338 return this.w <= 0 || this.h <= 0; 1339 } 1340 1341 maxX(){ 1342 return this.x + this.w; 1343 } 1344 1345 maxY(){ 1346 return this.y + this.h; 1347 } 1348 1349 equals(box){ 1350 return this.x === box.x && this.w === box.w && this.y === box.y && this.h === box.h; 1351 } 1352 1353 expand(box){ 1354 var v = Math.min(this.x, box.x); 1355 this.w = Math.max(this.x + this.w - v, box.x + box.w - v); 1356 this.x = v; 1357 1358 v = Math.min(this.y, box.y); 1359 this.h = Math.max(this.y + this.h - v, box.y + box.h - v); 1360 this.y = v; 1361 } 1362 1363 intersectsBox(box){ 1364 return box.x + box.w < this.x || box.x > this.x + this.w || box.y + box.h < this.y || box.y > this.y + this.h ? false : true; 1365 } 1366 1367 containsPoint(x, y){ 1368 return x < this.x || x > this.x + this.w || y < this.y || y > this.y + this.h ? false : true; 1369 } 1370 1371 containsBox(box){ 1372 return this.x <= box.x && box.x + box.w <= this.x + this.w && this.y <= box.y && box.y + box.h <= this.y + this.h; 1373 } 1374 1375 computeOverflow(p, r){ 1376 r["copy"](this); 1377 1378 if(this["x"] < p["x"]){ 1379 r["x"] = p["x"]; 1380 r["w"] -= p["x"] - this["x"]; 1381 } 1382 1383 if(this["y"] < p["y"]){ 1384 r["y"] = p["y"]; 1385 r["h"] -= p["y"] - this["y"]; 1386 } 1387 1388 var m = p["x"] + p["w"]; 1389 if(r["x"] + r["w"] > m) r["w"] = m - r["x"]; 1390 1391 m = p["y"] + p["h"]; 1392 if(r["y"] + r["h"] > m) r["h"] = m - r["y"]; 1393 } 1394 1395 } 1396 1397 1398 1399 1400 /* Circle 圆形 1401 parameter: 1402 attribute: 1403 x,y: Number; 中心点 1404 r: Number; 半径 1405 1406 //只读 1407 r2: Number; //返回直径 r*2 1408 1409 method: 1410 set(x, y, r): this; 1411 pos(x, y): this; 1412 copy(circle: Circle): this; 1413 clone(): Circle; 1414 distance(x, y): Number; 1415 equals(circle: Circle): Bool; 1416 expand(circle: Circle): undefined; //扩容; 把circle合并到this 1417 containsPoint(x, y): Bool; 1418 intersectsCircle(circle: Circle): Bool; 1419 intersectsBox(box: Box): Bool; 1420 setFromBox(box, inner = true): this; 1421 1422 */ 1423 class Circle{ 1424 1425 get r2(){ 1426 return this.r * 2; 1427 } 1428 1429 constructor(x = 0, y = 0, r = -1){ 1430 //this.set(0, 0, -1); 1431 this.x = x; 1432 this.y = y; 1433 this.r = r; 1434 } 1435 1436 set(x, y, r){ 1437 this.x = x; 1438 this.y = y; 1439 this.r = r; 1440 1441 return this; 1442 } 1443 1444 setFromBox(box, world = true, inner = true){ 1445 this.x = box.w / 2 + (world === true ? box.x : 0); 1446 this.y = box.h / 2 + (world === true ? box.y : 0); 1447 this.r = inner === true ? Math.min(box.w, box.h) / 2 : UTILS.distance(0, 0, box.w, box.h) / 2; 1448 1449 return this; 1450 } 1451 1452 toArray(array, index){ 1453 array[index] = this.x; 1454 array[index+1] = this.y; 1455 array[index+2] = this.r; 1456 1457 return this; 1458 } 1459 1460 pos(x, y){ 1461 this.x = x; 1462 this.y = y; 1463 1464 return this; 1465 } 1466 1467 copy(circle){ 1468 this.r = circle.r; 1469 this.x = circle.x; 1470 this.y = circle.y; 1471 1472 return this; 1473 } 1474 1475 clone(){ 1476 1477 return new this.constructor().copy(this); 1478 1479 } 1480 1481 distance(x, y){ 1482 1483 return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); 1484 1485 } 1486 1487 expand(circle){ 1488 1489 } 1490 1491 equals(circle){ 1492 1493 return circle.x === this.x && circle.y === this.y && circle.r === this.r; 1494 1495 } 1496 1497 containsPoint(x, y){ 1498 1499 return (Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2) <= Math.pow(this.r, 2)); 1500 1501 } 1502 1503 intersectsCircle(circle){ 1504 1505 return (Math.pow(circle.x - this.x, 2) + Math.pow(circle.y - this.y, 2) <= Math.pow(circle.r + this.r, 2)); 1506 1507 } 1508 1509 intersectsBox(box){ 1510 1511 return (Math.pow(Math.max(box.x, Math.min(box.x + box.w, this.x)) - this.x, 2) + Math.pow(Math.max(box.y, Math.min(box.y + box.h, this.y)) - this.y, 2) <= Math.pow(this.r, 2)); 1512 1513 } 1514 1515 } 1516 1517 1518 1519 1520 /* Polygon 多边形 1521 1522 parameter: 1523 path: Array[x, y]; 1524 1525 attribute: 1526 1527 //只读属性 1528 path: Array[x, y]; 1529 1530 method: 1531 add(x, y): this; //x,y添加至path; 1532 containsPoint(x, y): Bool; //x,y是否在多边形的内部(注意: 在路径上也返回 true) 1533 1534 */ 1535 class Polygon{ 1536 1537 #position = null; 1538 #path2D = null; 1539 1540 get path(){ 1541 1542 return this.#position; 1543 1544 } 1545 1546 constructor(path = []){ 1547 this.#position = path; 1548 1549 this.#path2D = new Path2D(); 1550 1551 var len = path.length; 1552 if(len >= 2){ 1553 if(len % 2 !== 0){ 1554 len -= 1; 1555 path.splice(len, 1); 1556 } 1557 1558 const con = this.#path2D; 1559 con.moveTo(path[0], path[1]); 1560 for(let k = 2; k < len; k+=2) con.lineTo(path[k], path[k+1]); 1561 1562 } 1563 1564 } 1565 1566 add(x, y){ 1567 this.#position.push(x, y); 1568 this.#path2D.lineTo(x, y); 1569 return this; 1570 } 1571 1572 containsPoint(x, y){ 1573 1574 return UTILS.emptyContext.isPointInPath(this.#path2D, x, y); 1575 1576 } 1577 1578 isInPolygon(checkPoint, polygonPoints) { 1579 var counter = 0; 1580 var i; 1581 var xinters; 1582 var p1, p2; 1583 var pointCount = polygonPoints.length; 1584 p1 = polygonPoints[0]; 1585 for (i = 1; i <= pointCount; i++) { 1586 p2 = polygonPoints[i % pointCount]; 1587 if ( 1588 checkPoint[0] > Math.min(p1[0], p2[0]) && 1589 checkPoint[0] <= Math.max(p1[0], p2[0]) 1590 ) { 1591 if (checkPoint[1] <= Math.max(p1[1], p2[1])) { 1592 if (p1[0] != p2[0]) { 1593 xinters = 1594 (checkPoint[0] - p1[0]) * 1595 (p2[1] - p1[1]) / 1596 (p2[0] - p1[0]) + 1597 p1[1]; 1598 if (p1[1] == p2[1] || checkPoint[1] <= xinters) { 1599 counter++; 1600 } 1601 } 1602 } 1603 } 1604 p1 = p2; 1605 } 1606 if (counter % 2 == 0) { 1607 return false; 1608 } else { 1609 return true; 1610 } 1611 } 1612 1613 containsPolygon(polygon){ 1614 const path = polygon.path, len = path.length; 1615 for(let k = 0; k < len; k += 2){ 1616 if(this.containsPoint(path[k], path[k+1]) === false) return false; 1617 } 1618 1619 return true; 1620 } 1621 1622 toPoints(){ 1623 const path = this.path, len = path.length, result = []; 1624 1625 for(let k = 0; k < len; k += 2) result.push(new Point(path[k], path[k+1])); 1626 1627 return result; 1628 } 1629 1630 toLines(){ 1631 const path = this.path, len = path.length, result = []; 1632 1633 for(let k = 0, x = NaN, y; k < len; k += 2){ 1634 1635 if(isNaN(x)){ 1636 x = path[k]; 1637 y = path[k+1]; 1638 continue; 1639 } 1640 1641 const line = new Line(x, y, path[k], path[k+1]); 1642 1643 x = line.x1; 1644 y = line.y1; 1645 1646 result.push(line); 1647 1648 } 1649 1650 return result; 1651 } 1652 1653 merge(polygon){ 1654 1655 const linesA = this.toLines(), linesB = polygon.toLines(), nodes = [], newLines = [], 1656 1657 pointA = new Point(), pointB = new Point(), 1658 1659 forEachNodes = (pathA, pathB, funcA = null, funcB = null) => { 1660 for(let k = 0, lenA = pathA.length, lenB = pathB.length; k < lenA; k++){ 1661 if(funcA !== null) funcA(pathA[k]); 1662 1663 for(let i = 0; i < lenB; i++){ 1664 if(funcB !== null) funcB(pathB[i], pathA[k]); 1665 } 1666 1667 } 1668 } 1669 1670 if(this.containsPolygon(polygon)){console.log('this -> polygon'); 1671 forEachNodes(linesA, linesB, lineA => newLines.push(lineA), (lineB, lineA) => { 1672 if(lineA.intersectPoint(lineB, pointA) === pointA) newLines.push(pointA.clone()); 1673 }); 1674 1675 return newLines; 1676 } 1677 1678 //收集所有的交点 (保存至 line) 1679 forEachNodes(linesA, linesB, lineA => lineA.nodes = [], (lineB, lineA) => { 1680 if(lineB.nodes === undefined) lineB.nodes = []; 1681 if(lineA.intersectPoint(lineB, pointA) === pointA){ 1682 const node = { 1683 lineA: lineA, 1684 lineB: lineB, 1685 point: pointA.clone(), 1686 disA: pointA.distanceCompare(pointB.set(lineA.x, lineA.y)), 1687 disB: pointA.distanceCompare(pointB.set(lineB.x, lineB.y)), 1688 } 1689 lineA.nodes.push(node); 1690 lineB.nodes.push(node); 1691 nodes.push(node); 1692 } 1693 }); 1694 1695 //交点以原点为目标排序 1696 for(let k = 0, sotr = function (a,b){return a.disA - b.disA;}, countA = linesA.length; k < countA; k++) linesA[k].nodes.sort(sotr); 1697 for(let k = 0, sotr = function (a,b){return a.disB - b.disB;}, countB = linesB.length; k < countB; k++) linesB[k].nodes.sort(sotr); 1698 1699 var _loopTypeA, _loopTypeB; 1700 const result_loop = { 1701 lines: null, 1702 loopType: '', 1703 line: null, 1704 count: 0, 1705 indexed: 0, 1706 }, 1707 1708 //遍历某条线 1709 loop = (lines, index, loopType) => { 1710 const length = lines.length, indexed = lines.length, model = lines === linesA ? polygon : this; 1711 1712 var line, i = 1; 1713 while(true){ 1714 if(loopType === 'next') index = index === length - 1 ? 0 : index + 1; 1715 else if(loopType === 'back') index = index === 0 ? length - 1 : index - 1; 1716 line = lines[index]; 1717 1718 result_loop.count = line.nodes.length; 1719 if(result_loop.count !== 0){ 1720 result_loop.lines = lines; 1721 result_loop.loopType = loopType; 1722 result_loop.line = line; 1723 result_loop.indexed = index; 1724 if(loopType === 'next') addLine(line, model); 1725 1726 return result_loop; 1727 } 1728 1729 addLine(line, model); 1730 if(indexed === i++) break; 1731 1732 } 1733 1734 }, 1735 1736 //更新或创建交点的索引 1737 setNodeIndex = (lines, index, loopType) => { 1738 const line = lines[index], count = line.nodes.length; 1739 if(loopType === undefined) loopType = lines === linesA ? _loopTypeA : _loopTypeB; 1740 1741 if(loopType === undefined) return; 1742 1743 if(line.nodeIndex === undefined){ 1744 line.nodeIndex = loopType === 'next' ? 0 : count - 1; 1745 line.nodeState = count === 1 ? 'end' : 'start'; 1746 1747 } 1748 1749 else{ 1750 if(line.nodeState === 'end' || line.nodeState === ''){ 1751 line.nodeState = ''; 1752 return; 1753 } 1754 1755 if(loopType === 'next'){ 1756 line.nodeIndex += 1; 1757 1758 if(line.nodeIndex === count - 1) line.nodeState = 'end'; 1759 else line.nodeState = 'run'; 1760 } 1761 1762 else if(loopType === 'back'){ 1763 line.nodeIndex -= 1; 1764 1765 if(line.nodeIndex === 0) line.nodeState = 'end'; 1766 else line.nodeState = 'run'; 1767 } 1768 1769 } 1770 1771 }, 1772 1773 //只有在跳线的时候才执行此方法, 如果跳线的话: 某条线上的交点必然在两端; 1774 getLoopType = (lines, index, nodePoint) => { 1775 const line = lines[index], lineNext = lines[index === lines.length - 1 ? 0 : index + 1], 1776 1777 model = lines === linesA ? polygon : this, 1778 isLineBack = newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false, 1779 isLineNext = newLines.includes(lineNext) === false && model.containsPoint(lineNext.x, lineNext.y) === false; 1780 1781 if(isLineBack && isLineNext){ 1782 const len = line.nodes.length; 1783 if(len >= 2){ 1784 if(line.nodes[len - 1].point.equals(nodePoint)) return 'next'; 1785 else if(line.nodes[0].point.equals(nodePoint)) return 'back'; 1786 } 1787 1788 else console.warn('路径复杂', line); 1789 1790 } 1791 1792 else if(isLineNext){ 1793 return 'next'; 1794 } 1795 1796 else if(isLineBack){ 1797 return 'back'; 1798 } 1799 1800 return ''; 1801 }, 1802 1803 //添加线至新的形状数组 1804 addLine = (line, model) => { 1805 //if(newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); 1806 if(line.nodes.length === 0) newLines.push(line); 1807 else if(model.containsPoint(line.x, line.y) === false) newLines.push(line); 1808 1809 }, 1810 1811 //处理拥有交点的线 1812 computeNodes = v => { 1813 if(v === undefined || v.count === 0) return; 1814 1815 setNodeIndex(v.lines, v.indexed, v.loopType); 1816 1817 //添加交点 1818 const node = v.line.nodes[v.line.nodeIndex]; 1819 if(newLines.includes(node.point) === false) newLines.push(node.point); 1820 else return; 1821 1822 var lines = v.lines === linesA ? linesB : linesA, 1823 line = lines === linesA ? node.lineA : node.lineB, 1824 index = lines.indexOf(line); 1825 1826 setNodeIndex(lines, index); 1827 1828 //选择交点状态 1829 var nodeState = line.nodeState !== undefined ? line.nodeState : v.line.nodeState; 1830 if((v.count <= 2 && line.nodes.length > 2) || (v.count > 2 && line.nodes.length <= 2)){ 1831 if(line.nodeState === 'start'){ 1832 const backLine = v.loopType === 'next' ? v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1] : v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1]; 1833 1834 if(newLines.includes(backLine) && backLine.nodes.length === 0){ 1835 nodeState = 'run'; 1836 } 1837 1838 } 1839 else if(line.nodeState === 'end'){ 1840 const nextLine = v.loopType === 'next' ? v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1] : v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1]; 1841 const model = v.lines === linesA ? polygon : this; 1842 if(model.containsPoint(nextLine.x, nextLine.y) === false && nextLine.nodes.length === 0){ 1843 nodeState = 'run'; 1844 } 1845 1846 } 1847 } 1848 1849 switch(nodeState){ 1850 1851 //不跳线 1852 case 'run': 1853 if(v.loopType === 'back') addLine(v.line, v.lines === linesA ? polygon : this); 1854 return computeNodes(loop(v.lines, v.indexed, v.loopType)); 1855 1856 //跳线 1857 case 'start': 1858 case 'end': 1859 const loopType = getLoopType(lines, index, node.point); 1860 if(loopType !== ''){ 1861 if(lines === linesA) _loopTypeA = loopType; 1862 else _loopTypeB = loopType; 1863 if(loopType === 'back') addLine(line, v.lines === linesA ? this : polygon); 1864 return computeNodes(loop(lines, index, loopType)); 1865 } 1866 break; 1867 1868 } 1869 1870 } 1871 1872 //获取介入点 1873 var startLine = null; 1874 for(let k = 0, len = nodes.length, node; k < len; k++){ 1875 node = nodes[k]; 1876 if(node.lineA.nodes.length !== 0){ 1877 startLine = node.lineA; 1878 if(node.lineA.nodes.length === 1 && node.lineB.nodes.length > 1){ 1879 startLine = node.lineB.nodes[0].lineB; 1880 result_loop.lines = linesB; 1881 result_loop.loopType = _loopTypeB = 'next'; 1882 } 1883 else{ 1884 result_loop.lines = linesA; 1885 result_loop.loopType = _loopTypeA = 'next'; 1886 } 1887 result_loop.line = startLine; 1888 result_loop.count = startLine.nodes.length; 1889 result_loop.indexed = result_loop.lines.indexOf(startLine); 1890 break; 1891 } 1892 } 1893 1894 if(startLine === null){ 1895 console.warn('Polygon: 找不到介入点, 终止了合并'); 1896 return newLines; 1897 } 1898 1899 computeNodes(result_loop); 1900 1901 return newLines; 1902 } 1903 1904 } 1905 1906 1907 1908 1909 /* ShapeRect (一般2d矩形的原点在左上, 此矩形类的原点在中间) 1910 parameter: 1911 width = 0, height = 0 1912 1913 attribute: 1914 width, height: Number; //矩形的宽高 1915 position: Point; //位置(是旋转和缩放的中心点) 1916 rotation: Number; //旋转(绕 Z 轴旋转的弧度) 1917 scale: Point; //缩放 1918 1919 method: 1920 setFromBox(box): this; //Box 转为 ShapeRect 1921 compute(): undefined; //计算出矩形 1922 applyCanvas(context): undefined; //矩形应用到画布的上下文中 1923 1924 demo: 1925 const canvasRect = new ShapeRect(100, 150); console.log(canvasRect); 1926 1927 const canvas = document.createElement("canvas"); 1928 canvas.width = WORLD.width; 1929 canvas.height = WORLD.height; 1930 canvas.style = ` 1931 position: absolute; 1932 z-index: 9999; 1933 background: rgb(127,127,127); 1934 `; 1935 document.body.appendChild(canvas); 1936 1937 canvasRect.position.set(300, 300); 1938 canvasRect.rotation = UTILS.toAngle(45); 1939 canvasRect.scale.set(1.5, 1.5); 1940 canvasRect.compute(); 1941 1942 const context = canvas.getContext("2d"); 1943 canvasRect.applyCanvas(context); 1944 1945 context.strokeStyle = "red"; 1946 context.stroke(); 1947 */ 1948 class ShapeRect{ 1949 1950 #dots = []; 1951 #size = new Point(); 1952 #min = new Point(); 1953 #max = new Point(); 1954 1955 constructor(width = 0, height = 0){ 1956 this.width = width; 1957 this.height = height; 1958 this.position = new Point(); 1959 this.rotation = 0; 1960 this.scale = new Point(1, 1); 1961 } 1962 1963 setFromBox(box){ 1964 this.width = box.w; 1965 this.height = box.h; 1966 this.position.set(box.cx, box.cy); 1967 return this; 1968 } 1969 1970 compute(){ 1971 //scale 1972 this.#size.set(this.width * this.scale.x, this.height * this.scale.y); 1973 1974 //position 1975 this.#min.set(this.position.x - this.#size.x / 2, this.position.y - this.#size.y / 2); 1976 this.#max.set(this.#min.x + this.#size.x, this.#min.y + this.#size.y); 1977 1978 //rotation 1979 const point = UTILS.emptyPoint; 1980 this.#dots.length = 0; 1981 1982 point.set(this.#min.x, this.#min.y).rotate(this.position, this.rotation); 1983 this.#dots.push(point.x, point.y); 1984 1985 point.set(this.#max.x, this.#min.y).rotate(this.position, this.rotation); 1986 this.#dots.push(point.x, point.y); 1987 1988 point.set(this.#max.x, this.#max.y).rotate(this.position, this.rotation); 1989 this.#dots.push(point.x, point.y); 1990 1991 point.set(this.#min.x, this.#max.y).rotate(this.position, this.rotation); 1992 this.#dots.push(point.x, point.y); 1993 1994 //缩放旋转后的 min 与 max 1995 //this.#min.set(Math.min(this.#dots[0], this.#dots[2], this.#dots[4], this.#dots[6]), Math.min(this.#dots[1], this.#dots[3], this.#dots[5], this.#dots[7])); 1996 //this.#max.set(Math.max(this.#dots[0], this.#dots[2], this.#dots[4], this.#dots[6]), Math.max(this.#dots[1], this.#dots[3], this.#dots[5], this.#dots[7])) 1997 } 1998 1999 drawImage(img, context){ 2000 UTILS.emptyContext.canvas.width = this.#size.x; 2001 UTILS.emptyContext.canvas.height = this.#size.y; 2002 UTILS.emptyContext.drawImage(img, 0, 0, this.#size.x, this.#size.y); 2003 2004 const imageData = UTILS.emptyContext.getImageData(0, 0, this.#size.x, this.#size.y), 2005 width = imageData.width, height = imageData.height, 2006 len = width * height, point = UTILS.emptyPoint, center = UTILS.emptyPointA.set(width/2, height/2), 2007 2008 newImageData = UTILS.emptyContext.createImageData(width, height); 2009 2010 for(let k = 0; k < len; k++){ 2011 point.set(k % width, Math.floor(k / width)).rotate(center, this.rotation); 2012 if(point.x < 0 || point.y < 0 || point.x > newImageData.width-1 || point.y > newImageData.height-1) continue; 2013 2014 point.set((width * Math.round(point.y) + Math.round(point.x)) * 4, k * 4); 2015 newImageData.data[point.x] = imageData.data[point.y]; 2016 newImageData.data[point.x + 1] = imageData.data[point.y + 1]; 2017 newImageData.data[point.x + 2] = imageData.data[point.y + 2]; 2018 newImageData.data[point.x + 3] = imageData.data[point.y + 3]; 2019 } 2020 2021 context.putImageData(newImageData, 20, 20); 2022 } 2023 2024 applyCanvas(context){ 2025 context.beginPath(); 2026 context.moveTo(this.#dots[0], this.#dots[1]); 2027 2028 for(let k = 2, len = this.#dots.length; k < len; k += 2){ 2029 context.lineTo(this.#dots[k], this.#dots[k + 1]); 2030 } 2031 2032 context.closePath(); 2033 } 2034 2035 } 2036 2037 2038 2039 2040 /* RGBColor 2041 parameter: 2042 r, g, b 2043 2044 method: 2045 set(r, g, b: Number): this; //rgb: 0 - 255; 第一个参数可以为 css color 2046 setFormHex(hex: Number): this; // 2047 setFormHSV(h, s, v: Number): this; //h:0-360; s,v:0-100; 颜色, 明度, 暗度 2048 setFormString(str: String): Number; //str: 英文|css color; 返回的是透明度 (如果为 rgba 则返回a; 否则总是返回1) 2049 2050 copy(v: RGBColor): this; 2051 clone(): RGBColor; 2052 2053 getHex(): Number; 2054 getHexString(): String; 2055 getHSV(result: Object{h, s, v}): result; //result: 默认是一个新的Object 2056 getRGBA(alpha: Number): String; //alpha: 0 - 1; 默认 1 2057 getStyle() //.getRGBA()别名 2058 2059 stringToColor(str: String): String; //str 转为 css color; 如果str不是color格式则返回 "" 2060 2061 */ 2062 class RGBColor{ 2063 2064 constructor(r = 255, g = 255, b = 255){ 2065 this.r = r; 2066 this.g = g; 2067 this.b = b; 2068 } 2069 2070 copy(v){ 2071 this.r = v.r; 2072 this.g = v.g; 2073 this.b = v.b; 2074 return this; 2075 } 2076 2077 clone(){ 2078 return new this.constructor().copy(this); 2079 } 2080 2081 set(r, g, b){ 2082 if(typeof r !== "string"){ 2083 this.r = r || 255; 2084 this.g = g || 255; 2085 this.b = b || 255; 2086 } 2087 2088 else this.setFormString(r); 2089 2090 return this; 2091 } 2092 2093 setFormHex(hex){ 2094 hex = Math.floor( hex ); 2095 2096 this.r = hex >> 16 & 255; 2097 this.g = hex >> 8 & 255; 2098 this.b = hex & 255; 2099 return this; 2100 } 2101 2102 setFormHSV(h, s, v){ 2103 h = h >= 360 ? 0 : h; 2104 var s=s/100; 2105 var v=v/100; 2106 var h1=Math.floor(h/60) % 6; 2107 var f=h/60-h1; 2108 var p=v*(1-s); 2109 var q=v*(1-f*s); 2110 var t=v*(1-(1-f)*s); 2111 var r,g,b; 2112 switch(h1){ 2113 case 0: 2114 r=v; 2115 g=t; 2116 b=p; 2117 break; 2118 case 1: 2119 r=q; 2120 g=v; 2121 b=p; 2122 break; 2123 case 2: 2124 r=p; 2125 g=v; 2126 b=t; 2127 break; 2128 case 3: 2129 r=p; 2130 g=q; 2131 b=v; 2132 break; 2133 case 4: 2134 r=t; 2135 g=p; 2136 b=v; 2137 break; 2138 case 5: 2139 r=v; 2140 g=p; 2141 b=q; 2142 break; 2143 } 2144 2145 this.r = Math.round(r*255); 2146 this.g = Math.round(g*255); 2147 this.b = Math.round(b*255); 2148 return this; 2149 } 2150 2151 setFormString(color){ 2152 if(typeof color !== "string") return 1; 2153 var _color = this.stringToColor(color); 2154 2155 if(_color[0] === "#"){ 2156 const len = _color.length; 2157 if(len === 4){ 2158 _color = _color.slice(1); 2159 this.setFormHex(parseInt("0x"+_color + "" + _color)); 2160 } 2161 else if(len === 7) this.setFormHex(parseInt("0x"+_color.slice(1))); 2162 } 2163 2164 else if(_color[0] === "r" && _color[1] === "g" && _color[2] === "b"){ 2165 const arr = []; 2166 for(let k = 0, len = _color.length, v = "", is = false; k < len; k++){ 2167 2168 if(is === true){ 2169 if(_color[k] === "," || _color[k] === ")"){ 2170 arr.push(parseFloat(v)); 2171 v = ""; 2172 } 2173 else v += _color[k]; 2174 2175 } 2176 2177 else if(_color[k] === "(") is = true; 2178 2179 } 2180 2181 this.set(arr[0], arr[1], arr[2]); 2182 return arr[3] === undefined ? 1 : arr[3]; 2183 } 2184 2185 return 1; 2186 } 2187 2188 getHex(){ 2189 2190 return Math.max( 0, Math.min( 255, this.r ) ) << 16 ^ Math.max( 0, Math.min( 255, this.g ) ) << 8 ^ Math.max( 0, Math.min( 255, this.b ) ) << 0; 2191 2192 } 2193 2194 getHexString(){ 2195 2196 return '#' + ( '000000' + this.getHex().toString( 16 ) ).slice( - 6 ); 2197 2198 } 2199 2200 getHSV(result){ 2201 result = result || {} 2202 var r=this.r/255; 2203 var g=this.g/255; 2204 var b=this.b/255; 2205 var h,s,v; 2206 var min=Math.min(r,g,b); 2207 var max=v=Math.max(r,g,b); 2208 var l=(min+max)/2; 2209 var difference = max-min; 2210 2211 if(max==min){ 2212 h=0; 2213 }else{ 2214 switch(max){ 2215 case r: h=(g-b)/difference+(g < b ? 6 : 0);break; 2216 case g: h=2.0+(b-r)/difference;break; 2217 case b: h=4.0+(r-g)/difference;break; 2218 } 2219 h=Math.round(h*60); 2220 } 2221 if(max==0){ 2222 s=0; 2223 }else{ 2224 s=1-min/max; 2225 } 2226 s=Math.round(s*100); 2227 v=Math.round(v*100); 2228 result.h = h; 2229 result.s = s; 2230 result.v = v; 2231 return result; 2232 } 2233 2234 getStyle(){ 2235 return this.getRGBA(1); 2236 } 2237 2238 getRGBA(alpha = 1){ 2239 return `rgba(${this.r},${this.g},${this.b},${alpha})`; 2240 } 2241 2242 stringToColor(str){ 2243 var _color = ""; 2244 for(let k = 0, len = str.length; k < len; k++){ 2245 if(str[k] === " ") continue; 2246 _color += str[k]; 2247 } 2248 2249 if(_color[0] === "#" || (_color[0] === "r" && _color[1] === "g" && _color[2] === "b")) return _color; 2250 2251 return ColorRefTable[_color] || ""; 2252 } 2253 2254 } 2255 2256 2257 2258 2259 /* Timer 定时器 2260 2261 parameter: 2262 func: Function; //定时器运行时的回调; 默认 null, 如果定义构造器将自动调用一次.restart()方法 2263 speed: Number; //延迟多少毫秒执行一次 func; 默认 3000; 2264 step: Integer; //执行多少次: 延迟speed毫秒执行一次 func; 默认 Infinity; 2265 2266 attribute: 2267 func, speed, step; //这些属性可以随时更改; 2268 2269 //只读属性 2270 readyState: String; //定时器状态; 可能值: '', 'start', 'running', 'done'; ''表示定时器从未启动 2271 number: Number; //运行的次数 2272 2273 method: 2274 start(func, speed): this; //启动定时器 (如果定时器正在运行则什么都不会做) 2275 restart(): undefined; //重启定时器 2276 stop(): undefined; //停止定时器 2277 2278 demo: 2279 //每 3000 毫秒 打印一次 timer.number 2280 new Timer(timer => { 2281 console.log(timer.number); 2282 }, 3000); 2283 2284 */ 2285 class Timer{ 2286 2287 #restart = -1; 2288 #speed = 0; 2289 #isRun = false; 2290 #i = 0; 2291 #readyState = ''; //start|running 2292 2293 get number(){ 2294 return this.#i; 2295 } 2296 2297 get readyState(){ 2298 return this.#i >= this.step ? 'done' : this.#readyState; 2299 } 2300 2301 get running(){ 2302 return this.#isRun; 2303 } 2304 2305 constructor(func = null, speed = 3000, step = Infinity){ 2306 this.func = func; 2307 this.speed = speed; 2308 this.step = step; 2309 //this.onDone = null; 2310 2311 if(typeof this.func === "function") this.restart(); 2312 2313 } 2314 2315 start(func, time){ 2316 if(typeof func === 'function') this.func = func; 2317 if(UTILS.isNumber(time) === true) this.speed = time; 2318 this.restart(); 2319 2320 return this; 2321 } 2322 2323 restart(){ 2324 if(this.#isRun === false){ 2325 setTimeout(this._loop, this.speed); 2326 this.#isRun = true; 2327 this.#restart = -1; 2328 this.#i = 0; 2329 this.#readyState = 'start'; 2330 2331 } 2332 2333 else{ 2334 this.#restart = Date.now(); 2335 this.#speed = this.speed; 2336 2337 } 2338 2339 } 2340 2341 stop(){ 2342 if(this.#isRun === true){ 2343 this.#restart = -1; 2344 this.#i = this.step; 2345 } 2346 2347 } 2348 2349 _loop = () => { 2350 2351 //重启计时器 2352 if(this.#restart !== -1){ 2353 2354 let gone = Date.now() - this.#restart; 2355 this.#restart = -1; 2356 2357 if(gone >= this.#speed) gone = this.speed; 2358 else{ 2359 if(this.#speed === this.speed) gone = this.#speed - gone; 2360 else gone = (this.#speed - gone) / this.#speed * this.speed; 2361 } 2362 2363 setTimeout(this._loop, gone); 2364 2365 this.#i = 1; 2366 if(this.func !== null) this.func(this); 2367 2368 } 2369 2370 //正在运行 2371 else if(this.#i < this.step){ 2372 2373 setTimeout(this._loop, this.speed); 2374 2375 this.#i++; 2376 if(this.#readyState !== 'running') this.#readyState = 'running'; 2377 if(this.func !== null) this.func(this); 2378 2379 } 2380 2381 //完成 2382 else this.#isRun = false; 2383 2384 } 2385 2386 } 2387 2388 2389 2390 2391 /* SeekPath A*寻路 2392 2393 parameter: 2394 option: Object{ 2395 angle: Number, //8 || 16 2396 timeout: Number, //单位为毫秒 2397 size: Number, //每格的宽高 2398 lenX, lenY: Number, //长度 2399 disables: Array[0||1], 2400 heights: Array[Number], 2401 path: Array[], //存放寻路结果 默认创建一个空数组 2402 } 2403 2404 attribute: 2405 size: Number; //每个索引的大小 2406 lenX: Number; //最大长度x (设置此属性时, 你需要重新.initMap(heights);) 2407 lenY: Number; //最大长度y (设置此属性时, 你需要重新.initMap(heights);) 2408 2409 //此属性已废弃 range: Box; //本次的搜索范围, 默认: 0,0,lenX,lenY 2410 angle: Number; //8四方向 或 16八方向 默认 16 2411 timeout: Number; //超时毫秒 默认 500 2412 mapRange: Box; //地图box 2413 //此属性已废弃(run函数不在检测相邻的高) maxHeight: Number; //相邻可走的最大高 默认 6 2414 2415 //只读属性 2416 success: Bool; //只读; 本次搜索是否成功找到终点; (如果为false说明.run()返回的是 距终点最近的路径; 超时也会被判定为false) 2417 path: Array[x, y, z]; //存放.run()返回的路径 2418 map: Map; //地图的缓存数据 2419 2420 method: 2421 initMap(heights: Array[Number]): undefiend; //初始化类时自动调用一次; heights:如果你的场景存在高请定义此参数 2422 run(x, y, x1, y1: Number): Array[x, y, z]; //参数索引坐标 2423 getDots(x, y, a, r): Array[ix, iy]; //获取周围的点 x,y, a:8|16, r:存放结果数组 2424 getLegalPoints(ix, iy, count): Array[x, y, z]; //获取 ix, iy 周围 合法的, 相邻的 count 个点 2425 2426 demo: 2427 const sp = new SeekPath({ 2428 angle: 16, 2429 timeout: 500, 2430 //maxHeight: 6, 2431 size: 10, 2432 lenX: 1000, 2433 lenY: 1000, 2434 }), 2435 2436 path = sp.run(0, 0, 1000, 1000); 2437 2438 console.log(sp); 2439 2440 */ 2441 class SeekPath{ 2442 2443 static _open = [] 2444 static _dots = [] //.run() .getLegalPoints() 2445 static dots4 = []; //._check() 2446 static _sort = function (a, b){return a["f"] - b["f"];} 2447 2448 #map = null; 2449 #path = null; 2450 #success = true; 2451 #halfX = 50; 2452 #halfY = 50; 2453 2454 #size = 10; 2455 #lenX = 10; 2456 #lenY = 10; 2457 2458 constructor(option = {}){ 2459 this.angle = (option.angle === 8 || option.angle === 16) ? option.angle : 16; //8四方向 或 16八方向 2460 this.timeout = option.timeout || 500; //超时毫秒 2461 //this.maxHeight = option.maxHeight || 6; 2462 this.mapRange = new Box(); 2463 this.size = option.size || 10; 2464 this.lenX = option.lenX || 10; 2465 this.lenY = option.lenY || 10; 2466 this.#path = Array.isArray(option.path) ? option.path : []; 2467 this.initMap(option.disable, option.height); 2468 option = undefined 2469 } 2470 2471 //this.#map[x][y] = Object{height: 此点的高,默认0, is: 此点是否可走,默认1(disable)}; 2472 get map(){ 2473 return this.#map; 2474 } 2475 2476 //this.#path = Array[x,y,z] 2477 get path(){ 2478 return this.#path; 2479 } 2480 2481 get success(){ 2482 return this.#success; 2483 } 2484 2485 get size(){ 2486 return this.#size; 2487 } 2488 2489 set size(v){ 2490 this.#size = v; 2491 v = v / 2; 2492 this.#halfX = v * this.#lenX; 2493 this.#halfY = v * this.#lenY; 2494 } 2495 2496 get lenX(){ 2497 return this.#lenX; 2498 } 2499 2500 set lenX(v){ 2501 this.#lenX = v; 2502 v = this.#size / 2; 2503 this.#halfX = v * this.#lenX; 2504 this.#halfY = v * this.#lenY; 2505 2506 } 2507 2508 get lenY(){ 2509 return this.#lenY; 2510 } 2511 2512 set lenY(v){ 2513 this.#lenY = v; 2514 v = this.#size / 2; 2515 this.#halfX = v * this.#lenX; 2516 this.#halfY = v * this.#lenY; 2517 2518 } 2519 2520 toScene(n, v){ //n = "x|y" 2521 //n = n === "y" ? "lenY" : "lenX"; 2522 if(n === "y" || n === "z") return v * this.#size - this.#halfY; 2523 return v * this.#size - this.#halfX; 2524 } 2525 2526 toIndex(n, v){ 2527 //n = n === "y" ? "lenY" : "lenX"; 2528 if(n === "y" || n === "z") return Math.round((this.#halfY + v) / this.#size); 2529 return Math.round((this.#halfX + v) / this.#size); 2530 } 2531 2532 initMap(disable, height){ 2533 2534 disable = Array.isArray(disable) === true ? disable : null; 2535 height = Array.isArray(height) === true ? height : null; 2536 2537 const lenX = this.lenX, lenY = this.lenY; 2538 var getHeight = (ix, iy) => { 2539 if(height === null) return 0; 2540 ix = height[ix * lenY + iy]; 2541 if(ix === undefined) return 0; 2542 return ix; 2543 }, 2544 getDisable = (ix, iy) => { 2545 if(disable === null) return 1; 2546 ix = disable[ix * lenY + iy]; 2547 if(ix === undefined) return 0; 2548 return ix; 2549 }, 2550 2551 map = []//new Map(); 2552 2553 for(let x = 0, y, m; x < lenX; x++){ 2554 m = []//new Map(); 2555 for(y = 0; y < lenY; y++) m[y] = {x:x, y:y, height:getHeight(x, y), is:getDisable(x, y), g:0, h:0, f:0, p:null, id:""}//m.set(y, {x:x, y:y, height:getHeight(x, y), g:0, h:0, f:0, p:null, id:""}); 2556 map[x] = m;//map.set(x, m); 2557 } 2558 2559 this.#map = map; 2560 this._id = -1; 2561 this._updateID(); 2562 this.mapRange.set(0, 0, this.#lenX-1, this.#lenY-1); 2563 2564 map = disable = height = getHeight = undefined; 2565 2566 } 2567 2568 getLegalPoints(ix, iy, count, result = []){ //不包括 ix, iy 2569 const _dots = SeekPath._dots; 2570 result.length = 0; 2571 result[0] = this.#map[ix][iy]; 2572 count += 1; 2573 2574 while(result.length < count){ 2575 for(let k = 0, i, n, d, len = result.length; k < len; k++){ 2576 n = result[k]; 2577 this.getDots(n.x, n.y, this.angle, _dots); 2578 for(i = 0; i < this.angle; i += 2){ 2579 d = this.#map[_dots[i]][_dots[i+1]]; 2580 if(d.is === 1 && this.mapRange.containsPoint(d.x, d.y) && !result.includes(d)){ 2581 if(Math.abs(n.x - d.x) + Math.abs(n.y - d.y) === 2 && this._check(n, d)){ 2582 result.push(d); 2583 } 2584 } 2585 } 2586 } 2587 } 2588 2589 result.splice(0, 1); 2590 return result; 2591 } 2592 2593 getLinePoints(now, next, count, result = []){ //不包括 now 2594 if(count % 2 !== 0) count += 1; 2595 2596 const len = count / 2, angle90 = 90/180*Math.PI; 2597 2598 var i, ix, iy, n, nn = next, is = false; 2599 2600 UTILS.emptyPoint.set(now.x, now.y).rotate(next, angle90); 2601 var disX = UTILS.emptyPoint.x - next.x, 2602 disY = UTILS.emptyPoint.y - next.y; 2603 2604 for(i = 0; i < len; i++){ 2605 if(is){ 2606 result[len-1-i] = nn; 2607 continue; 2608 } 2609 ix = disX + disX * i + next.x; 2610 iy = disY + disY * i + next.y; 2611 2612 n = this.#map[ix][iy]; 2613 if(n.is === 1) nn = n; 2614 else is = true; 2615 result[len-1-i] = nn; 2616 } 2617 2618 //result[len] = next; 2619 is = false; 2620 nn = next; 2621 2622 UTILS.emptyPoint.set(now.x, now.y).rotate(next, -angle90); 2623 disX = UTILS.emptyPoint.x - next.x, 2624 disY = UTILS.emptyPoint.y - next.y; 2625 2626 for(i = 0; i < len; i++){ 2627 if(is){ 2628 result[len+i] = nn; 2629 continue; 2630 } 2631 ix = disX + disX * i + next.x; 2632 iy = disY + disY * i + next.y; 2633 2634 n = this.#map[ix][iy]; 2635 if(n.is === 1) nn = n; 2636 else is = true; 2637 result[len+i] = nn; 2638 } 2639 2640 return result; 2641 } 2642 2643 getDots(x, y, a, r = []){ //获取周围的点 x,y, a:8|16, r:存放结果数组 2644 r.length = 0; 2645 const x_1 = x-1, x1 = x+1, y_1 = y-1, y1 = y+1; 2646 if(a === 16) r.push(x_1, y_1, x, y_1, x1, y_1, x_1, y, x1, y, x_1, y1, x, y1, x1, y1); 2647 else r.push(x, y_1, x, y1, x_1, y, x1, y); 2648 } 2649 2650 getDisMHD(nodeA, nodeB){ 2651 return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y); 2652 } 2653 2654 _updateID(){ //更新标记 2655 this._id++; 2656 this._openID = "o_"+this._id; 2657 this._closeID = "c_"+this._id; 2658 } 2659 2660 _check(dotA, dotB){ //检测 a 是否能到 b 2661 //获取 dotB 周围的4个点 并 遍历这4个点 2662 this.getDots(dotB.x, dotB.y, 8, SeekPath.dots4); 2663 for(let k = 0, x, y; k < 8; k += 2){ 2664 x = SeekPath.dots4[k]; 2665 y = SeekPath.dots4[k+1]; 2666 if(this.mapRange.containsPoint(x, y) === false) continue; 2667 2668 //找出 dotA 与 dotB 相交的两个点: 2669 if(Math.abs(dotA.x - x) + Math.abs(dotA.y - y) === 1){ 2670 //如果其中一个交点是不可走的则 dotA 到 dotB 不可走, 既返回 false 2671 if(this.#map[x][y].is === 0) return false; 2672 } 2673 2674 } 2675 2676 return true; 2677 } 2678 2679 run(x, y, x1, y1, path = this.#path){ 2680 path.length = 0; 2681 if(this.#map === null || this.mapRange.containsPoint(x, y) === false) return path; 2682 2683 var _n = this.#map[x][y]; 2684 if(_n.is === 0) return path; 2685 2686 const _sort = SeekPath._sort, 2687 _open = SeekPath._open, 2688 _dots = SeekPath._dots, 2689 time = Date.now(); 2690 2691 //var isDot = true, 2692 var suc = _n, k, mhd, g, h, f, _d; 2693 2694 _n.g = 0; 2695 _n.h = _n.h = Math.abs(x1 - x) * 10 + Math.abs(y1 - y) * 10; 2696 _n.f = _n.h; 2697 _n.p = null; 2698 this._updateID(); 2699 _n.id = this._openID; 2700 _open.push(_n); 2701 2702 while(_open.length !== 0){ 2703 if(Date.now() - time > this.timeout) break; 2704 2705 _open.sort(_sort); 2706 _n = _open.shift(); 2707 if(_n.x === x1 && _n.y === y1){ 2708 suc = _n; 2709 break; 2710 } 2711 2712 if(suc.h > _n.h) suc = _n; 2713 _n.id = this._closeID; 2714 this.getDots(_n.x, _n.y, this.angle, _dots); 2715 2716 for(k = 0; k < this.angle; k += 2){ 2717 2718 _d = this.#map[_dots[k]][_dots[k+1]]; 2719 if(_d.id === this._closeID || _d.is === 0 || this.mapRange.containsPoint(_d.x, _d.y) === false) continue; 2720 2721 mhd = Math["abs"](_n["x"] - _d.x) + Math["abs"](_n["y"] - _d.y); 2722 g = _n["g"] + (mhd === 1 ? 10 : 14); 2723 h = Math["abs"](x1 - _d.x) * 10 + Math["abs"](y1 - _d.y) * 10; 2724 f = g + h; 2725 2726 if(_d.id !== this._openID){ 2727 //如果是斜角和8方向: 2728 if(mhd !== 1 && this.angle === 16){ 2729 if(this._check(_n, _d)){ 2730 _d.g = g; 2731 _d.h = h; 2732 _d.f = f; 2733 _d.p = _n; 2734 _d.id = this._openID; 2735 _open.push(_d); 2736 } 2737 }else{ 2738 _d.g = g; 2739 _d.h = h; 2740 _d.f = f; 2741 _d.p = _n; 2742 _d.id = this._openID; 2743 _open.push(_d); 2744 } 2745 } 2746 2747 else if(g < _d.g){ 2748 _d.g = g; 2749 _d.f = g + _d.h; 2750 _d.p = _n; 2751 } 2752 2753 } 2754 } 2755 2756 this.#success = suc === _n; 2757 2758 while(suc !== null){ 2759 path.unshift(suc); 2760 //path.unshift(this.toScene("x", suc["x"]), suc["height"], this.toScene("y", suc["y"])); 2761 suc = suc["p"]; 2762 } 2763 2764 _open.length = _dots.length = 0; 2765 2766 return path; 2767 } 2768 2769 } 2770 2771 2772 2773 2774 /* TweenValue (从 原点 以规定的时间到达 终点) 2775 2776 parameter: origin, end, time, onUpdate, onEnd; 2777 2778 attribute: 2779 origin: Object; //原点(起点) 2780 end: Object; //终点 2781 time: Number; //origin 到 end 花费的时间 2782 onUpdate: Function; //更新回调; 一个回调参数 origin; 默认null; 2783 onEnd: Function; //结束回调; 没有回调参数; 默认null; (如果返回的是"restart"将不从队列删除, 你可以在onEnd中更新.end不间断的继续补间) 2784 2785 method: 2786 reset(origin, end: Object): undefined; //更换 .origin, .end; 它会清除其它对象的缓存属性 2787 reverse(): undefined; //this.end 复制 this.origin 的原始值 2788 update(): undefined; //Tween 通过此方法统一更新 TweenValue 2789 2790 demo: 2791 //init Tween: 2792 const tween = new Tween(), 2793 animate = function (){ 2794 requestAnimationFrame(animate); 2795 tween.update(); 2796 } 2797 2798 //init TweenValue: 2799 const v1 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v)); 2800 2801 animate(); 2802 tween.start(v1); 2803 2804 */ 2805 class TweenValue{ 2806 2807 constructor(origin = {}, end = {}, time = 500, onEnd = null, onUpdate = null, onStart = null){ 2808 this.origin = origin; 2809 this.end = end; 2810 this.time = time; 2811 2812 this.onUpdate = onUpdate; 2813 this.onEnd = onEnd; 2814 this.onStart = onStart; 2815 2816 //以下属性不能直接设置 2817 this._r = null; 2818 this._t = 0; 2819 this._v = Object.create(null); 2820 } 2821 2822 _start(){ 2823 var v = ""; 2824 for(v in this.origin) this._v[v] = this.origin[v]; 2825 if(this.onStart !== null) this.onStart(this); 2826 this._t = Date.now(); 2827 //this.update(); 2828 } 2829 2830 reset(origin, end){ 2831 this.origin = origin; 2832 this.end = end; 2833 this._v = Object.create(null); 2834 } 2835 2836 reverse(){ 2837 var n = ""; 2838 for(n in this.origin) this.end[n] = this._v[n]; 2839 } 2840 2841 update(){ 2842 2843 if(this["_r"] !== null){ 2844 2845 var ted = Date["now"]() - this["_t"]; 2846 2847 if(ted >= this["time"]){ 2848 2849 for(ted in this["origin"]) this["origin"][ted] = this["end"][ted]; 2850 2851 if(this["onEnd"] !== null){ 2852 2853 if(this["onEnd"](this) === "restart"){ 2854 if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); 2855 this["_start"](); 2856 } 2857 2858 else this["_r"]["stop"](this); 2859 2860 } 2861 2862 else this["_r"]["stop"](this); 2863 2864 } 2865 2866 else{ 2867 ted = ted / this["time"]; 2868 let n = ""; 2869 for(n in this["origin"]) this["origin"][n] = ted * (this["end"][n] - this["_v"][n]) + this["_v"][n]; 2870 if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); 2871 } 2872 2873 } 2874 2875 } 2876 2877 } 2878 2879 2880 2881 2882 /* Tween 动画补间 (TweenValue 的root, 可以管理多个TweenValue) 2883 2884 parameter: 2885 attribute: 2886 method: 2887 start(value: TweenValue): undefined; 2888 stop(value: TweenValue): undefined; 2889 2890 static: 2891 Value: TweenValue; 2892 2893 demo: 2894 //init Tween: 2895 const tween = new Tween(), 2896 animate = function (){ 2897 requestAnimationFrame(animate); 2898 tween.update(); 2899 } 2900 2901 //init TweenValue: 2902 const v2 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v), v => { 2903 v2.reverse(); //v2.end 复制起始值 2904 return "restart"; //返回"restart"表示不删除队列, 需要继续补间 2905 }); 2906 2907 animate(); 2908 tween.start(v2); 2909 2910 */ 2911 class Tween extends RunningList{ 2912 2913 static Value = TweenValue; 2914 2915 constructor(){ 2916 super(); 2917 } 2918 2919 start(value){ 2920 this.add(value); 2921 value._r = this; 2922 value._start(); 2923 } 2924 2925 stop(value){ 2926 this.remove(value); 2927 value._r = null; 2928 } 2929 2930 } 2931 2932 2933 2934 2935 /* TweenTarget 朝着轴插值(有效的跟踪动态目标, 注意此类需要配合 RunningList 类使用, 因为此类在任何情况下都没有阻止你调用.update()方法) 2936 parameter: 2937 v1 = {x: 0}, 2938 v2 = {x: 100}, 2939 distance = 1, //每次移动的距离 2940 onUpdate = null, // 2941 onEnd = null 2942 2943 attribute: 2944 v1: Object; //起点 2945 v2: Object; //终点 2946 onUpdate: Function; // 2947 onEnd: Function; // 2948 2949 method: 2950 update(): undefined; //一般在动画循环里执行此方法 2951 updateAxis(): undefined; //更新v1至v2的方向轴 (初始化时构造器自动调用一次) 2952 setDistance(distance: Number): undefined; //设置每次移动的距离 (初始化时构造器自动调用一次) 2953 2954 demo: 2955 const ttc = new TweenTarget({x:0, y:0}, {x:100, y:100}, 10), 2956 2957 //计时器模拟动画循环函数, 每秒执行一次.update() 2958 timer = new Timer(() => { 2959 ttc.update(); 2960 console.log('update: ', ttc.v1); 2961 2962 }, 1000, Infinity); 2963 2964 ttc.onEnd = v => { 2965 timer.stop(); 2966 console.log('end: ', v); 2967 } 2968 2969 timer.start(); 2970 console.log(ttc); 2971 2972 */ 2973 class TweenTarget{ 2974 2975 #distance = 1; 2976 #distancePow2 = 1; 2977 #axis = {}; 2978 get distance(){return this.#distance;} 2979 2980 constructor(v1 = {x: 0}, v2 = {x: 100}, distance, onUpdate = null, onEnd = null){ 2981 this.v1 = v1; 2982 this.v2 = v2; 2983 this.onUpdate = onUpdate; 2984 this.onEnd = onEnd; 2985 2986 this.setDistance(distance); 2987 this.updateAxis(); 2988 } 2989 2990 setDistance(v = 1){ 2991 this.#distance = v; 2992 this.#distancePow2 = Math.pow(v, 2); 2993 } 2994 2995 updateAxis(){ 2996 var n, v, len = 0; 2997 2998 for(n in this.v1){ 2999 v = this.v2[n] - this.v1[n]; 3000 len += v * v; 3001 this.#axis[n] = v; 3002 3003 } 3004 3005 len = Math.sqrt(len); 3006 3007 if(len !== 0){ 3008 3009 for(n in this.v1) this.#axis[n] *= 1 / len; 3010 3011 } 3012 } 3013 3014 update(){ 3015 var n, len = 0; 3016 3017 for(n in this.v1) len += Math.pow(this.v1[n] - this.v2[n], 2); 3018 3019 if(len > this.#distancePow2){ 3020 3021 for(n in this.v1) this.v1[n] += this.#axis[n] * this.#distance; 3022 if(this.onUpdate !== null) this.onUpdate(this.v1); 3023 3024 } 3025 3026 else{ 3027 3028 for(n in this.v1) this.v1[n] = this.v2[n]; 3029 if(this.onEnd !== null) this.onEnd(this.v1); 3030 3031 } 3032 } 3033 3034 } 3035 3036 3037 3038 3039 /* EventDispatcher 自定义事件管理器 3040 parameter: 3041 attribute: 3042 3043 method: 3044 clearEvents(eventName): undefined; //清除eventName列表, 如果 eventName 未定义清除所有事件 3045 customEvents(eventName, eventParam): this; //创建自定义事件 eventParam 可选 默认{} 3046 getParam(eventName): eventParam; 3047 trigger(eventName, callback): undefined; //触发 (callback: 可选) 3048 register(eventName, callback): undefined; // 3049 deleteEvent(eventName, callback): undefined; // 3050 3051 demo: 3052 const eventDispatcher = new EventDispatcher(); 3053 eventDispatcher.customEvents("test", {name: "test"}); 3054 3055 eventDispatcher.register("test", eventParam => { 3056 console.log(eventParam) //Object{name: "test"} 3057 }); 3058 3059 eventDispatcher.trigger("test"); 3060 3061 */ 3062 class EventDispatcher{ 3063 3064 constructor(){ 3065 this._eventsList = {}; 3066 this.__eventsList = []; 3067 this.__trigger = ""; 3068 } 3069 3070 clearEvents(eventName){ 3071 3072 //if(this.__trigger === eventName) return console.warn("EventDispatcher: 清除事件失败"); 3073 if(this._eventsList[eventName] !== undefined) this._eventsList[eventName].func = [] 3074 3075 else this._eventsList = {} 3076 3077 } 3078 3079 customEvents(eventName, eventParam = {}){ 3080 //if(typeof eventName !== "string") return console.warn("EventDispatcher: 注册自定义事件失败"); 3081 if(this._eventsList[eventName] !== undefined) return console.warn("EventDispatcher: "+eventName+" 已存在"); 3082 //eventParam = eventParam || {} 3083 //if(eventParam.type === undefined) eventParam.type = eventName; 3084 this._eventsList[eventName] = {func: [], param: eventParam} 3085 return this; 3086 } 3087 3088 getParam(eventName){ 3089 return this._eventsList[eventName]["param"]; 3090 } 3091 3092 trigger(eventName, callback){ 3093 //if(this._eventsList[eventName] === undefined) return; 3094 3095 const obj = this._eventsList[eventName]; 3096 var k, len = obj.func.length; 3097 3098 if(len !== 0){ 3099 if(typeof callback === "function") callback(obj["param"]); //更新参数(eventParam) 3100 3101 //触发过程(如果触发过程中删除事件, 不仅 len 没有变, 而且还会漏掉一个key, 所以在触发过程中删除的事件要特殊处理) 3102 this.__trigger = eventName; 3103 for(k = 0; k < len; k++) obj["func"][k](obj["param"]); 3104 this.__trigger = ""; 3105 //触发过程结束 3106 3107 //删除在触发过程中要删除的事件 3108 len = this.__eventsList.length; 3109 for(k = 0; k < len; k++) this.deleteEvent(eventName, this.__eventsList[k]); 3110 this.__eventsList.length = 0; 3111 3112 } 3113 3114 } 3115 3116 register(eventName, callback){ 3117 if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); 3118 const obj = this._eventsList[eventName]; 3119 //if(obj.func.includes(callback) === false) obj.func.push(callback); 3120 //else console.warn("EventDispatcher: 回调函数重复"); 3121 obj.func.push(callback); 3122 } 3123 3124 deleteEvent(eventName, callback){ 3125 if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); 3126 3127 if(this.__trigger === eventName) this.__eventsList.push(callback); 3128 else{ 3129 const obj = this._eventsList[eventName], i = obj.func.indexOf(callback); 3130 if(i !== -1) obj.func.splice(i, 1); 3131 } 3132 3133 } 3134 3135 } 3136 3137 3138 3139 3140 export { 3141 UTILS, 3142 ColorRefTable, 3143 AnimateLoop, 3144 Ajax, 3145 IndexedDB, 3146 TreeStruct, 3147 Point, 3148 Line, 3149 Box, 3150 Circle, 3151 Polygon, 3152 RGBColor, 3153 Timer, 3154 SeekPath, 3155 RunningList, 3156 TweenValue, 3157 Tween, 3158 TweenTarget, 3159 EventDispatcher, 3160 Rotate, 3161 ShapeRect, 3162 SecurityDoor, 3163 }
1 "use strict" 2 3 import { 4 Vector3, 5 Skeleton, 6 Frustum, 7 Matrix4, 8 Mesh, 9 sRGBEncoding, 10 RepeatWrapping, 11 TextureLoader, 12 ObjectLoader, 13 AnimationLoader, 14 BufferGeometryLoader, 15 } from 'three'; //import * as THREE from '../../../build/three.module.js'; 16 17 /* import {Sky} from '../../jsm/objects/Sky.js'; 18 import {Water} from '../../jsm/objects/Water.js'; 19 import {CSS2DRenderer, CSS2DObject} from '../../jsm/renderers/CSS2DRenderer.js'; 20 import {mergeBufferGeometries} from '../../jsm/utils/BufferGeometryUtils.js'; 21 import {VertexNormalsHelper} from '../../jsm/helpers/VertexNormalsHelper.js'; 22 import { DecalGeometry } from '../../jsm/geometries/DecalGeometry.js'; 23 import { LightningStrike } from '../../jsm/geometries/LightningStrike.js'; */ 24 25 import {MTLLoader} from '../../jsm/loaders/MTLLoader.js'; 26 import {OBJLoader} from '../../jsm/loaders/OBJLoader.js'; 27 import {GLTFLoader} from '../../jsm/loaders/GLTFLoader.js'; 28 import {STLLoader} from '../../jsm/loaders/STLLoader.js'; 29 import {FBXLoader} from '../../jsm/loaders/FBXLoader.js'; 30 import {GLTFExporter} from '../../jsm/exporters/GLTFExporter.js'; 31 import {STLExporter} from '../../jsm/exporters/STLExporter.js'; 32 import {OBJExporter} from '../../jsm/exporters/OBJExporter.js'; 33 34 import {UTILS} from './Utils.js'; 35 36 /** ThreeUtils three.js实用类 37 38 */ 39 const cameraFCVMatrix4 = new Matrix4(), 40 41 textureNames = [ 42 "map", 43 "alphaMap", 44 "aoMap", 45 "envMap", 46 "lightMap", 47 "specularMap", 48 "emissiveMap", 49 "bumpMap", 50 "displacementMap", 51 "metalnessMap", 52 "roughnessMap", 53 "normalMap" 54 ], 55 56 _disposeMaterial = function (material){ 57 for(let k = 0, len = textureNames.length; k < len; k++){ 58 if(material[textureNames[k]]) material[textureNames[k]].dispose(); 59 } 60 61 material.dispose(); 62 }, 63 64 ThreeUtils = { 65 66 frustum: new Frustum(), 67 68 textureLoader: new TextureLoader(), 69 MTLLoader: MTLLoader, 70 OBJLoader: OBJLoader, 71 GLTFLoader: GLTFLoader, 72 STLLoader: STLLoader, 73 FBXLoader: FBXLoader, 74 ObjectLoader: ObjectLoader, 75 AnimationLoader: AnimationLoader, 76 GeometryLoader: BufferGeometryLoader, 77 78 GLTFExporter: GLTFExporter, 79 STLExporter: STLExporter, 80 OBJExporter: OBJExporter, 81 82 traverse(object3D, callback){ 83 84 if(callback(object3D) !== "continue"){ 85 86 for(let k = 0, v, len = object3D.children.length; k < len; k++){ 87 88 v = object3D.children[k]; 89 90 if(v.children.length !== 0) this.traverse(v, callback); 91 else callback(v); 92 93 } 94 95 } 96 97 }, 98 99 //清除 Object3D 缓存(不删除对象) 100 disposeObject3D(object3D){ 101 102 if(object3D.material !== undefined) this.disposeMaterial(object3D.material); 103 if(object3D.geometry !== undefined) object3D.geometry.dispose(); 104 if(object3D.skeleton !== undefined) object3D.skeleton.dispose(); 105 106 }, 107 108 //支持数组材质 109 disposeMaterial(material){ 110 111 if(Array.isArray(material) === true){ 112 for(let k = 0, c = material.length; k < c; k++) _disposeMaterial(material[k]); 113 } 114 else _disposeMaterial(material); 115 116 return material; 117 118 }, 119 120 //更换 geometry 121 updateGeometry(mesh, geometry){ 122 123 mesh.geometry.dispose(); 124 mesh.geometry = geometry; 125 126 }, 127 128 isObject3D(v){//v是否是object3D 129 130 return UTILS.isObject(v) && v.isObject3D === true; 131 132 }, 133 134 isBone(v){ 135 136 return (this.isObject3D(v) && v.isBone === true); 137 138 }, 139 140 isSkinMesh(v){ 141 142 return (this.isObject3D(v) && v.isSkinnedMesh === true); 143 144 }, 145 146 isCSS2D(v){ 147 148 return (this.isObject3D(v) && v.isCSS2DObject === true); 149 150 }, 151 152 //克隆蒙皮网格(材质共享) 153 cloneSkinnedMesh(v){ 154 155 let bone = v.skeleton.bones[0].clone(true), bones = []; 156 bone.traverse((e)=>bones.push(e)); 157 158 const newMesh = v.clone(); 159 newMesh.geometry = v.geometry.clone(); 160 newMesh.children.length = 0; 161 newMesh.add(bone); 162 newMesh.bind(new Skeleton(bones)); 163 164 return newMesh; 165 166 }, 167 168 //3维世界坐标 转为 屏幕坐标 必须: obj: Vector3 || Object3D; camera: 相机 169 //width: number屏幕宽; height: number屏幕高; 可选: vec3: Vector3;(结果复制到vec3中) 170 worldPosToScreen(obj, camera, width, height, vec3){ 171 172 vec3 = vec3 || new Vector3(); 173 174 if(this.isObject3D(obj) === true) obj.getWorldPosition(vec3);//如果是3D对象 175 else if(obj.isVector3 === true) vec3.copy(obj);//如果是Vector3对象 176 else return vec3; 177 178 var _x = 0.5 * width, _y = 0.5 * height;//中心点坐标 179 vec3.project(camera);//转为标准设备坐标 180 vec3.set((vec3.x * _x) + _x, -(vec3.y * _y) + _y, 0);//转为屏幕坐标 181 182 return vec3; 183 184 }, 185 186 //检测一个点是否在相机视椎体内,返回boolean 187 cameraFrustumContainsVec3(camera, vec3){ 188 189 camera.updateMatrix(); 190 camera.updateMatrixWorld(); 191 this.frustum.setFromProjectionMatrix(cameraFCVMatrix4.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)); 192 return this.frustum.containsPoint(vec3); 193 194 }, 195 196 loadModels(url, callback, type){ 197 198 if(!type) type = UTILS.getFileType(url); 199 var name = "", uuid; 200 201 switch(type){ 202 case "gltf": 203 case "glb": 204 name = "GLTFLoader"; 205 break; 206 207 case "fbx": 208 name = "FBXLoader"; 209 break; 210 211 case "json": 212 name = "ObjectLoader"; 213 break; 214 215 case "stl": //只由geometry, 文件体积最小 216 name = "STLLoader"; 217 break; 218 219 case "obj": //需要额外加载材质 220 name = "OBJLoader"; 221 break; 222 223 case "mtl": 224 name = "MTLLoader"; 225 break; 226 227 default: 228 console.warn("ThreeUtils: 暂不支持: "+type); 229 return; 230 } 231 232 const lode = new this[name](); 233 lode.load(url, (e)=>{ 234 //ObjectLoader (bug: 有时候会执行两次回调) 235 if(this.isObject3D(e) === true){ 236 if(e.uuid !== uuid){ 237 callback(e); 238 uuid = e.uuid; 239 } 240 } 241 242 //other 243 else callback(e.isBufferGeometry === true ? new Mesh(e) : e) 244 245 }); 246 247 return lode; 248 249 }, 250 251 loadGeometry(url, callback){ 252 253 //new this.GeometryLoader(url, callback); 254 255 }, 256 257 loadTexture(url){ 258 259 const map = new TextureLoader().load(url); 260 map.encoding = sRGBEncoding; 261 map.wrapS = map.wrapT = RepeatWrapping; 262 263 return map; 264 265 }, 266 267 } 268 269 270 export {ThreeUtils}
1 "use strict"; 2 3 /* O3D 此类用于测试 ThreeJS(javascript 3D库) 4 attributes: 5 orbitEnabled: bool; //禁或启用轨道控制器 6 transformEnabled: bool; //禁或启用变换控制器 7 //只读属性: 8 domElement: HTMLDivElement; //3D场景的父div 9 animateLoop: AnimateLoop; //这属性可以启或禁用动画循环 (可以的方法参见 Utils.js/AnimateLoop) 10 手动更新3D场景: O3D.animateLoop.update() 11 12 method: 13 init(parentElement: HTMLElement): undefined; //初始化O3D 14 dispose(): undefined; //销毁O3D (销毁后可重新init使用) 15 resize(w, h: String): undefined; //设置或更新 domElement 的宽高 (渲染器和相机也会更新) 16 intersect(offsetX, offsetY: Number, obj3ds: Array): Array; //运行一次射线拾取 17 clickEvent(callback: Func): undefined; //给 domElement 添加点击事件 18 addEvent(eventName: String, callback: Func): undefined; // 19 removeEvent(eventName: String, callback: Func): undefined; // 20 appendObject3D(obj3d, parentObject3D: Object3D): undefined; //往场景添加3D对象 21 removeObject3D(obj3d: Object3D): undefined; //从场景删除3D对象 22 23 helloBox(w, h, d, ws, hs, ds: Number): undefined; //往3D场景添加一个box网格 24 css2dObject(div: HTMLDivElement): CSS2DObject; //创建一个 CSS2DObject 对象(用div生成一个3D对象) 25 26 initBoxHelper(): undefined; //拾取辅助框 27 initGridHelper(): undefined; //地面网格辅助 28 initSelection(obj3ds: Object3D): undefined; //点拾取3D对象 29 initSelectionBox(startPoint, endPoint, deep): Object; //矩形框拾取3D对象 30 31 eventNames: 32 "down", "up", "move" //domElement 的鼠标事件 33 "selectChange" //拾取列表改变时触发 34 "orbitControlsChange" //OrbitControlsChange 轨道控制器的 change 事件 35 "transformControlsChange" //TransformControls 变换控制器的 change 事件 36 37 demo: 38 import { UTILS } from './lib/Utils.js'; 39 import { O3D } from './O3D.js'; 40 41 document.body.style.overflow = "hidden"; 42 O3D.init(); //初始化 O3D 43 44 for(let i = 0, helloBox; i < 10; i++){ 45 helloBox = O3D.helloBox(); //添加一个box网格到场景 46 helloBox.position.x = UTILS.random(-10, 10); 47 helloBox.position.z = UTILS.random(-10, 10); 48 } 49 50 O3D.initGridHelper(); //地面辅助网格 51 O3D.initBoxHelper(); //拾取辅助框 52 53 //点拾取对象 (鼠标左键) 54 O3D.initSelection(); 55 56 //矩形框拾取对象 (鼠标右键) 57 const selectionBox = O3D.initSelectionBox().initHelper(); 58 selectionBox.helperEnabled = true; //启用矩形框拾取对象 (当它启用时, 轨道控制器的鼠标右键会失效) 59 60 addEventListener("resize", () => O3D.resize(innerWidth+"px", innerHeight+"px")); 61 62 */ 63 64 import { 65 Vector2, 66 Raycaster, 67 sRGBEncoding, 68 WebGLRenderer, 69 PerspectiveCamera, 70 Scene, 71 Color, 72 AmbientLight, 73 Group, 74 Mesh, 75 BoxGeometry, 76 BoxHelper, 77 Box3, 78 GridHelper, 79 Frustum, 80 Vector3, 81 Sphere, 82 Quaternion, 83 Euler, 84 //Clock, 85 //AnimationMixer, 86 } from 'three'; 87 88 import { CSS2DRenderer, CSS2DObject } from '../jsm/renderers/CSS2DRenderer.js'; 89 import { OrbitControls } from '../jsm/controls/OrbitControls.js'; 90 import {TransformControls} from '../jsm/controls/TransformControls.js'; 91 92 import { ThreeUtils } from './lib/ThreeUtils.js'; 93 import { AnimateLoop, EventDispatcher, SecurityDoor } from './lib/Utils.js'; 94 95 var eventDispatcher, domElement, renderer, css2dRenderer, camera, scene, ambientLight, object3d, orbitControls, transformControls; 96 97 //射线拾取 98 const ray = new Raycaster(), ray_vec2 = new Vector2(), ray_result = []; 99 100 //拾取队列 101 const selectObject3ds = [], selectObject3ds_box3 = new Box3(); 102 103 //点击与抬起的延迟时间 104 const clickDelay = 300; 105 106 //动画循环 107 const animateLoop = new AnimateLoop(function (){ 108 renderer["render"](scene, camera); 109 css2dRenderer["render"](scene, camera); 110 }); 111 112 //renderer 113 function exitRender(){ 114 renderer.dispose(); 115 //css2dRenderer.dispose(); 116 if(domElement.parentElement) domElement.parentElement.removeChild(domElement); 117 eventDispatcher = domElement = css2dRenderer = renderer = undefined; 118 } 119 120 function initRender(){ 121 renderer = new WebGLRenderer({ //precision: "highp" "mediump" "lowp"; 122 //antialias: true, 123 //precision: "lowp", 124 }); 125 renderer.debug.checkShaderErrors = false; 126 renderer.outputEncoding = sRGBEncoding; 127 renderer.setPixelRatio(window.devicePixelRatio); 128 renderer.domElement.style.zIndex = -1; 129 renderer.domElement.style.position = "absolute"; 130 renderer.domElement.style.left = 0; 131 renderer.domElement.style.top = 0; 132 133 // 134 css2dRenderer = new CSS2DRenderer(); 135 css2dRenderer.domElement.style.zIndex = 0; 136 css2dRenderer.domElement.style.position = "absolute"; 137 css2dRenderer.domElement.style.left = 0; 138 css2dRenderer.domElement.style.top = 0; 139 140 //domElement 141 domElement = globalThis.document.createElement("div"); 142 domElement.appendChild(renderer.domElement); 143 domElement.appendChild(css2dRenderer.domElement); 144 domElement.style = ` 145 width: ${globalThis.innerWidth}px; 146 height: ${globalThis.innerHeight}px; 147 `; 148 149 //event 150 const param_pointerdown = {pointerEvent: null, button:0, pointerId:0, pageX:0, pageY:0, offsetX:0, offsetY:0}, 151 param_pointerup = {pointerEvent: null, button:0, pointerId:0, pageX:0, pageY:0, offsetX:0, offsetY:0}, 152 param_pointermove = {pointerEvent: null, button:0, pointerId:0, pageX:0, pageY:0, offsetX:0, offsetY:0} 153 eventDispatcher = new EventDispatcher() 154 .customEvents('down', param_pointerdown) 155 .customEvents('up', param_pointerup) 156 .customEvents('move', param_pointermove) 157 .customEvents('selectChange', {targets: selectObject3ds}); 158 domElement.onpointerdown = function (event) { 159 param_pointerdown.pointerEvent = event; 160 param_pointerdown.button = event.button; 161 param_pointerdown.pointerId = event.pointerId; 162 param_pointerdown.pageX = event.pageX; 163 param_pointerdown.pageY = event.pageY; 164 param_pointerdown.offsetX = event.offsetX; 165 param_pointerdown.offsetY = event.offsetY; 166 eventDispatcher.trigger("down"); 167 } 168 domElement.onpointerup = function (event) { 169 param_pointerup.pointerEvent = event; 170 param_pointerup.button = event.button; 171 param_pointerup.pointerId = event.pointerId; 172 param_pointerup.pageX = event.pageX; 173 param_pointerup.pageY = event.pageY; 174 param_pointerup.offsetX = event.offsetX; 175 param_pointerup.offsetY = event.offsetY; 176 eventDispatcher.trigger("up"); 177 } 178 domElement.onpointermove = function (event) { 179 param_pointermove.pointerEvent = event; 180 param_pointermove.button = event.button; 181 param_pointermove.pointerId = event.pointerId; 182 param_pointermove.pageX = event.pageX; 183 param_pointermove.pageY = event.pageY; 184 param_pointermove.offsetX = event.offsetX; 185 param_pointermove.offsetY = event.offsetY; 186 eventDispatcher.trigger("move"); 187 } 188 } 189 190 function initSelectsBox3(){ 191 const _box3 = new Box3(), box3 = selectObject3ds_box3, vec3 = new Vector3(), 192 193 updateBox3s = function (obj3d){ 194 obj3d.getWorldPosition(vec3); 195 if(obj3d.geometry !== undefined){ 196 if(obj3d.geometry.boundingBox === null) obj3d.geometry.computeBoundingBox(); 197 _box3.copy(obj3d.geometry.boundingBox); 198 _box3.min.add(vec3); 199 _box3.max.add(vec3); 200 box3.union(_box3); 201 }else{ 202 box3.expandByPoint(vec3); 203 } 204 }, 205 206 updateBox3 = function (){ 207 if(selectObject3ds.length === 0) return; 208 const obj3d = selectObject3ds[0]; 209 obj3d.getWorldPosition(vec3); 210 if(obj3d.geometry !== undefined){ 211 if(obj3d.geometry.boundingBox === null) obj3d.geometry.computeBoundingBox(); 212 box3.copy(obj3d.geometry.boundingBox); 213 box3.min.add(vec3); 214 box3.max.add(vec3); 215 }else{ 216 box3.min.copy(vec3); 217 box3.max.copy(vec3); 218 } 219 for(let k = 1, len = selectObject3ds.length; k < len; k++) updateBox3s(selectObject3ds[k]); 220 } 221 222 eventDispatcher.register("selectChange", updateBox3); 223 } 224 225 226 //PerspectiveCamera 227 function exitCamera(){ 228 camera = undefined; 229 } 230 231 function initCamera(){ 232 camera = new PerspectiveCamera(45, globalThis.innerWidth / globalThis.innerHeight, 1, 1000); 233 camera.position.set(0, 5, 10); 234 camera.lookAt(0, 0, 0); 235 //camera.updateProjectionMatrix(); 236 } 237 238 239 //Scene 240 function exitScene(){ 241 scene.traverse(object3D => ThreeUtils.disposeObject3D(object3D)); 242 object3d = scene = undefined 243 } 244 245 function initScene(){ 246 scene = new Scene(); 247 //scene.autoUpdate = false; //默认值为true,若设置了这个值,则渲染器会检查每一帧是否需要更新场景及其中物体的矩阵。 当设为false时,你必须亲自手动维护场景中的矩阵 248 //scene.background = new Color("#b9c0bc"); 249 //scene.fog = new this.THREE.Fog(new this.THREE.Color("#dce0de"), camera.far*0.8, camera.far); 250 //scene.fog = new this.THREE.FogExp2(scene.background, 0.0005); 251 //scene.overrideMaterial = new MeshBasicMaterial(); //如果不为空,它将强制场景中的每个物体使用这里的材质来渲染。默认值为null 252 object3d = new Group(); 253 object3d.name = "O3D_root"; 254 ambientLight = new AmbientLight(); 255 scene.add(ambientLight, object3d); 256 } 257 258 259 //OrbitControls 260 function exitOrbitControls(){ 261 orbitControls._securityDoor = undefined; 262 orbitControls.dispose(); 263 orbitControls = undefined; 264 } 265 266 function initOrbitControls(){ 267 const onchange = function (){ 268 eventDispatcher.trigger("orbitControlsChange"); 269 animateLoop.update(); 270 }, 271 _onchange = function (enabled){ 272 orbitControls.enabled = enabled; 273 if(enabled){ 274 orbitControls.addEventListener("change", onchange); 275 } 276 else{ 277 orbitControls.removeEventListener("change", onchange); 278 } 279 }, 280 281 _securityDoor = new SecurityDoor(null, _onchange); 282 283 orbitControls = new OrbitControls(camera, domElement); 284 _onchange(_securityDoor.empty); 285 286 /* orbitControls.zoomSpeed = 5; 287 orbitControls.enablePan = true; 288 orbitControls.minDistance = 80; 289 orbitControls.maxDistance = 200; 290 orbitControls.minPolarAngle = Math.PI*0.2; 291 orbitControls.maxPolarAngle = Math.PI*0.54; */ 292 //orbitControls.enabled 293 eventDispatcher.customEvents('orbitControlsChange'); 294 orbitControls._securityDoor = _securityDoor; 295 orbitControls.update(); 296 } 297 298 //TransformControls 299 function exitTransformControls(){ 300 transformControls.dispose(); 301 transformControls = undefined; 302 } 303 304 function initTransformControls(){ 305 const ondragging = function (event){ 306 if(event.value) orbitControls._securityDoor.add("O3D_transformControls"); 307 else orbitControls._securityDoor.remove("O3D_transformControls"); 308 }, 309 _onchange = function (enabled){ 310 transformControls.enabled = enabled; 311 if(enabled){ 312 transformControls.addEventListener('dragging-changed', ondragging); 313 transformControls.addEventListener('change', onchange); 314 eventDispatcher.register("selectChange", onselectChange); 315 } 316 else{ 317 transformControls.removeEventListener('dragging-changed', ondragging); 318 transformControls.removeEventListener('change', onchange); 319 eventDispatcher.deleteEvent("selectChange", onselectChange); 320 } 321 }, 322 _securityDoor = new SecurityDoor(null, _onchange), 323 _object3d = new Group(), 324 _outPoint = new Vector3(), 325 _vec3 = new Vector3(), 326 327 onselectChange = function (){ 328 const len = selectObject3ds.length; 329 if(len === 0 || selectObject3ds_box3.isEmpty()) transformControls.detach(_object3d); 330 else{ 331 selectObject3ds_box3.getCenter(_object3d.position); 332 _outPoint.copy(_object3d.position); 333 if(len === 1){ 334 _object3d.quaternion.copy(selectObject3ds[0].quaternion); 335 _object3d.scale.copy(selectObject3ds[0].scale); 336 }else{ 337 _object3d.quaternion.set(0,0,0,1); 338 _object3d.scale.set(1,1,1); 339 } 340 transformControls.attach(_object3d); 341 } 342 }, 343 344 updatePosition = function (obj3d){ 345 switch(transformControls.mode){ 346 case "translate": obj3d.position.add(_vec3); break; 347 case "rotate": obj3d.quaternion.copy(_object3d.quaternion); break; 348 case "scale": obj3d.scale.copy(_object3d.scale); break; 349 } 350 }, 351 352 onchange = function (){ 353 _vec3.copy(_object3d.position).sub(_outPoint); 354 selectObject3ds.forEach(updatePosition); 355 _outPoint.copy(_object3d.position); 356 eventDispatcher.trigger("transformControlsChange"); 357 animateLoop.update(); 358 } 359 360 transformControls = new TransformControls(camera, domElement); 361 //transformControls.mode = "translate"; //默认"translate"、"rotate" 和 "scale" 362 //transformControls.space = "world"; //默认"world" 和 "local"; 363 transformControls.size = 0.5; //默认 1 364 365 eventDispatcher.customEvents('transformControlsChange'); 366 transformControls._securityDoor = _securityDoor; 367 transformControls._object = _object3d; 368 if(scene) scene.add(transformControls, _object3d); 369 _onchange(_securityDoor.empty); 370 } 371 372 // 373 const O3D = { 374 375 get orbitEnabled(){ 376 return orbitControls._securityDoor.empty; 377 }, 378 379 set orbitEnabled(v){ 380 if(v === true) orbitControls._securityDoor.remove("O3D_user"); 381 else if(v === false) orbitControls._securityDoor.add("O3D_user"); 382 }, 383 384 get transformEnabled(){ 385 return transformControls._securityDoor.empty; 386 }, 387 388 set transformEnabled(v){ 389 if(v === true) transformControls._securityDoor.remove("O3D_user"); 390 else if(v === false) transformControls._securityDoor.add("O3D_user"); 391 }, 392 393 get domElement(){ 394 return domElement; 395 }, 396 397 get camera(){ 398 return camera; 399 }, 400 401 get renderer(){ 402 return renderer; 403 }, 404 405 get scene(){ 406 return scene; 407 }, 408 409 get animateLoop(){ 410 return animateLoop; 411 }, 412 413 init(parentElement = globalThis.document.body){ 414 initRender(); 415 initSelectsBox3(); 416 initCamera(); 417 initScene(); 418 initOrbitControls(); 419 initTransformControls(); 420 421 parentElement.appendChild(domElement); 422 this.resize(); 423 }, 424 425 dispose(){ 426 animateLoop.stop(); 427 domElement.parentElement.removeChild(domElement); 428 exitRender(); 429 exitCamera(); 430 exitTransformControls(); 431 exitOrbitControls(); 432 exitScene(); 433 selectObject3ds.length = ray_result.length = 0; 434 }, 435 436 resize(w = domElement.offsetWidth+"px", h = domElement.offsetHeight+"px"){ 437 domElement.style.width = w; 438 domElement.style.height = h; 439 440 renderer.setSize(domElement.offsetWidth, domElement.offsetHeight); 441 css2dRenderer.setSize(domElement.offsetWidth, domElement.offsetHeight); 442 443 camera.aspect = domElement.offsetWidth / domElement.offsetHeight; 444 camera.updateProjectionMatrix(); 445 446 animateLoop.update(); 447 }, 448 449 intersect(offsetX, offsetY, obj3ds = object3d.children){ 450 ray_result.length = 0; 451 ray_vec2.x = offsetX / domElement.offsetWidth * 2 - 1; 452 ray_vec2.y = - offsetY / domElement.offsetHeight * 2 + 1; 453 ray.setFromCamera(ray_vec2, camera); 454 ray.intersectObjects(obj3ds, true, ray_result); 455 return ray_result; 456 }, 457 458 clickEvent(callback){ 459 if(typeof callback !== "function") return console.warn("clickEvent: 参数必须是一个函数"); 460 var pointerId = -1, isDown = false, sTime = 0; 461 462 const _onup = () => { 463 eventDispatcher.deleteEvent("up", onup); 464 isDown = false; 465 }, 466 467 onup = event => { 468 if(isDown && event.pointerId === pointerId && Date.now() - sTime < clickDelay){ 469 _onup(); 470 callback(event); 471 } 472 }, 473 474 ondown = event => { 475 if(isDown) _onup(); 476 if(event.button !== 0) return; 477 478 sTime = Date.now(); 479 pointerId = event.pointerId; 480 isDown = true; 481 482 eventDispatcher.register("up", onup); 483 } 484 485 eventDispatcher.register("down", ondown); 486 }, 487 488 addEvent(eventName, callback){ 489 eventDispatcher.register(eventName, callback); 490 }, 491 492 removeEvent(eventName, callback){ 493 eventDispatcher.deleteEvent(eventName, callback); 494 }, 495 496 helloBox(w, h, d, ws, hs, ds){ 497 const mesh = new Mesh(new BoxGeometry(w, h, d, ws, hs, ds)); 498 object3d.add(mesh); 499 return mesh; 500 }, 501 502 css2dObject(div = globalThis.document.createElement("div")){ 503 return new CSS2DObject(div); 504 }, 505 506 appendObject3D(obj3d, parentObject3D = object3d){ 507 if(ThreeUtils.isObject3D(obj3d)){ 508 if(ThreeUtils.isObject3D(obj3d.parent)){ 509 ThreeUtils.disposeObject3D(obj3d); 510 obj3d.parent.remove(obj3d); 511 } 512 parentObject3D.add(obj3d); 513 } 514 }, 515 516 removeObject3D(obj3d){ 517 if(ThreeUtils.isObject3D(obj3d) && ThreeUtils.isObject3D(obj3d.parent)){ 518 const i = selectObject3ds.indexOf(obj3d); 519 if(i !== -1){ 520 selectObject3ds.splice(i, 1); 521 eventDispatcher.trigger("selectChange"); 522 } 523 ThreeUtils.disposeObject3D(obj3d); 524 obj3d.parent.remove(obj3d); 525 } 526 }, 527 528 initBoxHelper(){ 529 const boxHelper = new BoxHelper(); 530 boxHelper.geometry.computeBoundingBox(); 531 boxHelper.visible = false; 532 transformControls._object.add(boxHelper); 533 534 const box3 = new Box3(), min = box3.min, max = box3.max, 535 array = boxHelper.geometry.attributes.position.array, 536 537 updateBoxHelper = () => { 538 if(selectObject3ds.length === 0 || selectObject3ds_box3.isEmpty()){ 539 boxHelper.visible = false; 540 541 }else{ 542 box3.copy(selectObject3ds_box3); 543 min.sub(transformControls._object.position); 544 max.sub(transformControls._object.position); 545 array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z; 546 array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z; 547 array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z; 548 array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z; 549 array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z; 550 array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z; 551 array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z; 552 array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z; 553 boxHelper.geometry.attributes.position.needsUpdate = true; 554 boxHelper.geometry.computeBoundingSphere(); 555 boxHelper.visible = true; 556 } 557 } 558 559 eventDispatcher.register("selectChange", updateBoxHelper); 560 561 }, 562 563 initGridHelper(size = 20, divisions = 20, colorCenterLine, colorGrid){ 564 scene.add(new GridHelper(size, divisions, colorCenterLine, colorGrid)); 565 }, 566 567 initSelection(obj3ds = object3d.children){ 568 var obj3d, i; 569 const func = event => { 570 obj3d = this.intersect(event.offsetX, event.offsetY, obj3ds)[0]; 571 if(!obj3d){ 572 selectObject3ds.length = 0; 573 eventDispatcher.trigger("selectChange"); 574 animateLoop.update(); 575 return; 576 } 577 578 obj3d = obj3d.object; 579 i = selectObject3ds.indexOf(obj3d); 580 if(i === -1){ 581 //如果选中列表存在object3D的上级,就解除其上级的选中状态 582 var par = obj3d.parent, i; 583 while(par !== null){ 584 i = selectObject3ds.indexOf(par); 585 if(i !== -1) selectObject3ds.splice(i, 1); 586 par = par.parent; 587 } 588 589 //如果选中列表存在object3D的下级,就解除其下级的选中状态 590 obj3d.traverse(o => { 591 if(obj3d !== o){ 592 i = selectObject3ds.indexOf(o); 593 if(i !== -1) selectObject3ds.splice(i, 1); 594 } 595 }); 596 597 selectObject3ds.push(obj3d); 598 } 599 600 else selectObject3ds.splice(i, 1); 601 602 eventDispatcher.trigger("selectChange"); 603 animateLoop.update(); 604 } 605 606 this.clickEvent(func); 607 }, 608 609 initSelectionBox(startPoint = new Vector3(), endPoint = new Vector3(), deep = globalThis.Number.MAX_VALUE){ 610 const _frustum = new Frustum(); 611 const _tmpPoint = new Vector3(); 612 613 const _vecNear = new Vector3(); 614 const _vecTopLeft = new Vector3(); 615 const _vecTopRight = new Vector3(); 616 const _vecDownRight = new Vector3(); 617 const _vecDownLeft = new Vector3(); 618 619 const _vecFarTopLeft = new Vector3(); 620 const _vecFarTopRight = new Vector3(); 621 const _vecFarDownRight = new Vector3(); 622 const _vecFarDownLeft = new Vector3(); 623 624 const _vectemp1 = new Vector3(); 625 const _vectemp2 = new Vector3(); 626 const _vectemp3 = new Vector3(); 627 628 const scope = this; 629 var _helperEnabled = null; 630 631 return { 632 633 get frustum(){ 634 return _frustum; 635 }, 636 637 get helperEnabled(){ 638 return _helperEnabled !== null && _helperEnabled.empty; 639 }, 640 641 set helperEnabled(v){ 642 if(_helperEnabled === null) return; 643 if(v === true) _helperEnabled.remove("O3D_user"); 644 else if(v === false) _helperEnabled.add("O3D_user"); 645 }, 646 647 updateFrustum(){ 648 // Avoid invalid frustum 649 650 if ( startPoint.x === endPoint.x ) { 651 652 endPoint.x += Number.EPSILON; 653 654 } 655 656 if ( startPoint.y === endPoint.y ) { 657 658 endPoint.y += Number.EPSILON; 659 660 } 661 662 camera.updateProjectionMatrix(); 663 camera.updateMatrixWorld(); 664 665 if ( camera.isPerspectiveCamera ) { 666 667 _tmpPoint.copy( startPoint ); 668 _tmpPoint.x = Math.min( startPoint.x, endPoint.x ); 669 _tmpPoint.y = Math.max( startPoint.y, endPoint.y ); 670 endPoint.x = Math.max( startPoint.x, endPoint.x ); 671 endPoint.y = Math.min( startPoint.y, endPoint.y ); 672 673 _vecNear.setFromMatrixPosition( camera.matrixWorld ); 674 _vecTopLeft.copy( _tmpPoint ); 675 _vecTopRight.set( endPoint.x, _tmpPoint.y, 0 ); 676 _vecDownRight.copy( endPoint ); 677 _vecDownLeft.set( _tmpPoint.x, endPoint.y, 0 ); 678 679 _vecTopLeft.unproject( camera ); 680 _vecTopRight.unproject( camera ); 681 _vecDownRight.unproject( camera ); 682 _vecDownLeft.unproject( camera ); 683 684 _vectemp1.copy( _vecTopLeft ).sub( _vecNear ); 685 _vectemp2.copy( _vecTopRight ).sub( _vecNear ); 686 _vectemp3.copy( _vecDownRight ).sub( _vecNear ); 687 _vectemp1.normalize(); 688 _vectemp2.normalize(); 689 _vectemp3.normalize(); 690 691 _vectemp1.multiplyScalar( deep ); 692 _vectemp2.multiplyScalar( deep ); 693 _vectemp3.multiplyScalar( deep ); 694 _vectemp1.add( _vecNear ); 695 _vectemp2.add( _vecNear ); 696 _vectemp3.add( _vecNear ); 697 698 const planes = _frustum.planes; 699 700 planes[ 0 ].setFromCoplanarPoints( _vecNear, _vecTopLeft, _vecTopRight ); 701 planes[ 1 ].setFromCoplanarPoints( _vecNear, _vecTopRight, _vecDownRight ); 702 planes[ 2 ].setFromCoplanarPoints( _vecDownRight, _vecDownLeft, _vecNear ); 703 planes[ 3 ].setFromCoplanarPoints( _vecDownLeft, _vecTopLeft, _vecNear ); 704 planes[ 4 ].setFromCoplanarPoints( _vecTopRight, _vecDownRight, _vecDownLeft ); 705 planes[ 5 ].setFromCoplanarPoints( _vectemp3, _vectemp2, _vectemp1 ); 706 planes[ 5 ].normal.multiplyScalar( - 1 ); 707 708 } else if ( camera.isOrthographicCamera ) { 709 710 const left = Math.min( startPoint.x, endPoint.x ); 711 const top = Math.max( startPoint.y, endPoint.y ); 712 const right = Math.max( startPoint.x, endPoint.x ); 713 const down = Math.min( startPoint.y, endPoint.y ); 714 715 _vecTopLeft.set( left, top, - 1 ); 716 _vecTopRight.set( right, top, - 1 ); 717 _vecDownRight.set( right, down, - 1 ); 718 _vecDownLeft.set( left, down, - 1 ); 719 720 _vecFarTopLeft.set( left, top, 1 ); 721 _vecFarTopRight.set( right, top, 1 ); 722 _vecFarDownRight.set( right, down, 1 ); 723 _vecFarDownLeft.set( left, down, 1 ); 724 725 _vecTopLeft.unproject( camera ); 726 _vecTopRight.unproject( camera ); 727 _vecDownRight.unproject( camera ); 728 _vecDownLeft.unproject( camera ); 729 730 _vecFarTopLeft.unproject( camera ); 731 _vecFarTopRight.unproject( camera ); 732 _vecFarDownRight.unproject( camera ); 733 _vecFarDownLeft.unproject( camera ); 734 735 const planes = _frustum.planes; 736 737 planes[ 0 ].setFromCoplanarPoints( _vecTopLeft, _vecFarTopLeft, _vecFarTopRight ); 738 planes[ 1 ].setFromCoplanarPoints( _vecTopRight, _vecFarTopRight, _vecFarDownRight ); 739 planes[ 2 ].setFromCoplanarPoints( _vecFarDownRight, _vecFarDownLeft, _vecDownLeft ); 740 planes[ 3 ].setFromCoplanarPoints( _vecFarDownLeft, _vecFarTopLeft, _vecTopLeft ); 741 planes[ 4 ].setFromCoplanarPoints( _vecTopRight, _vecDownRight, _vecDownLeft ); 742 planes[ 5 ].setFromCoplanarPoints( _vecFarDownRight, _vecFarTopRight, _vecFarTopLeft ); 743 planes[ 5 ].normal.multiplyScalar( - 1 ); 744 745 } else { 746 747 console.error( 'Unsupported camera type' ); 748 749 } 750 }, 751 752 initHelper(obj3ds, div){ 753 if(globalThis.Array.isArray(obj3ds) === false) obj3ds = object3d.children; 754 if(globalThis.HTMLDivElement.prototype.isPrototypeOf(div) === false){ 755 div = globalThis.document.createElement("div"); 756 div.style.background = "rgba(0,173,255,0.2)"; 757 div.style.border = "1px solid rgb(0,173,255)"; 758 } 759 760 div.style.pointerEvents = 'none'; 761 div.style.position = "absolute"; 762 763 var pointerId = -1, isDown = false, sTime = 0; 764 765 const _onup = function (){ 766 isDown = false; 767 setDIVBox(0, 0); 768 orbitControls._securityDoor.remove("O3D_selectionBoxHelper"); 769 if(div.parentElement !== null) div.parentElement.removeChild(div); 770 eventDispatcher.deleteEvent("move", onmove); 771 eventDispatcher.deleteEvent("up", onup); 772 //globalThis.document.body.removeEventListener("pointerup", onup); 773 }, 774 775 _onchange = function (enabled){ 776 if(enabled){ 777 eventDispatcher.register("down", ondown); 778 }else{ 779 eventDispatcher.deleteEvent("down", ondown); 780 _onup(); 781 } 782 }, 783 784 _securityDoor = new SecurityDoor(null, _onchange), 785 originPoint = new Vector2(), 786 minPoint = new Vector2(), 787 maxPoint = new Vector2(), 788 sphere = new Sphere(), 789 790 setDIVBox = function (clientX, clientY, width = 0, height = 0) { 791 div.style.left = clientX + 'px'; 792 div.style.top = clientY + 'px'; 793 div.style.width = width + 'px'; 794 div.style.height = height+ 'px'; 795 }, 796 797 ondown = function (event){ 798 if(event.button === 0) return; 799 if(isDown === true) _onup(); 800 sTime = Date.now(); 801 pointerId = event.pointerId; 802 isDown = true; 803 orbitControls._securityDoor.add("O3D_selectionBoxHelper"); 804 originPoint.set(event.pageX, event.pageY); 805 domElement.parentElement.appendChild(div); 806 eventDispatcher.register("move", onmove); 807 eventDispatcher.register("up", onup); 808 //globalThis.document.body.addEventListener("pointerup", onup); 809 }, 810 811 onmove = function (event){ 812 if(isDown === true && event.pointerId === pointerId){ 813 minPoint.set(Math.min(originPoint.x, event.pageX), Math.min(originPoint.y, event.pageY)); 814 maxPoint.set(Math.max(originPoint.x, event.pageX), Math.max(originPoint.y, event.pageY)); 815 setDIVBox(minPoint.x, minPoint.y, maxPoint.x - minPoint.x, maxPoint.y - minPoint.y); 816 } 817 }, 818 819 push = function (obj3d){ 820 //如果选中列表存在object3D的上级,就解除其上级的选中状态 821 var par = obj3d.parent; 822 while(par !== null){ 823 if(selectObject3ds.includes(par)) return; 824 par = par.parent; 825 } 826 827 selectObject3ds.push(obj3d); 828 }, 829 830 run = function (v){ 831 if(v.geometry !== undefined){ 832 if(v.geometry.boundingSphere === null) v.geometry.computeBoundingSphere(); 833 sphere.radius = v.geometry.boundingSphere.radius; 834 v.getWorldPosition(sphere.center); 835 if(_frustum.intersectsSphere(sphere)) push(v); 836 }else{ 837 v.getWorldPosition(sphere.center); 838 if(_frustum.containsPoint(sphere.center)) push(v); 839 } 840 }, 841 842 onup = event => { 843 if(isDown === false || event.pointerId !== pointerId) return; 844 _onup(); 845 if(Date.now() - sTime < clickDelay) return; 846 startPoint.x = minPoint.x / domElement.offsetWidth * 2 - 1; 847 startPoint.y = -minPoint.y / domElement.offsetHeight * 2 + 1; 848 endPoint.x = maxPoint.x / domElement.offsetWidth * 2 - 1; 849 endPoint.y = -maxPoint.y / domElement.offsetHeight * 2 + 1; 850 this.updateFrustum(); 851 selectObject3ds.length = 0; 852 for(let k = 0, len = obj3ds.length; k < len; k++) obj3ds[k].traverse(run); 853 eventDispatcher.trigger("selectChange"); 854 animateLoop.update(); 855 } 856 857 _helperEnabled = _securityDoor; 858 this.helperEnabled = false; 859 return this; 860 }, 861 862 } 863 }, 864 865 } 866 867 export { O3D }
index.html:
1 <!DOCTYPE html> 2 <html lang = "zh-cmn-Hans"> 3 4 <head> 5 <title>treeStructScene</title> 6 <meta charset = "utf-8" /> 7 <meta name="author" contect="admin, 3247940050@qq.com" /> <!-- 关于作者的联系方式 --> 8 <meta http-equiv="Cache-Control" content="no-siteapp"/> <!-- 不允许百度贴广告 --> 9 <meta name="format-detection" content="telphone=no, email=no"/> <!-- 不允许自动识别电话号码和邮箱号 --> 10 <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <!-- 不允许用户缩放 --> 11 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <!-- 优先使用ie最新版本或谷歌内核 --> 12 <style> 13 *{padding:0; margin:0; color:#fff; font-size:12px;} 14 body{overflow: hidden; background: #000;} 15 </style> 16 <script type = "importmap"> 17 { 18 "imports": { 19 "three": "./build/three.module.js" 20 } 21 } 22 </script> 23 <script type = "text/javascript"> 24 document.oncontextmenu = function (){return false;}//禁用默认的鼠标右键菜单 25 </script> 26 </head> 27 28 <body> 29 30 <script src = "./examples/js/main.js" type = "module"></script> 31 32 </body> 33 34 </html>
main.js:
1 import { UTILS } from './lib/Utils.js'; 2 import { O3D } from './O3D.js'; 3 4 //初始化 O3D 5 document.body.style.overflow = "hidden"; 6 O3D.init(document.body); 7 O3D.resize(innerWidth+"px", innerHeight+"px"); //3D场景的宽高 8 //O3D.domElement.style.position = "absolute"; 9 //O3D.domElement.style.left = "50px"; 10 O3D.initGridHelper(); //地面辅助网格 11 O3D.initBoxHelper(); //拾取辅助框 12 13 //随机位置添加box网格到场景 14 for(let i = 0, helloBox; i < 10; i++){ 15 helloBox = O3D.helloBox(); 16 helloBox.position.x = UTILS.random(-10, 10); 17 helloBox.position.z = UTILS.random(-10, 10); 18 } 19 O3D.animateLoop.update(); //更新一次场景 20 21 //点拾取对象 (鼠标左键) 22 O3D.initSelection(); 23 24 //矩形框拾取对象 (鼠标右键) 25 const selectionBox = O3D.initSelectionBox().initHelper(); 26 selectionBox.helperEnabled = true; //启用矩形框拾取对象 (当它启用时, 轨道控制器的鼠标右键会失效) 27 28 addEventListener("resize", () => O3D.resize(innerWidth+"px", innerHeight+"px")); 29 30 //测试 selectChange 事件 (拾取队列发送改变时触发) 31 O3D.addEvent("selectChange", event => console.log(event.targets));
Three.js版本: 140dev
结果图:
innerWidth