$ git clone http://thingshare.ion.nu/thingshare.git
commit f434929efcecfdb5944d62e742ad93ace2ea4663
Author: Alicia <...>
Date:   Mon Mar 23 21:03:13 2020 +0100

    Added a 3D view for 3D model files.

diff --git a/3dview.js b/3dview.js
new file mode 100644
index 0000000..9dc0608
--- /dev/null
+++ b/3dview.js
@@ -0,0 +1,32 @@
+/*
+    This file is part of Thingshare, a federated system for sharing data for home manufacturing (e.g. 3D models to 3D print)
+    https://thingshare.ion.nu/
+    Copyright (C) 2020  Alicia <...>
+
+    See /COPYING for license text (AGPLv3+)
+*/
+function threedview(link,w,h)
+{
+  div=link.parentElement.parentElement;
+  // Toggle view
+  var x3d=div.getElementsByTagName('x3d');
+  if(x3d.length>0)
+  {
+    link.textContent='3D view';
+    // Remove 3D view
+    for(var i=0; i<x3d.length; ++i){x3d[i].remove();}
+    // Restore 2D view
+    var img=div.getElementsByTagName('img');
+    for(var i=0; i<img.length; ++i){img[i].style.display='';}
+    return false;
+  }else{
+    link.textContent='2D view';
+  }
+  // Hide 2D view
+  var img=div.getElementsByTagName('img');
+  for(var i=0; i<img.length; ++i){img[i].style.display='none';}
+  // Add 3D preview (tried to do this with the DOM first, but x3dom wouldn't pick it up)
+  div.innerHTML+='<x3d width="'+w+'px" height="'+h+'px"><scene><inline url="'+link.dataset.file+'"></inline></scene></x3d>';
+  x3dom.reload(); // Load the new addition
+  return false;
+}
diff --git a/Dependencies b/Dependencies
index 8e5d611..589e08e 100644
--- a/Dependencies
+++ b/Dependencies
@@ -1,6 +1,7 @@
-Site dependencies (clone directly to web directory)
+Site dependencies (clone/download directly to web directory)
 Parsedown (git clone https://github.com/erusev/parsedown)
 Mdjs (git clone https://github.com/hangxingliu/mdjs)
+x3dom (wget https://x3dom.org/download/1.8.1/x3dom.{debug.js,css})
 
 Web dependencies:
 PHP
diff --git a/files.php b/files.php
index c377f9b..cb4091b 100644
--- a/files.php
+++ b/files.php
@@ -43,4 +43,10 @@ function getpreview($name, $hash)
   if(file_exists('icons/'.$filetype.'.png')){return BASEURL.'/icons/'.$filetype.'.png';}
   return BASEURL.'/icons/default.svg';
 }
+function get3dpreview($name, $hash)
+{
+  $file=getfilepath($hash, true);
+  if(file_exists($file.'.x3d')){return BASEURL.'/'.$file.'.x3d';}
+  return false;
+}
 ?>
diff --git a/genpreviews.php b/genpreviews.php
index 5c8c713..1fa452f 100755
--- a/genpreviews.php
+++ b/genpreviews.php
@@ -27,13 +27,14 @@ if(!flock($lock, LOCK_EX|LOCK_NB)){die("Lock failed");}
 include_once('db.php');
 include_once('config.php');
 include_once('files.php');
-mkdir(TMPDIR, 0755);
+$regen=($argv[1]=='all'); // Regenerate all previews or only those missing 2D previews
+@mkdir(TMPDIR, 0755);
 $res=mysqli_query($db, 'select distinct lower(substring_index(name,".",-1)) as type, hash from files');
 while($row=mysqli_fetch_assoc($res))
 {
   $file=getfilepath($row['hash'], true);
   // Preview image
-  if(file_exists($file.'.png')){continue;}
+  if(file_exists($file.'.png') && !$regen){continue;}
   $img=false;
   switch($row['type'])
   {
@@ -67,7 +68,11 @@ while($row=mysqli_fetch_assoc($res))
       // Convert to STL for the next step (even for stl for bin->ascii)
       copy($file, TMPDIR.'/thingsharepreview_orig.'.$row['type']);
       system('assimp export '.escapeshellarg(TMPDIR.'/thingsharepreview_orig.'.$row['type']).' '.escapeshellarg(TMPDIR.'/thingsharepreview.stl'));
+      // Convert to X3D for 3D view while we're at it
+      system('assimp export '.escapeshellarg(TMPDIR.'/thingsharepreview_orig.'.$row['type']).' '.escapeshellarg(TMPDIR.'/thingsharepreview.x3d'));
       unlink(TMPDIR.'/thingsharepreview_orig.'.$row['type']);
+      $min=false;
+      $max=false;
       switch(PREVIEW_RENDERMETHOD)
       {
         case 'povray':
@@ -107,7 +112,7 @@ while($row=mysqli_fetch_assoc($res))
               fwrite($out, "mesh {\n");
             }
           }
-          fclose($f);
+          fclose($in);
           $dist=0;
           $maxdist=0;
           $avg=Array();
@@ -129,28 +134,91 @@ while($row=mysqli_fetch_assoc($res))
           $dist/=1.65;
           fwrite($out, '  location <'.($max[0]+$dist).','.($max[1]+$dist).','.($max[2]+$dist).">\n");
           fwrite($out, '  look_at <'.$avg[0].','.$avg[1].','.$avg[2].">\n");
-          fwrite($out, '  right x*'.(PREVIEW_SIZE[0]/PREVIEW_SIZE[1])."\n");
+          fwrite($out, '  right -x*'.(PREVIEW_SIZE[0]/PREVIEW_SIZE[1])."\n");
           fwrite($out, "  sky <0,0,1>\n}\n"); // Set Z vertical
           fwrite($out, 'light_source { <'.($max[0]+$dist+10).','.($max[1]+$dist).','.($max[2]+$dist+20).'> color rgb<1, 1, 1> }'); // Light slightly offset from camera to distinguish corners better
+          fclose($out);
           // Render using Povray
           $tmpfile=escapeshellarg(TMPDIR.'/thingsharepreview.pov');
           system('povray +I'.$tmpfile.' +O'.escapeshellarg($file.'.png').' -D +P +W'.(int)PREVIEW_SIZE[0].' +H'.(int)PREVIEW_SIZE[1].' +A0.5');
           unlink(TMPDIR.'/thingsharepreview.pov');
           break;
-        case 'openscad': // TODO: Test this code (completely untested so far), looks like we need to rename the file for the import to identify it correctly
+        case 'openscad': // TODO: Test this code (not well tested so far)
           $c=sprintf('#%02x%02x%02x', PREVIEW_COLOR[0], PREVIEW_COLOR[1], PREVIEW_COLOR[2]);
-          copy($file, TMPDIR.'/thingsharepreview.'.$row['type']);
-          file_put_contents(TMPDIR.'/thingsharepreview.scad', 'color("'.$c.'")import("thingsharepreview.'.str_replace('"', '', $row['type']).'");');
+          file_put_contents(TMPDIR.'/thingsharepreview.scad', 'color("'.$c.'")import("thingsharepreview.stl");');
           if(getenv('DISPLAY')==''){putenv('DISPLAY=:0');} // Default X server
           system('openscad --imgsize '.(int)PREVIEW_SIZE[0].','.(int)PREVIEW_SIZE[1].' -o '.escapeshellarg($file.'.png').' '.escapeshellarg(TMPDIR.'/thingsharepreview.scad'));
-          unlink(TMPDIR.'/thingsharepreview.'.$row['type']);
           unlink(TMPDIR.'/thingsharepreview.scad');
           break;
       }
+      if(!$min || !$max) // If we haven't found them as part of 2D rendering, find minmax coordinates
+      {
+        $in=fopen(TMPDIR.'/thingsharepreview.stl', 'r');
+        while($line=fgets($in))
+        {
+          if(substr(trim($line), 0, 7)=='vertex ')
+          {
+            $vertex=array_slice(explode(' ', trim($line)), 1);
+            for($i=0; $i<3; ++$i)
+            {
+              if($min[$i]===false || $vertex[$i]<$min[$i]){$min[$i]=$vertex[$i];}
+              if($max[$i]===false || $vertex[$i]>$max[$i]){$max[$i]=$vertex[$i];}
+            }
+          }
+        }
+        fclose($in);
+      }
+      $dist=0;
+      $maxdist=0;
+      $avg=Array();
+      for($i=0; $i<3; ++$i)
+      {
+        $avg[$i]=($max[$i]+$min[$i])/2;
+        if($max[$i]-$min[$i]>$maxdist){$maxdist=$max[$i]-$min[$i];}
+        $dist+=($max[$i]-$min[$i])/3;
+      }
+      $dist=($dist+$maxdist)/3.3;
+      // Edit X3D to match
+      $in=fopen(TMPDIR.'/thingsharepreview.x3d', 'r');
+      $out=fopen($file.'.x3d', 'w');
+      $c=sprintf('#%02x%02x%02x', PREVIEW_COLOR[0]*0.75, PREVIEW_COLOR[1]*0.75, PREVIEW_COLOR[2]*0.75);
+      $cs=sprintf('#%02x%02x%02x', PREVIEW_COLOR[0], PREVIEW_COLOR[1], PREVIEW_COLOR[2]);
+      while($line=fgets($in, 2048))
+      {
+        // Replace appearance with our own
+        if(substr_count(strtolower($line), '<appearance ')>0)
+        {
+          while(substr_count(strtolower($line), '</appearance>')<1 && ($line=fgets($in)));
+          $line='<appearance><material diffusecolor="'.$c.'" specularcolor="'.$cs.'"></material></appearance>';
+        }
+        if(substr_count(strtolower($line), '<scene>')>0 && substr_count(strtolower($line), '<!--')==0)
+        {
+          // Calculate orientation
+          $x=$avg[0]-($max[0]+$dist);
+          $y=$avg[1]-($max[1]+$dist);
+          $z=$avg[2]-($max[2]+$dist);
+          $rz=-atan2($x, $y);
+          $xy=hypot($x*cos($rz), $y*sin($rz));
+          if($x<0 && $y<0){$xy=-$xy;} // Restore sign of values turned absolute in the process
+          $xy*=1.5; // Not sure why, but without this view ends up too low
+          $rx=atan2($xy, $z)+pi();
+          $line.='<transform translation="'.($max[0]+$dist).','.($max[1]+$dist).','.($max[2]+$dist).'">
+<transform rotation="0,0,1,'.$rz.'">
+<transform rotation="1,0,0,'.$rx.'">
+<viewpoint></viewpoint>
+</transform>
+</transform>
+</transform>'."\n";
+        }
+        fwrite($out, $line);
+      }
+      fclose($in);
+      fclose($out);
+      // Clean up
+      unlink(TMPDIR.'/thingsharepreview.x3d');
       unlink(TMPDIR.'/thingsharepreview.stl');
       break;
   }
-  // TODO: Preview 3D model (x3d)
 //  print($file.'.'.$row['type']."\n");
 }
 ?>
diff --git a/head.php b/head.php
index e1b02d9..a1ac9b3 100644
--- a/head.php
+++ b/head.php
@@ -50,6 +50,9 @@ $search=htmlentities(isset($_GET['q'])?$_GET['q']:'');
 <head>
   <title><?=NODENAME?></title>
   <link rel="stylesheet" href="<?=BASEURL?>/style.css" type="text/css" />
+  <link rel="stylesheet" href="<?=BASEURL?>/x3dom.css" type="text/css" />
+  <script src="<?=BASEURL?>/x3dom.debug.js"></script>
+  <script src="<?=BASEURL?>/3dview.js"></script>
   <script src="<?=BASEURL?>/time.js"></script>
 </head>
 <body>
diff --git a/rpc_thing.php b/rpc_thing.php
index 9ee70c0..3c4a854 100644
--- a/rpc_thing.php
+++ b/rpc_thing.php
@@ -38,6 +38,7 @@ while($row=mysqli_fetch_assoc($res))
   $file=Array('name'=>$row['name'],
               'path'=>getfilepath($row['hash']),
               'preview'=>getpreview($row['name'], $row['hash']));
+  if($preview3d=get3dpreview($row['name'], $row['hash'])){$file['preview3d']=$preview3d;}
   // Grab mimetype, if set
   $mime=db_getmimetype($row['name']);
   if($mime!==false){$file['type']=$mime;}
diff --git a/setup.php b/setup.php
index d2b3870..e538e4c 100644
--- a/setup.php
+++ b/setup.php
@@ -87,6 +87,7 @@ if(!is_dir('parsedown'))
   $mandatory=true;
 }
 if(!is_dir('mdjs')){$deps.='<li>The javascript library mdjs appears to be missing. It can optionally be used to generate live previews of markdown formatted text. See <a href="Dependencies">Dependencies</a> for how to obtain it</li>';}
+if(!file_exists('x3dom.debug.js') || !file_exists('x3dom.css')){$deps.='<li>The javascript library x3dom appears to be missing. It can optionally be used to preview models in 3D. See <a href="Dependencies">Dependencies</a> for how to obtain it</li>';}
 if($deps!='')
 {
   print('<h1>Dependencies</h1><ul>'.$deps.'</ul>');
diff --git a/style.css b/style.css
index 563b6f7..1c8b8de 100644
--- a/style.css
+++ b/style.css
@@ -108,11 +108,13 @@ div.boxbottom {
   text-align:center;
   width:100%;
   background-color:#ffffffa0;
+  z-index:1;
 }
 div.boxtop {
   position:absolute;
   top:0px;
   background-color:#ffffffa0;
+  z-index:1;
 }
 div.thing>a>img {
   width:100%;
diff --git a/thing.php b/thing.php
index a82629a..cc5722d 100644
--- a/thing.php
+++ b/thing.php
@@ -53,7 +53,9 @@ $files='';
 foreach($thingobj['files'] as $file)
 {
   $type=(isset($file['type'])?$file['type']:'unknown');
-  $files.='<div class="thing"><a href="https://'.$thing[1].$file['path'].'" download="'.htmlentities($file['name']).'" type="'.$type.'"><div class="boxbottom">'.htmlentities($file['name']).'</div><img src="https://'.$thing[1].$file['preview'].'" /></a></div>';
+  $files.='<div class="thing">';
+  if(isset($file['preview3d'])){$files.='<div class="boxtop" style="right:0px;"><a href="#" onclick="return threedview(this,'.PREVIEW_SIZE[0].','.PREVIEW_SIZE[1].');" data-file="https://'.$thing[1].$file['preview3d'].'">3D view</a></div>';}
+  $files.='<a href="https://'.$thing[1].$file['path'].'" download="'.htmlentities($file['name']).'" type="'.$type.'"><div class="boxbottom">'.htmlentities($file['name']).'</div><img src="https://'.$thing[1].$file['preview'].'" /></a></div>';
 }
 ?>
 <h1><?=$name?> <small class="subheader">by <a href="<?=BASEURL?>/user/<?=$thingobj['by']['name']?>@<?=$thing[1]?>" title="<?=$thingobj['by']['name']?>@<?=$thing[1]?>"><?=htmlentities($thingobj['by']['displayname'])?></a></small></h1>