diff --git a/icons.css b/icons.css
index 008faebc..ae40ee00 100644
--- a/icons.css
+++ b/icons.css
@@ -286,4 +286,5 @@
.icon-button-siege:before {content:'🏰'; padding-right: .4em;}
.icon-button-ambush:before {content:'🌳'; padding-right: .4em;}
.icon-button-landing:before {content:'⚓'; padding-right: .4em;}
-.icon-button-air:before {content:'💨'; padding-right: .4em;}
\ No newline at end of file
+.icon-button-air:before {content:'💨'; padding-right: .4em;}
+.icon-button-screenshot:before {content:'🖥️'; padding-right: .4em;}
diff --git a/index.html b/index.html
index 602c922c..50d83aab 100644
--- a/index.html
+++ b/index.html
@@ -3358,7 +3358,8 @@
-
+
+
diff --git a/libs/objexporter.min.js b/libs/objexporter.min.js
new file mode 100644
index 00000000..fe5b5289
--- /dev/null
+++ b/libs/objexporter.min.js
@@ -0,0 +1,5 @@
+THREE.OBJExporter=function(){};
+THREE.OBJExporter.prototype={constructor:THREE.OBJExporter,parse:function(A){var f="",n=0,w=0,x=0,g=new THREE.Vector3,p=new THREE.Vector3,u=new THREE.Vector2,a,b,y,c,k,v=[];A.traverse(function(e){if(e instanceof THREE.Mesh){var r=0,t=0,d=0,h=e.geometry,z=new THREE.Matrix3;h instanceof THREE.Geometry&&(h=(new THREE.BufferGeometry).setFromObject(e));if(h instanceof THREE.BufferGeometry){var q=h.getAttribute("position"),l=h.getAttribute("normal"),m=h.getAttribute("uv");h=h.getIndex();f+="o "+e.name+
+"\n";e.material&&e.material.name&&(f+="usemtl "+e.material.name+"\n");if(void 0!==q)for(a=0,c=q.count;ak;k++)b=h.getX(a+k)+1,v[k]=n+b+(l||m?"/"+(m?w+b:"")+(l?"/"+(x+b):""):"");f+="f "+v.join(" ")+"\n"}else for(a=0,c=q.count;ak;k++)b=a+k+1,v[k]=n+b+(l||m?"/"+(m?w+b:"")+(l?"/"+(x+b):""):"");f+="f "+v.join(" ")+"\n"}}else console.warn("THREE.OBJExporter.parseMesh(): geometry type unsupported",h);n+=r;w+=d;x+=t}if(e instanceof THREE.Line){r=0;d=e.geometry;t=e.type;d instanceof THREE.Geometry&&(d=(new THREE.BufferGeometry).setFromObject(e));if(d instanceof
+THREE.BufferGeometry){d=d.getAttribute("position");f+="o "+e.name+"\n";if(void 0!==d)for(a=0,c=d.count;a window.URL.revokeObjectURL(URL), 5000);
}
+const saveOBJ = async function() {
+ downloadFile(await getOBJ(), getFileName() + ".obj", "text/plain;charset=UTF-8");
+}
+
// start 3d view and heightmap edit preview
async function newMesh(canvas) {
const loaded = await loadTHREE();
@@ -289,6 +294,13 @@ async function updateGlobeTexure(addMesh) {
img2.src = await getMapURL("mesh", "globe");;
}
+async function getOBJ() {
+ objexporter = await OBJExporter();
+
+ const data = await objexporter.parse(mesh);
+ return data;
+}
+
function addGlobe3dMesh() {
geometry = new THREE.SphereBufferGeometry(1, 64, 64);
mesh = new THREE.Mesh(geometry, material);
@@ -332,6 +344,18 @@ function OrbitControls(camera, domElement) {
});
}
-return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleSky, setResolution, setColors, saveScreenshot};
+function OBJExporter() {
+ if (THREE.OBJExporter) return new THREE.OBJExporter();
+
+ return new Promise(resolve => {
+ const script = document.createElement('script');
+ script.src = "libs/objexporter.min.js"
+ document.head.append(script);
+ script.onload = () => resolve(new THREE.OBJExporter());
+ script.onerror = () => resolve(false);
+ });
+}
+
+return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ};
})));
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 45488121..8d7bb481 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -707,6 +707,7 @@ function toggle3dOptions() {
document.getElementById("options3dUpdate").addEventListener("click", ThreeD.update);
document.getElementById("options3dSave").addEventListener("click", ThreeD.saveScreenshot);
+ document.getElementById("options3dOBJSave").addEventListener("click", ThreeD.saveOBJ);
document.getElementById("options3dScaleRange").addEventListener("input", changeHeightScale);
document.getElementById("options3dScaleNumber").addEventListener("change", changeHeightScale);