diff --git a/modules/save.js b/modules/save.js
index 4066532e..138928e1 100644
--- a/modules/save.js
+++ b/modules/save.js
@@ -85,6 +85,7 @@ async function getMapURL(type, options=[]) {
if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove();
if (options.includes("globe")) clone.select("#scaleBar").remove();
if (options.includes("noLabels")) {
+ clone.select("#labels #states").remove();
clone.select("#labels #burgLabels").remove();
clone.select("#icons #burgIcons").remove();
}
diff --git a/modules/ui/3d.js b/modules/ui/3d.js
index 2ca03d3a..c80a63cc 100644
--- a/modules/ui/3d.js
+++ b/modules/ui/3d.js
@@ -12,7 +12,8 @@ const options = {scale: 50, lightness: .7, shadow: .5, sun: {x: 100, y: 600, z:
let Renderer, scene, camera, controls, animationFrame, material, texture,
geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh,
objexporter;
-let drawCtx = document.createElement('canvas').getContext('2d');
+const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+document.body.appendChild(drawSVG);
let textMeshs = [], iconMeshs = [];
// initiate 3d scene
@@ -44,17 +45,7 @@ const stop = function() {
material.dispose();
if (waterPlane) waterPlane.dispose();
if (waterMaterial) waterMaterial.dispose();
- for (const mesh of textMeshs) {
- mesh.material.map.dispose();
- mesh.material.dispose();
- mesh.geometry.dispose();
- scene.remove(mesh);
- }
- for (const mesh of iconMeshs) {
- mesh.material.dispose();
- mesh.geometry.dispose();
- scene.remove(mesh);
- }
+ deleteLabels();
Renderer.renderLists.dispose(); // is it required?
Renderer.dispose();
@@ -84,7 +75,6 @@ const setScale = function(scale) {
}
for (const mesh of iconMeshs) {
mesh.position.y = getMeshHeight(findGridCell(mesh.base_x, mesh.base_y));
-
}
geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i));
geometry.verticesNeedUpdate = true;
@@ -125,8 +115,10 @@ const toggleSky = function() {
}
const toggleLabels = function() {
+ if (options.labels3d) deleteLabels(); else createLabels();
+
options.labels3d = !options.labels3d;
- redraw();
+ update();
}
const setColors = function(sky, water) {
@@ -198,32 +190,149 @@ async function newMesh(canvas) {
return true;
}
-function createTextMesh(text, font, size, color) {
- drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height);
- drawCtx.font = "50px " + font;
+function svg2base64(svg) {
+ const str_xml = new XMLSerializer().serializeToString(svg);
+ return 'data:image/svg+xml;base64,' + btoa(str_xml);
+}
- drawCtx.canvas.width = drawCtx.measureText(text).width;
- drawCtx.canvas.height = 50 + 5;
- drawCtx.font = "50px " + font;
+function svg2mesh(svg, sx=1, sy=1) {
+ svg.removeAttribute("viewBox");
+ const bbox = svg.getBBox();
+ svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" "));
+ svg.setAttribute("width", bbox.width);
+ svg.setAttribute("height", bbox.height);
- drawCtx.fillStyle = color;
- drawCtx.fillText(text, 0, 50);
-
- // canvas contents will be used for a texture
- const text_texture = new THREE.TextureLoader().load(drawCtx.canvas.toDataURL());
- text_texture.minFilter = THREE.LinearFilter
- text_texture.needsUpdate = true;
-
- const text_material = new THREE.MeshBasicMaterial({map: text_texture/*, side:THREE.DoubleSide*/, depthWrite: false});
- text_material.transparent = true;
+ const texture = new THREE.TextureLoader().load(svg2base64(svg));
+ texture.minFilter = THREE.LinearFilter; // remove `texture has been resized` warning
- const text_mesh = new THREE.Mesh(
- new THREE.PlaneGeometry(drawCtx.canvas.width*(size/100), drawCtx.canvas.height*(size/100)),
- text_material
+ const material = new THREE.MeshBasicMaterial({map: texture, side:THREE.DoubleSide, depthWrite: false});
+ material.transparent = true;
+
+ const mesh = new THREE.Mesh(
+ new THREE.PlaneGeometry(1, 1),
+ material
);
- text_mesh.renderOrder = 1;
+ mesh.scale.x = bbox.width * (sx / 100);
+ mesh.scale.y = bbox.height * (sy / 100);
+ mesh.renderOrder = 1;
- return text_mesh
+ return mesh;
+}
+
+function createStateText(font, size, color, label) {
+ drawSVG.innerHTML = "";
+ drawSVG.appendChild(label.cloneNode(true));
+ drawSVG.children[0].innerHTML = ``;
+ drawSVG.children[0].appendChild(svg.select(label.childNodes[0].href.baseVal).node().cloneNode(true)); // href of path in defs
+ drawSVG.children[1].setAttribute("transform", `scale(${5} ${5})`)
+ drawSVG.children[1].setAttribute('font-family', font);
+ drawSVG.children[1].setAttribute('font-size', size);
+ drawSVG.children[1].setAttribute('fill', color);
+
+ const mesh = svg2mesh(drawSVG, 20, 20);
+ mesh.rotation.set(THREE.Math.degToRad(-90), 0, 0);
+
+ return mesh;
+}
+
+function createBurgText(text, font, size, color) {
+ drawSVG.innerHTML = `${text}
+`;
+ return svg2mesh(drawSVG, 7*2, 7*2);
+}
+
+function get3dCoords(base_x, base_y) {
+ const x = base_x - svg.attr("width")/2;
+ const y = getMeshHeight(findGridCell(base_x, base_y)); // work better than getMeshHeight(burg.cell) but I don't know why
+ const z = base_y - svg.attr("height")/2;
+ return [x, y, z];
+}
+
+function createLabels() {
+ // Burg labels
+ const cities = svg.select("#viewbox #labels #burgLabels #cities");
+ const towns = svg.select('#viewbox #labels #burgLabels #towns');
+ const cities_icons = svg.select('#viewbox #icons #burgIcons #cities');
+ const towns_icons = svg.select('#viewbox #icons #burgIcons #towns');
+
+ for (const burg of pack.burgs) {
+ const [x, y, z] = get3dCoords(burg.x, burg.y)
+
+ if(layerIsOn("toggleLabels")) {
+ if (burg.capital) {
+ var text_mesh = createBurgText(burg.name, cities.attr('font-family'), cities.attr('font-size'), cities.attr('fill')); // cities.attr('font-size')
+ } else {
+ var text_mesh = createBurgText(burg.name, towns.attr('font-family'), towns.attr('font-size'), towns.attr('fill')); // towns.attr('font-size')
+ }
+
+ if (burg.capital) {
+ text_mesh.position.set(x, y + 15, z);
+ text_mesh.base_height = 15;
+ text_mesh.animate = function () {
+ this.rotation.copy(camera.rotation);
+ }
+ } else {
+ text_mesh.position.set(x, y + 5, z);
+ text_mesh.base_height = 5;
+ text_mesh.animate = function () {
+ if(this.position.distanceTo(camera.position) > 200) {
+ this.visible = false;
+ } else {
+ this.visible = true;
+ this.rotation.copy(camera.rotation);
+ }
+ }
+ }
+
+ text_mesh.base_x = burg.x;
+ text_mesh.base_y = burg.y;
+
+ textMeshs.push(text_mesh);
+ scene.add(text_mesh);
+ }
+
+ // Icon
+ if(layerIsOn("toggleIcons")) {
+ const icon_material = new THREE.MeshBasicMaterial({color: burg.capital ? cities_icons.attr('fill') : towns_icons.attr('fill')});
+ const icon_mesh = new THREE.Mesh(
+ new THREE.SphereGeometry((burg.capital ? cities_icons.attr("size") : towns_icons.attr("size")) * 2, 16, 16),
+ icon_material
+ );
+ icon_mesh.position.set(x, y, z)
+ icon_mesh.base_x = burg.x
+ icon_mesh.base_y = burg.y
+
+ iconMeshs.push(icon_mesh);
+ scene.add(icon_mesh);
+ }
+ }
+
+ // State labels
+ const state_labels = svg.select("#viewbox #labels #states")
+ for (const label of state_labels.node().children) {
+ const text_mesh = createStateText(state_labels.attr("font-family"), state_labels.attr("font-size"), state_labels.attr("fill"), label);
+ const id = label.id.match(/\d+$/);
+ const pos = pack.states[id].pole
+ const [x, y, z] = get3dCoords(pos[0], pos[1])
+ text_mesh.position.set(x, y + 25, z);
+ textMeshs.push(text_mesh)
+ scene.add(text_mesh);
+ }
+}
+
+function deleteLabels() {
+ for (const mesh of textMeshs) {
+ mesh.material.map.dispose();
+ mesh.material.dispose();
+ mesh.geometry.dispose();
+ scene.remove(mesh);
+ }
+
+ for (const mesh of iconMeshs) {
+ mesh.material.dispose();
+ mesh.geometry.dispose();
+ scene.remove(mesh);
+ }
}
// create a mesh from pixel data
@@ -255,79 +364,12 @@ async function createMesh(width, height, segmentsX, segmentsY) {
mesh.receiveShadow = true;
scene.add(mesh);
- for (const mesh of textMeshs) {
- mesh.material.map.dispose();
- mesh.material.dispose();
- mesh.geometry.dispose();
- scene.remove(mesh);
- }
- textMeshs = []
-
- for (const mesh of iconMeshs) {
- mesh.material.dispose();
- mesh.geometry.dispose();
- scene.remove(mesh);
- }
- iconMeshs = []
+ deleteLabels();
+ textMeshs = [];
+ iconMeshs = [];
if (options.labels3d) {
- const cities = svg.select("#viewbox #labels #burgLabels #cities");
- const towns = svg.select('#viewbox #labels #burgLabels #towns');
- const cities_icons = svg.select('#viewbox #icons #burgIcons #cities');
- const towns_icons = svg.select('#viewbox #icons #burgIcons #towns');
-
- for (const burg of pack.burgs) {
- const x = burg.x - svg.attr("width")/2;
- const y = getMeshHeight(findGridCell(burg.x, burg.y)); // work better than getMeshHeight(burg.cell) but I don't know why
- const z = burg.y - svg.attr("height")/2;
-
- if(layerIsOn("toggleLabels")) {
- if (burg.capital) {
- var text_mesh = createTextMesh(burg.name, cities.attr('font-family'), 25, cities.attr('fill')); // cities.data('size')
- } else {
- var text_mesh = createTextMesh(burg.name, towns.attr('font-family'), 7, towns.attr('fill')); // towns.data('size')
- }
-
- if (burg.capital) {
- text_mesh.position.set(x, y + 15, z);
- text_mesh.base_height = 15
- text_mesh.animate = function () {
- this.rotation.copy(camera.rotation);
- }
- } else {
- text_mesh.position.set(x, y + 5, z);
- text_mesh.base_height = 5
- text_mesh.animate = function () {
- if(this.position.distanceTo(camera.position) > 200) {
- this.visible = false;
- } else {
- this.visible = true;
- this.rotation.copy(camera.rotation);
- }
- }
- }
- text_mesh.base_x = burg.x
- text_mesh.base_y = burg.y
-
- textMeshs.push(text_mesh);
- scene.add(text_mesh);
- }
-
- // Icon
- if(layerIsOn("toggleIcons")) {
- const icon_material = new THREE.MeshBasicMaterial({color: burg.capital ? cities_icons.attr('fill') : towns_icons.attr('fill')});
- const icon_mesh = new THREE.Mesh(
- new THREE.SphereGeometry((burg.capital ? cities_icons.attr("size") : towns_icons.attr("size")) * 2, 16, 16),
- icon_material
- );
- icon_mesh.position.set(x, y, z)
- icon_mesh.base_x = burg.x
- icon_mesh.base_y = burg.y
-
- iconMeshs.push(icon_mesh);
- scene.add(icon_mesh);
- }
- }
+ createLabels();
}
}