From 34b880e10ff671afb33b9deafd31d27e974a0a29 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 28 Nov 2025 15:11:11 +0100 Subject: [PATCH] fix: bug fixes, 3d mode controls change to MapContol --- index.html | 327 ++++++++++++++++++-------- libs/mapControls.min.js | 1 + main.js | 5 +- modules/burgs-generator.js | 40 ++-- modules/dynamic/auto-update.js | 2 +- modules/features.js | 10 +- modules/lakes.js | 11 +- modules/renderers/draw-burg-icons.js | 4 +- modules/renderers/draw-burg-labels.js | 2 +- modules/resample.js | 20 +- modules/routes-generator.js | 7 +- modules/states-generator.js | 3 +- modules/submap.js | 3 +- modules/ui/3d.js | 124 +++++++--- modules/ui/burg-group-editor.js | 2 +- modules/ui/heightmap-editor.js | 10 +- modules/ui/options.js | 3 +- modules/ui/tools.js | 3 +- modules/ui/world-configurator.js | 3 +- 19 files changed, 398 insertions(+), 182 deletions(-) create mode 100644 libs/mapControls.min.js diff --git a/index.html b/index.html index 05dc25ff..fec43b80 100644 --- a/index.html +++ b/index.html @@ -1092,7 +1092,7 @@ - + Stroke linejoin - + + @@ -7873,7 +7887,9 @@ - + @@ -7892,120 +7908,194 @@ - + - + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - + + + - - - - + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -8013,9 +8103,17 @@ - - - + + + @@ -8023,35 +8121,74 @@ - - - - - + + + + + - - - - - + + + + + - - - - - - + + + + + + - + diff --git a/libs/mapControls.min.js b/libs/mapControls.min.js new file mode 100644 index 00000000..ba07d42e --- /dev/null +++ b/libs/mapControls.min.js @@ -0,0 +1 @@ +!function(){let e={type:"change"},t={type:"start"},n={type:"end"};class o extends THREE.EventDispatcher{constructor(o,a){super(),void 0===a&&console.warn('THREE.MapControls: The second parameter "domElement" is now mandatory.'),a===document&&console.error('THREE.MapControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'),this.object=o,this.domElement=a,this.domElement.style.touchAction="none",this.enabled=!0,this.target=new THREE.Vector3,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.minAzimuthAngle=-1/0,this.maxAzimuthAngle=1/0,this.enableDamping=!1,this.dampingFactor=.05,this.enableZoom=!0,this.zoomSpeed=1,this.enableRotate=!0,this.rotateSpeed=1,this.enablePan=!0,this.panSpeed=1,this.screenSpacePanning=!1,this.keyPanSpeed=7,this.autoRotate=!1,this.autoRotateSpeed=2,this.keys={LEFT:"ArrowLeft",UP:"ArrowUp",RIGHT:"ArrowRight",BOTTOM:"ArrowDown"},this.mouseButtons={LEFT:THREE.MOUSE.PAN,MIDDLE:THREE.MOUSE.DOLLY,RIGHT:THREE.MOUSE.ROTATE},this.touches={ONE:THREE.TOUCH.PAN,TWO:THREE.TOUCH.DOLLY_ROTATE},this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.zoom0=this.object.zoom,this._domElementKeyEvents=null,this.getPolarAngle=function(){return l.phi},this.getAzimuthalAngle=function(){return l.theta},this.getDistance=function(){return this.object.position.distanceTo(this.target)},this.listenToKeyEvents=function(e){e.addEventListener("keydown",K),this._domElementKeyEvents=e},this.saveState=function(){i.target0.copy(i.target),i.position0.copy(i.object.position),i.zoom0=i.object.zoom},this.reset=function(){i.target.copy(i.target0),i.object.position.copy(i.position0),i.object.zoom=i.zoom0,i.object.updateProjectionMatrix(),i.dispatchEvent(e),i.update(),s=r.NONE},this.update=function(){let t=new THREE.Vector3,n=(new THREE.Quaternion).setFromUnitVectors(o.up,new THREE.Vector3(0,1,0)),a=n.clone().invert(),h=new THREE.Vector3,d=new THREE.Quaternion,b=2*Math.PI;return function(){let o=i.object.position;t.copy(o).sub(i.target),t.applyQuaternion(n),l.setFromVector3(t),i.autoRotate&&s===r.NONE&&A(2*Math.PI/60/60*i.autoRotateSpeed),i.enableDamping?(l.theta+=p.theta*i.dampingFactor,l.phi+=p.phi*i.dampingFactor):(l.theta+=p.theta,l.phi+=p.phi);let T=i.minAzimuthAngle,g=i.maxAzimuthAngle;return isFinite(T)&&isFinite(g)&&(T<-Math.PI?T+=b:T>Math.PI&&(T-=b),g<-Math.PI?g+=b:g>Math.PI&&(g-=b),l.theta=T<=g?Math.max(T,Math.min(g,l.theta)):l.theta>(T+g)/2?Math.max(T,l.theta):Math.min(g,l.theta)),l.phi=Math.max(i.minPolarAngle,Math.min(i.maxPolarAngle,l.phi)),l.makeSafe(),l.radius*=m,l.radius=Math.max(i.minDistance,Math.min(i.maxDistance,l.radius)),!0===i.enableDamping?i.target.addScaledVector(E,i.dampingFactor):i.target.add(E),t.setFromSpherical(l),t.applyQuaternion(a),o.copy(i.target).add(t),i.object.lookAt(i.target),!0===i.enableDamping?(p.theta*=1-i.dampingFactor,p.phi*=1-i.dampingFactor,E.multiplyScalar(1-i.dampingFactor)):(p.set(0,0,0),E.set(0,0,0)),m=1,!!(u||h.distanceToSquared(i.object.position)>c||8*(1-d.dot(i.object.quaternion))>c)&&(i.dispatchEvent(e),h.copy(i.object.position),d.copy(i.object.quaternion),u=!1,!0)}}(),this.dispose=function(){i.domElement.removeEventListener("contextmenu",B),i.domElement.removeEventListener("pointerdown",_),i.domElement.removeEventListener("pointercancel",X),i.domElement.removeEventListener("wheel",Z),i.domElement.removeEventListener("pointermove",z),i.domElement.removeEventListener("pointerup",F),null!==i._domElementKeyEvents&&i._domElementKeyEvents.removeEventListener("keydown",K)};let i=this,r={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_PAN:4,TOUCH_DOLLY_PAN:5,TOUCH_DOLLY_ROTATE:6},s=r.NONE,c=1e-6,l=new THREE.Spherical,p=new THREE.Spherical,m=1,E=new THREE.Vector3,u=!1,h=new THREE.Vector2,d=new THREE.Vector2,b=new THREE.Vector2,T=new THREE.Vector2,g=new THREE.Vector2,O=new THREE.Vector2,f=new THREE.Vector2,R=new THREE.Vector2,H=new THREE.Vector2,y=[],P={};function v(){return Math.pow(.95,i.zoomSpeed)}function A(e){p.theta-=e}function L(e){p.phi-=e}let M=function(){let e=new THREE.Vector3;return function(t,n){e.setFromMatrixColumn(n,0),e.multiplyScalar(-t),E.add(e)}}(),N=function(){let e=new THREE.Vector3;return function(t,n){!0===i.screenSpacePanning?e.setFromMatrixColumn(n,1):(e.setFromMatrixColumn(n,0),e.crossVectors(i.object.up,e)),e.multiplyScalar(t),E.add(e)}}(),w=function(){let e=new THREE.Vector3;return function(t,n){let o=i.domElement;if(i.object.isPerspectiveCamera){let a=i.object.position;e.copy(a).sub(i.target);let r=e.length();M(2*t*(r*=Math.tan(i.object.fov/2*Math.PI/180))/o.clientHeight,i.object.matrix),N(2*n*r/o.clientHeight,i.object.matrix)}else i.object.isOrthographicCamera?(M(t*(i.object.right-i.object.left)/i.object.zoom/o.clientWidth,i.object.matrix),N(n*(i.object.top-i.object.bottom)/i.object.zoom/o.clientHeight,i.object.matrix)):(console.warn("WARNING: MapControls.js encountered an unknown camera type - pan disabled."),i.enablePan=!1)}}();function j(e){i.object.isPerspectiveCamera?m/=e:i.object.isOrthographicCamera?(i.object.zoom=Math.max(i.minZoom,Math.min(i.maxZoom,i.object.zoom*e)),i.object.updateProjectionMatrix(),u=!0):(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),i.enableZoom=!1)}function S(e){i.object.isPerspectiveCamera?m*=e:i.object.isOrthographicCamera?(i.object.zoom=Math.max(i.minZoom,Math.min(i.maxZoom,i.object.zoom/e)),i.object.updateProjectionMatrix(),u=!0):(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),i.enableZoom=!1)}function k(e){h.set(e.clientX,e.clientY)}function C(e){T.set(e.clientX,e.clientY)}function Y(){if(1===y.length)h.set(y[0].pageX,y[0].pageY);else{let e=.5*(y[0].pageX+y[1].pageX),t=.5*(y[0].pageY+y[1].pageY);h.set(e,t)}}function x(){if(1===y.length)T.set(y[0].pageX,y[0].pageY);else{let e=.5*(y[0].pageX+y[1].pageX),t=.5*(y[0].pageY+y[1].pageY);T.set(e,t)}}function D(){let e=y[0].pageX-y[1].pageX,t=y[0].pageY-y[1].pageY;f.set(0,Math.sqrt(e*e+t*t))}function I(e){if(1==y.length)d.set(e.pageX,e.pageY);else{let t=q(e),n=.5*(e.pageX+t.x),o=.5*(e.pageY+t.y);d.set(n,o)}b.subVectors(d,h).multiplyScalar(i.rotateSpeed);let t=i.domElement;A(2*Math.PI*b.x/t.clientHeight),L(2*Math.PI*b.y/t.clientHeight),h.copy(d)}function U(e){if(1===y.length)g.set(e.pageX,e.pageY);else{let t=q(e),n=.5*(e.pageX+t.x),o=.5*(e.pageY+t.y);g.set(n,o)}O.subVectors(g,T).multiplyScalar(i.panSpeed),w(O.x,O.y),T.copy(g)}function V(e){let t=q(e),n=e.pageX-t.x,o=e.pageY-t.y;R.set(0,Math.sqrt(n*n+o*o)),H.set(0,Math.pow(R.y/f.y,i.zoomSpeed)),j(H.y),f.copy(R)}function _(e){var n;!1!==i.enabled&&(0===y.length&&(i.domElement.setPointerCapture(e.pointerId),i.domElement.addEventListener("pointermove",z),i.domElement.addEventListener("pointerup",F)),n=e,y.push(n),"touch"===e.pointerType?function(e){switch(W(e),y.length){case 1:switch(i.touches.ONE){case THREE.TOUCH.ROTATE:if(!1===i.enableRotate)return;Y(),s=r.TOUCH_ROTATE;break;case THREE.TOUCH.PAN:if(!1===i.enablePan)return;x(),s=r.TOUCH_PAN;break;default:s=r.NONE}break;case 2:switch(i.touches.TWO){case THREE.TOUCH.DOLLY_PAN:if(!1===i.enableZoom&&!1===i.enablePan)return;i.enableZoom&&D(),i.enablePan&&x(),s=r.TOUCH_DOLLY_PAN;break;case THREE.TOUCH.DOLLY_ROTATE:if(!1===i.enableZoom&&!1===i.enableRotate)return;i.enableZoom&&D(),i.enableRotate&&Y(),s=r.TOUCH_DOLLY_ROTATE;break;default:s=r.NONE}break;default:s=r.NONE}s!==r.NONE&&i.dispatchEvent(t)}(e):function(e){let n;switch(e.button){case 0:n=i.mouseButtons.LEFT;break;case 1:n=i.mouseButtons.MIDDLE;break;case 2:n=i.mouseButtons.RIGHT;break;default:n=-1}switch(n){case THREE.MOUSE.DOLLY:var o;if(!1===i.enableZoom)return;o=e,f.set(o.clientX,o.clientY),s=r.DOLLY;break;case THREE.MOUSE.ROTATE:if(e.ctrlKey||e.metaKey||e.shiftKey){if(!1===i.enablePan)return;C(e),s=r.PAN}else{if(!1===i.enableRotate)return;k(e),s=r.ROTATE}break;case THREE.MOUSE.PAN:if(e.ctrlKey||e.metaKey||e.shiftKey){if(!1===i.enableRotate)return;k(e),s=r.ROTATE}else{if(!1===i.enablePan)return;C(e),s=r.PAN}break;default:s=r.NONE}s!==r.NONE&&i.dispatchEvent(t)}(e))}function z(e){!1!==i.enabled&&("touch"===e.pointerType?function(e){var t,n;switch(W(e),s){case r.TOUCH_ROTATE:if(!1===i.enableRotate)return;I(e),i.update();break;case r.TOUCH_PAN:if(!1===i.enablePan)return;U(e),i.update();break;case r.TOUCH_DOLLY_PAN:if(!1===i.enableZoom&&!1===i.enablePan)return;t=e,i.enableZoom&&V(t),i.enablePan&&U(t),i.update();break;case r.TOUCH_DOLLY_ROTATE:if(!1===i.enableZoom&&!1===i.enableRotate)return;n=e,i.enableZoom&&V(n),i.enableRotate&&I(n),i.update();break;default:s=r.NONE}}(e):function(e){var t,n;if(!1!==i.enabled)switch(s){case r.ROTATE:if(!1===i.enableRotate)return;!function(e){d.set(e.clientX,e.clientY),b.subVectors(d,h).multiplyScalar(i.rotateSpeed);let t=i.domElement;A(2*Math.PI*b.x/t.clientHeight),L(2*Math.PI*b.y/t.clientHeight),h.copy(d),i.update()}(e);break;case r.DOLLY:if(!1===i.enableZoom)return;t=e,R.set(t.clientX,t.clientY),H.subVectors(R,f),H.y>0?j(v()):H.y<0&&S(v()),f.copy(R),i.update();break;case r.PAN:if(!1===i.enablePan)return;n=e,g.set(n.clientX,n.clientY),O.subVectors(g,T).multiplyScalar(i.panSpeed),w(O.x,O.y),T.copy(g),i.update()}}(e))}function F(e){G(e),0===y.length&&(i.domElement.releasePointerCapture(e.pointerId),i.domElement.removeEventListener("pointermove",z),i.domElement.removeEventListener("pointerup",F)),i.dispatchEvent(n),s=r.NONE}function X(e){G(e)}function Z(e){var o;!1!==i.enabled&&!1!==i.enableZoom&&s===r.NONE&&(e.preventDefault(),i.dispatchEvent(t),(o=e).deltaY<0?S(v()):o.deltaY>0&&j(v()),i.update(),i.dispatchEvent(n))}function K(e){!1!==i.enabled&&!1!==i.enablePan&&function(e){let t=!1;switch(e.code){case i.keys.UP:w(0,i.keyPanSpeed),t=!0;break;case i.keys.BOTTOM:w(0,-i.keyPanSpeed),t=!0;break;case i.keys.LEFT:w(i.keyPanSpeed,0),t=!0;break;case i.keys.RIGHT:w(-i.keyPanSpeed,0),t=!0}t&&(e.preventDefault(),i.update())}(e)}function B(e){!1!==i.enabled&&e.preventDefault()}function G(e){delete P[e.pointerId];for(let t=0;t { pack.burgs = burgs; TIME && console.timeEnd("generateBurgs"); - function getCapitalsNumber() { - let number = +byId("statesNumber").value; - - if (populatedCells.length < number * 10) { - WARN && console.warn(`Not enough populated cells. Generating only ${number} capitals/states`); - number = Math.floor(sorted.length / 10); - } - - return number; - } - - function getTownsNumber() { - const manorsInput = byId("manorsInput"); - const isAuto = manorsInput.value === "1000"; // '1000' is considered as auto - if (isAuto) return rn(populatedCells.length / 5 / (grid.points.length / 10000) ** 0.8); - - return Math.min(manorsInput.valueAsNumber, sorted.length); - } - function generateCapitals() { const randomize = score => score * (0.5 + Math.random() * 0.5); const score = new Int16Array(cells.s.map(randomize)); @@ -110,6 +91,25 @@ window.Burgs = (() => { } } + function getCapitalsNumber() { + let number = +byId("statesNumber").value; + + if (populatedCells.length < number * 10) { + number = Math.floor(populatedCells.length / 10); + WARN && console.warn(`Not enough populated cells. Generating only ${number} capitals/states`); + } + + return number; + } + + function getTownsNumber() { + const manorsInput = byId("manorsInput"); + const isAuto = manorsInput.value === "1000"; // '1000' is considered as auto + if (isAuto) return rn(populatedCells.length / 5 / (grid.points.length / 10000) ** 0.8); + + return Math.min(manorsInput.valueAsNumber, populatedCells.length); + } + // define port status and shift ports and burgs on rivers function shiftBurgs() { const {cells, features} = pack; @@ -321,7 +321,7 @@ window.Burgs = (() => { const defaultGroup = options.burgs.groups.find(g => g.isDefault); if (!defaultGroup) { - ERROR & console.error("No default group defined"); + ERROR && console.error("No default group defined"); return; } burg.group = defaultGroup.name; diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index c45a53e4..124abfb2 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -951,7 +951,7 @@ export function resolveVersionConflicts(mapVersion) { .attr("stroke", null); // pole can be missing for some states/provinces - BurgsAndStates.getPoles(); + States.getPoles(); Provinces.getPoles(); } diff --git a/modules/features.js b/modules/features.js index 9ef785d6..714d4f38 100644 --- a/modules/features.js +++ b/modules/features.js @@ -213,7 +213,7 @@ window.Features = (function () { } // add properties to pack features - function specify() { + function defineGroups() { const gridCellsNumber = grid.cells.i.length; const OCEAN_MIN_SIZE = gridCellsNumber / 25; const SEA_MIN_SIZE = gridCellsNumber / 1000; @@ -223,12 +223,8 @@ window.Features = (function () { for (const feature of pack.features) { if (!feature || feature.type === "ocean") continue; + if (feature.type === "lake") feature.height = Lakes.getHeight(feature); feature.group = defineGroup(feature); - - if (feature.type === "lake") { - feature.height = Lakes.getHeight(feature); - feature.name = Lakes.getName(feature); - } } function defineGroup(feature) { @@ -267,5 +263,5 @@ window.Features = (function () { } } - return {markupGrid, markupPack, specify}; + return {markupGrid, markupPack, defineGroups}; })(); diff --git a/modules/lakes.js b/modules/lakes.js index d95b87a9..8ce18793 100644 --- a/modules/lakes.js +++ b/modules/lakes.js @@ -106,11 +106,18 @@ window.Lakes = (function () { return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2); }; + const defineNames = function () { + pack.features.forEach(feature => { + if (feature.type !== "lake") return; + feature.name = getName(feature); + }); + }; + const getName = function (feature) { - const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20); + const landCell = feature.shoreline[0]; const culture = pack.cells.culture[landCell]; return Names.getCulture(culture); }; - return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName}; + return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName}; })(); diff --git a/modules/renderers/draw-burg-icons.js b/modules/renderers/draw-burg-icons.js index 4ae374e7..5952ec81 100644 --- a/modules/renderers/draw-burg-icons.js +++ b/modules/renderers/draw-burg-icons.js @@ -85,8 +85,8 @@ function createIconGroups() { }); // create groups for each burg group and apply stored or default style - const defaultIconStyle = style.burgIcons.town || Object.values(style.burgIcons)[0]; - const defaultAnchorStyle = style.anchors.town || Object.values(style.anchors)[0]; + const defaultIconStyle = style.burgIcons.town || Object.values(style.burgIcons)[0] || {}; + const defaultAnchorStyle = style.anchors.town || Object.values(style.anchors)[0] || {}; const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order); for (const {name} of sortedGroups) { const burgGroup = burgIcons.append("g"); diff --git a/modules/renderers/draw-burg-labels.js b/modules/renderers/draw-burg-labels.js index 88e61ca3..721025a2 100644 --- a/modules/renderers/draw-burg-labels.js +++ b/modules/renderers/draw-burg-labels.js @@ -68,7 +68,7 @@ function createLabelGroups() { }); // create groups for each burg group and apply stored or default style - const defaultStyle = style.burgLabels.town || Object.values(style.burgLabels)[0]; + const defaultStyle = style.burgLabels.town || Object.values(style.burgLabels)[0] || {}; const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order); for (const {name} of sortedGroups) { const group = burgLabels.append("g"); diff --git a/modules/resample.js b/modules/resample.js index 21349b9e..819214b1 100644 --- a/modules/resample.js +++ b/modules/resample.js @@ -187,11 +187,27 @@ window.Resample = (function () { function getBurgCoordinates(burg, closestCell, cell, xp, yp) { const haven = pack.cells.haven[cell]; - if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven); + if (burg.port && haven) return getCloseToEdgePoint(cell, haven); if (closestCell !== cell) return pack.cells.p[cell]; return [rn(xp, 2), rn(yp, 2)]; } + + function getCloseToEdgePoint(cell1, cell2) { + const {cells, vertices} = pack; + + const [x0, y0] = cells.p[cell1]; + const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2)); + const [x1, y1] = vertices.p[commonVertices[0]]; + const [x2, y2] = vertices.p[commonVertices[1]]; + const xEdge = (x1 + x2) / 2; + const yEdge = (y1 + y2) / 2; + + const x = rn(x0 + 0.95 * (xEdge - x0), 2); + const y = rn(y0 + 0.95 * (yEdge - y0), 2); + + return [x, y]; + } } function restoreStates(parentMap, projection) { @@ -202,7 +218,7 @@ window.Resample = (function () { return {...state, removed: true, lock: false}; }); - BurgsAndStates.getPoles(); + States.getPoles(); const regimentCellsMap = {}; const VERTICAL_GAP = 8; diff --git a/modules/routes-generator.js b/modules/routes-generator.js index 4454b8ff..ab17e5ec 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -360,7 +360,7 @@ window.Routes = (function () { // connect cell with routes system by land function connect(cellId) { const getCost = createCostEvaluator({isWater: false, connections: new Map()}); - const isExit = cellId => isLand(cellId) && isConnected(cellId); + const isExit = c => isLand(c) && isConnected(c); const pathCells = findPath(cellId, isExit, getCost); if (!pathCells) return; @@ -372,9 +372,9 @@ window.Routes = (function () { pack.routes.push(newRoute); for (let i = 0; i < pathCells.length; i++) { - const cellId = pathCells[i]; + const currentCell = pathCells[i]; const nextCellId = pathCells[i + 1]; - if (nextCellId) addConnection(cellId, nextCellId, routeId); + if (nextCellId) addConnection(currentCell, nextCellId, routeId); } return newRoute; @@ -446,6 +446,7 @@ window.Routes = (function () { const connectivity = Object.values(connections).reduce((acc, routeId) => { const route = pack.routes.find(route => route.i === routeId); + if (!route) return acc; const rate = connectivityRateMap[route.group] || connectivityRateMap.default; return acc + rate; }, 0.8); diff --git a/modules/states-generator.js b/modules/states-generator.js index 568f422d..9662e648 100644 --- a/modules/states-generator.js +++ b/modules/states-generator.js @@ -18,11 +18,12 @@ window.States = (() => { function createStates() { const states = [{i: 0, name: "Neutrals"}]; const each5th = each(5); + const sizeVariety = byId("sizeVariety").valueAsNumber; pack.burgs.forEach(burg => { if (!burg.i || !burg.capital) return; - const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1); + const expansionism = rn(Math.random() * sizeVariety + 1, 1); const basename = burg.name.length < 9 && each5th(burg.cell) ? burg.name : Names.getCultureShort(burg.culture); const name = Names.getState(basename, burg.culture); const type = pack.cultures[burg.culture].type; diff --git a/modules/submap.js b/modules/submap.js index 5904b8c7..912daafb 100644 --- a/modules/submap.js +++ b/modules/submap.js @@ -200,6 +200,7 @@ window.Submap = (function () { // it's safe to recalculate. stage("Regenerating Biome"); Biomes.define(); + Features.defineGroups(); // recalculate suitability and population // TODO: normalize according to the base-map rankCells(); @@ -259,7 +260,7 @@ window.Submap = (function () { regenerateRoutes(); Rivers.specify(); - Features.specify(); + Lakes.defineNames(); stage("Porting military"); for (const s of pack.states) { diff --git a/modules/ui/3d.js b/modules/ui/3d.js index f02d7641..fd6b849b 100644 --- a/modules/ui/3d.js +++ b/modules/ui/3d.js @@ -68,10 +68,11 @@ window.ThreeD = (function () { // try to clean the memory as much as possible const stop = function () { + if (controls) controls.dispose(); cancelAnimationFrame(animationFrame); - texture.dispose(); - geometry.dispose(); - material.dispose(); + if (texture) texture.dispose(); + if (geometry) geometry.dispose(); + if (material) material.dispose(); if (waterPlane) waterPlane.dispose(); if (waterMaterial) waterMaterial.dispose(); deleteLabels(); @@ -255,7 +256,6 @@ window.ThreeD = (function () { async function newMesh(canvas) { const loaded = await loadTHREE(); if (!loaded) return tip("Cannot load 3d library", false, "error", 4000); - scene = new THREE.Scene(); // light @@ -267,7 +267,6 @@ window.ThreeD = (function () { spotLight.shadow.mapSize.width = 2048; spotLight.shadow.mapSize.height = 2048; scene.add(spotLight); - // scene.add(new THREE.SpotLightHelper(spotLight)); // Renderer Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true}); @@ -277,22 +276,32 @@ window.ThreeD = (function () { if (options.extendedWater) extendWater(graphWidth, graphHeight); createMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY); - // camera camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 0.1, 2000); - camera.position.set(0, rn(svgWidth / 3.5), 500); + camera.position.set(0, 400, 500); // Set initial camera position for isometric view + controls = await MapControls(camera, canvas); // Google Maps-style navigation + if (!controls) return false; - // controls - controls = await OrbitControls(camera, canvas); - controls.listenToKeyEvents(window); - controls.zoomSpeed = 0.25; + // Set initial target at map center + if (controls.target) controls.target.set(0, 0, 0); - controls.panSpeed = 0.5; - controls.minDistance = 100; + // Configure for bird's eye view with Google Maps-style controls + controls.enableDamping = true; + controls.dampingFactor = 0.05; + controls.screenSpacePanning = false; + controls.minDistance = 50; controls.maxDistance = 1000; - controls.maxPolarAngle = Math.PI / 2; + controls.minZoom = 0.05; + controls.maxZoom = 4; + controls.zoomSpeed = 0.6; + controls.panSpeed = 1.6; + controls.enableRotate = true; + controls.rotateSpeed = 0.5; + controls.maxPolarAngle = Math.PI / 2; // Prevent camera from going below horizon + controls.minPolarAngle = 0; // Allow full 90 degrees top-down view + controls.autoRotate = Boolean(options.rotateMesh); controls.autoRotateSpeed = options.rotateMesh; - if (controls.autoRotate) animate(); + animate(); controls.addEventListener("change", render); return true; @@ -344,10 +353,10 @@ window.ThreeD = (function () { const stateOptions = { font: states.attr("font-family"), - size: +states.attr("data-size"), + size: +states.attr("data-size") / 2, color: states.attr("fill"), elevation: 20, - quality: 20 + quality: 80 }; // Cache icon materials and geometries by group to avoid recreating them @@ -377,7 +386,7 @@ window.ThreeD = (function () { size, color, elevation, - quality: 20, + quality: 40, iconSize, iconColor }; @@ -545,9 +554,20 @@ window.ThreeD = (function () { if (texture) texture.dispose(); if (!options.wireframe) { - texture = new THREE.TextureLoader().load(await createMeshTextureUrl(), render); - texture.needsUpdate = true; - texture.anisotropy = Renderer.capabilities.getMaxAnisotropy(); + const url = await createMeshTextureUrl(); + await new Promise(resolve => { + texture = new THREE.TextureLoader().load( + url, + t => { + resolve(t); + }, + undefined, + () => resolve(null) + ); + }); + if (texture) { + texture.anisotropy = Renderer.capabilities.getMaxAnisotropy(); + } } if (material) material.dispose(); @@ -589,10 +609,11 @@ window.ThreeD = (function () { mesh.castShadow = true; mesh.receiveShadow = true; scene.add(mesh); + render(); if (options.labels3d) { - render(); await createLabels(); + render(); } } @@ -671,18 +692,59 @@ window.ThreeD = (function () { // camera camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.1, 1000).translateZ(5); - // controls - controls = await OrbitControls(camera, Renderer.domElement); + controls = await OrbitControls(camera, Renderer.domElement); // OrbitControls for globe view + if (!controls) return false; controls.zoomSpeed = 0.25; controls.minDistance = 1.5; controls.maxDistance = 10; controls.autoRotate = Boolean(options.rotateGlobe); controls.autoRotateSpeed = options.rotateGlobe; + + // ensure OrbitControls behavior (reset potentially changed defaults by MapControls) + controls.mouseButtons = { + LEFT: THREE.MOUSE.ROTATE, + MIDDLE: THREE.MOUSE.DOLLY, + RIGHT: THREE.MOUSE.PAN + }; + controls.screenSpacePanning = true; + controls.minPolarAngle = 0; + controls.maxPolarAngle = Math.PI; + controls.addEventListener("change", render); return true; } + function OrbitControls(camera, domElement) { + if (THREE.OrbitControls) return new THREE.OrbitControls(camera, domElement); + + return new Promise(resolve => { + const script = document.createElement("script"); + script.src = "libs/orbitControls.min.js"; + document.head.append(script); + script.onload = () => resolve(new THREE.OrbitControls(camera, domElement)); + script.onerror = () => resolve(false); + }); + } + + function MapControls(camera, domElement) { + if (THREE.MapControls) return new THREE.MapControls(camera, domElement); + + return new Promise(resolve => { + const script = document.createElement("script"); + script.src = "libs/mapControls.min.js"; + document.head.append(script); + script.onload = () => { + if (THREE.MapControls) { + resolve(new THREE.MapControls(camera, domElement)); + } else { + resolve(false); + } + }; + script.onerror = () => resolve(false); + }); + } + async function updateGlobeTexure(addMesh) { const world = mapCoordinates.latT > 179; // define if map covers whole world @@ -751,7 +813,7 @@ window.ThreeD = (function () { // animate 3d scene and camera function animate() { animationFrame = requestAnimationFrame(animate); - controls.update(); + if (controls && controls.update) controls.update(); } function loadTHREE() { @@ -778,18 +840,6 @@ window.ThreeD = (function () { }); } - function OrbitControls(camera, domElement) { - if (THREE.OrbitControls) return new THREE.OrbitControls(camera, domElement); - - return new Promise(resolve => { - const script = document.createElement("script"); - script.src = "libs/orbitControls.min.js"; - document.head.append(script); - script.onload = () => resolve(new THREE.OrbitControls(camera, domElement)); - script.onerror = () => resolve(false); - }); - } - function OBJExporter() { if (THREE.OBJExporter) return new THREE.OBJExporter(); diff --git a/modules/ui/burg-group-editor.js b/modules/ui/burg-group-editor.js index 88edd591..09efc130 100644 --- a/modules/ui/burg-group-editor.js +++ b/modules/ui/burg-group-editor.js @@ -66,7 +66,7 @@ function editBurgGroups() { - + diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index f49f9a73..d655e39d 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -238,6 +238,7 @@ function editHeightmap(options) { } Biomes.define(); + Features.defineGroups(); rankCells(); Cultures.generate(); @@ -256,7 +257,7 @@ function editHeightmap(options) { Provinces.getPoles(); Rivers.specify(); - Features.specify(); + Lakes.defineNames(); Military.generate(); Markers.generate(); @@ -345,7 +346,10 @@ function editHeightmap(options) { reGraph(); Features.markupPack(); - if (erosionAllowed) Rivers.generate(true); + if (erosionAllowed) { + Rivers.generate(true); + Features.defineGroups(); + } // assign saved pack data from grid back to pack const n = pack.cells.i.length; @@ -439,7 +443,7 @@ function editHeightmap(options) { if (erosionAllowed) { Rivers.specify(); - Features.specify(); + Lakes.defineNames(); } // restore zones from grid diff --git a/modules/ui/options.js b/modules/ui/options.js index 9f00eb74..07d77cb0 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -987,8 +987,7 @@ async function enter3dView(type) { canvas.style.display = "block"; canvas.onmouseenter = () => { - const help = - "Left mouse to change angle, middle mouse. Mousewheel to zoom. Right mouse or hold Shift to pan. O to toggle options"; + const help = "Drag to pan • Scroll to zoom • Right-click drag to rotate • O to toggle options"; +canvas.dataset.hovered > 2 ? tip("") : tip(help); canvas.dataset.hovered = (+canvas.dataset.hovered | 0) + 1; }; diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 5a170026..d89182e0 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -129,7 +129,8 @@ function regenerateRoutes() { function regenerateRivers() { Rivers.generate(); Rivers.specify(); - Features.specify(); + Features.defineGroups(); + Lakes.defineNames(); if (layerIsOn("toggleRivers")) drawRivers(); } diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js index 0b5abab0..b3c6da39 100644 --- a/modules/ui/world-configurator.js +++ b/modules/ui/world-configurator.js @@ -89,7 +89,8 @@ function editWorld() { Rivers.specify(); pack.cells.h = new Float32Array(heights); Biomes.define(); - Features.specify(); + Features.defineGroups(); + Lakes.defineNames(); if (layerIsOn("toggleTemperature")) drawTemperature(); if (layerIsOn("togglePrecipitation")) drawPrecipitation();