Initial commit

This commit is contained in:
Thomas Forgione 2019-12-09 18:41:42 +01:00
commit 844f72f207
No known key found for this signature in database
GPG Key ID: 203DAEA747F48F41
10 changed files with 2890 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
assets

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
all: js
OUTPUT=js/main.js
js: src/*
rm -f ${OUTPUT}
cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT}
cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT}
cat src/main.js >> ${OUTPUT} && echo >> ${OUTPUT}

18
index.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<title>TP</title>
<meta charset="utf-8">
<style>
body {
overflow: hidden;
margin: 0px;
}
</style>
</head>
<body>
<script src="js/three.min.js"></script>
<script src="js/OrbitControls.js"></script>
<script src="js/main.js"></script>
</body>
</html>

1181
js/OrbitControls.js Normal file

File diff suppressed because it is too large Load Diff

198
js/main.js Normal file
View File

@ -0,0 +1,198 @@
function fetchData(path, start, end, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.setRequestHeader('Range', 'bytes=' + start + "-" + (end - 1));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 206) {
callback(xhr.responseText);
}
}
};
xhr.send();
}
function parseLine(line) {
let element = {};
let split = line.split(/[ \t]+/);
if (split.length === 0) {
return;
}
switch (split[0]) {
case "v":
element.type = Element.AddVertex;
element.value = new THREE.Vector3(parseFloat(split[1]), parseFloat(split[2]), parseFloat(split[3]));
return element;
case "f":
element.type = Element.AddFace;
element.value = new THREE.Face3(parseInt(split[1], 10), parseInt(split[2], 10), parseInt(split[3], 10));
return element;
case "":
case "#":
return;
default:
throw new Error(split[0] + " is not a defined macro");
}
}
const Element = {};
Element.AddVertex = "AddVertex";
Element.AddFace = "AddFace";
class Loader {
constructor(path, chunkSize = 1024, timeout = 20) {
this.path = path;
this.chunkSize = chunkSize;
this.timeout = timeout;
this.currentByte = 0;
this.remainder = "";
}
start(callback) {
this.downloadAndParseNextChunk((data) => {
callback(data);
setTimeout(() => {
this.start(callback);
}, this.timeout);
});
}
downloadAndParseNextChunk(callback) {
fetchData(this.path, this.currentByte, this.currentByte + this.chunkSize, (data) => {
if (data.length === 0) {
console.log("Loading finished");
return;
}
this.currentByte += this.chunkSize;
let elements = [];
let split = data.split('\n');
split[0] = this.remainder + split[0];
this.remainder = split.pop();
for (let line of split) {
elements.push(parseLine(line));
}
callback(elements);
});
}
}
class Model extends THREE.Mesh {
constructor() {
let geometry = new THREE.BufferGeometry();
let vertices = new Float32Array(10000000);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
let normals = new Float32Array(10000000);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
let material = new THREE.MeshLambertMaterial({color: 0xffffff});
super(geometry, material);
this.frustumCulled = false;
this.vertices = [];
this.currentFace = 0;
}
manageElement(element) {
switch (element.type) {
case Element.AddVertex:
this.vertices.push(element.value);
break;
case Element.AddFace:
let buf = this.geometry.attributes.position.array;
buf[this.currentFace * 9 ] = this.vertices[element.value.a - 1].x;
buf[this.currentFace * 9 + 1] = this.vertices[element.value.a - 1].y;
buf[this.currentFace * 9 + 2] = this.vertices[element.value.a - 1].z;
buf[this.currentFace * 9 + 3] = this.vertices[element.value.b - 1].x;
buf[this.currentFace * 9 + 4] = this.vertices[element.value.b - 1].y;
buf[this.currentFace * 9 + 5] = this.vertices[element.value.b - 1].z;
buf[this.currentFace * 9 + 6] = this.vertices[element.value.c - 1].x;
buf[this.currentFace * 9 + 7] = this.vertices[element.value.c - 1].y;
buf[this.currentFace * 9 + 8] = this.vertices[element.value.c - 1].z;
this.geometry.attributes.position.needsUpdate = true;
let normal =
this.vertices[element.value.b - 1].clone().sub(this.vertices[element.value.a - 1])
.cross(this.vertices[element.value.c - 1].clone().sub(this.vertices[element.value.a - 1]));
normal.normalize();
buf = this.geometry.attributes.normal.array;
buf[this.currentFace * 9 ] = normal.x;
buf[this.currentFace * 9 + 1] = normal.y;
buf[this.currentFace * 9 + 2] = normal.z;
buf[this.currentFace * 9 + 3] = normal.x;
buf[this.currentFace * 9 + 4] = normal.y;
buf[this.currentFace * 9 + 5] = normal.z;
buf[this.currentFace * 9 + 6] = normal.x;
buf[this.currentFace * 9 + 7] = normal.y;
buf[this.currentFace * 9 + 8] = normal.z;
this.geometry.attributes.normal.needsUpdate = true;
this.currentFace++;
break;
default:
throw new Error("unknown element type: " + element.type);
}
this.geometry.verticesNeedUpdate = true;
this.geometry.elementsNeedUpdate = true;
}
}
let camera, scene, renderer, loader, light1, light2, controls, model;
init();
animate();
function init() {
loader = new Loader('assets/bunny_low_res_scaled.obj');
loader.start(function(elements) {
for (let element of elements) {
if (element !== undefined) {
model.manageElement(element);
}
}
});
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.0001, 1000);
camera.position.z = 3;
scene = new THREE.Scene();
model = new Model();
scene.add(model);
light1 = new THREE.AmbientLight(0x999999);
scene.add(light1);
light2 = new THREE.DirectionalLight(0xffffff, 1.0);
light2.position.set(0.0, 1.0, 0.0);
scene.add(light2);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}

1034
js/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

253
server.py Executable file
View File

@ -0,0 +1,253 @@
#! /usr/bin/env python2
# Shamefully copied from this gist :
# https://gist.github.com/pankajp/280596a5dabaeeceaaaa/
# Standard library imports.
from SocketServer import ThreadingMixIn
import BaseHTTPServer
import SimpleHTTPServer
import sys
import json
import os
from os.path import (join, exists, dirname, abspath, isabs, sep, walk, splitext,
isdir, basename, expanduser, split, splitdrive)
from os import makedirs, unlink, getcwd, chdir, curdir, pardir, rename, fstat
from shutil import copyfileobj, copytree
import glob
from zipfile import ZipFile
from urlparse import urlparse, parse_qs
from urllib import urlopen, quote, unquote
from posixpath import normpath
from cStringIO import StringIO
import re
import ConfigParser
import cgi
import threading
import socket
import errno
DATA_DIR = getcwd() # join(expanduser('~'), APP_NAME)
class ThreadingHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
pass
class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
""" Handler to handle POST requests for actions.
"""
serve_path = DATA_DIR
def do_GET(self):
""" Overridden to handle HTTP Range requests. """
self.range_from, self.range_to = self._get_range_header()
if self.range_from is None:
# nothing to do here
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
print 'range request', self.range_from, self.range_to
f = self.send_range_head()
if f:
self.copy_file_range(f, self.wfile)
f.close()
def copy_file_range(self, in_file, out_file):
""" Copy only the range in self.range_from/to. """
in_file.seek(self.range_from)
# Add 1 because the range is inclusive
left_to_copy = 1 + self.range_to - self.range_from
buf_length = 64*1024
bytes_copied = 0
while bytes_copied < left_to_copy:
read_buf = in_file.read(min(buf_length, left_to_copy - bytes_copied))
if len(read_buf) == 0:
break
out_file.write(read_buf)
bytes_copied += len(read_buf)
return bytes_copied
def send_range_head(self):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the outputfile by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do.
"""
path = self.translate_path(self.path)
f = None
if isdir(path):
if not self.path.endswith('/'):
# redirect browser - doing basically what apache does
self.send_response(301)
self.send_header("Location", self.path + "/")
self.end_headers()
return None
for index in "index.html", "index.htm":
index = join(path, index)
if exists(index):
path = index
break
else:
return self.list_directory(path)
if not exists(path) and path.endswith('/data'):
# FIXME: Handle grits-like query with /data appended to path
# stupid grits
if exists(path[:-5]):
path = path[:-5]
ctype = self.guess_type(path)
try:
# Always read in binary mode. Opening files in text mode may cause
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
f = open(path, 'rb')
except IOError:
self.send_error(404, "File not found")
return None
if self.range_from is None:
self.send_response(200)
else:
self.send_response(206)
self.send_header("Content-type", ctype)
fs = fstat(f.fileno())
file_size = fs.st_size
if self.range_from is not None:
if self.range_to is None or self.range_to >= file_size:
self.range_to = file_size-1
self.send_header("Content-Range",
"bytes %d-%d/%d" % (self.range_from,
self.range_to,
file_size))
# Add 1 because ranges are inclusive
self.send_header("Content-Length",
(1 + self.range_to - self.range_from))
else:
self.send_header("Content-Length", str(file_size))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.end_headers()
return f
def list_directory(self, path):
"""Helper to produce a directory listing (absent index.html).
Return value is either a file object, or None (indicating an
error). In either case, the headers are sent, making the
interface the same as for send_head().
"""
try:
list = os.listdir(path)
except os.error:
self.send_error(404, "No permission to list directory")
return None
list.sort(key=lambda a: a.lower())
f = StringIO()
displaypath = cgi.escape(unquote(self.path))
f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
f.write("<html>\n<title>Directory listing for %s</title>\n" % displaypath)
f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)
f.write("<hr>\n<ul>\n")
for name in list:
fullname = os.path.join(path, name)
displayname = linkname = name
# Append / for directories or @ for symbolic links
if os.path.isdir(fullname):
displayname = name + "/"
linkname = name + "/"
if os.path.islink(fullname):
displayname = name + "@"
# Note: a link to a directory displays with @ and links with /
f.write('<li><a href="%s">%s</a>\n'
% (quote(linkname), cgi.escape(displayname)))
f.write("</ul>\n<hr>\n</body>\n</html>\n")
length = f.tell()
f.seek(0)
self.send_response(200)
encoding = sys.getfilesystemencoding()
self.send_header("Content-type", "text/html; charset=%s" % encoding)
self.send_header("Content-Length", str(length))
self.end_headers()
return f
def translate_path(self, path):
""" Override to handle redirects.
"""
path = path.split('?',1)[0]
path = path.split('#',1)[0]
path = normpath(unquote(path))
words = path.split('/')
words = filter(None, words)
path = self.serve_path
for word in words:
drive, word = splitdrive(word)
head, word = split(word)
if word in (curdir, pardir): continue
path = join(path, word)
return path
# Private interface ######################################################
def _get_range_header(self):
""" Returns request Range start and end if specified.
If Range header is not specified returns (None, None)
"""
range_header = self.headers.getheader("Range")
if range_header is None:
return (None, None)
if not range_header.startswith("bytes="):
print "Not implemented: parsing header Range: %s" % range_header
return (None, None)
regex = re.compile(r"^bytes=(\d+)\-(\d+)?")
rangething = regex.search(range_header)
if rangething:
from_val = int(rangething.group(1))
if rangething.group(2) is not None:
return (from_val, int(rangething.group(2)))
else:
return (from_val, None)
else:
print 'CANNOT PARSE RANGE HEADER:', range_header
return (None, None)
def get_server(port=8000, next_attempts=0, serve_path=None):
Handler = RequestHandler
if serve_path:
Handler.serve_path = serve_path
while next_attempts >= 0:
try:
httpd = ThreadingHTTPServer(("", port), Handler)
return httpd
except socket.error as e:
if e.errno == errno.EADDRINUSE:
next_attempts -= 1
port += 1
else:
raise
def main(args=None):
if args is None:
args = sys.argv[1:]
PORT = 8000
if len(args)>0:
PORT = int(args[-1])
serve_path = DATA_DIR
if len(args) > 1:
serve_path = abspath(args[-2])
httpd = get_server(port=PORT, serve_path=serve_path)
print "serving at port", PORT
httpd.serve_forever()
if __name__ == "__main__" :
main()

89
src/Loader.js Normal file
View File

@ -0,0 +1,89 @@
function fetchData(path, start, end, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.setRequestHeader('Range', 'bytes=' + start + "-" + (end - 1));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 206) {
callback(xhr.responseText);
}
}
};
xhr.send();
}
function parseLine(line) {
let element = {};
let split = line.split(/[ \t]+/);
if (split.length === 0) {
return;
}
switch (split[0]) {
case "v":
element.type = Element.AddVertex;
element.value = new THREE.Vector3(parseFloat(split[1]), parseFloat(split[2]), parseFloat(split[3]));
return element;
case "f":
element.type = Element.AddFace;
element.value = new THREE.Face3(parseInt(split[1], 10), parseInt(split[2], 10), parseInt(split[3], 10));
return element;
case "":
case "#":
return;
default:
throw new Error(split[0] + " is not a defined macro");
}
}
const Element = {};
Element.AddVertex = "AddVertex";
Element.AddFace = "AddFace";
class Loader {
constructor(path, chunkSize = 1024, timeout = 20) {
this.path = path;
this.chunkSize = chunkSize;
this.timeout = timeout;
this.currentByte = 0;
this.remainder = "";
}
start(callback) {
this.downloadAndParseNextChunk((data) => {
callback(data);
setTimeout(() => {
this.start(callback);
}, this.timeout);
});
}
downloadAndParseNextChunk(callback) {
fetchData(this.path, this.currentByte, this.currentByte + this.chunkSize, (data) => {
if (data.length === 0) {
console.log("Loading finished");
return;
}
this.currentByte += this.chunkSize;
let elements = [];
let split = data.split('\n');
split[0] = this.remainder + split[0];
this.remainder = split.pop();
for (let line of split) {
elements.push(parseLine(line));
}
callback(elements);
});
}
}

60
src/Model.js Normal file
View File

@ -0,0 +1,60 @@
class Model extends THREE.Mesh {
constructor() {
let geometry = new THREE.BufferGeometry();
let vertices = new Float32Array(10000000);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
let normals = new Float32Array(10000000);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
let material = new THREE.MeshLambertMaterial({color: 0xffffff});
super(geometry, material);
this.frustumCulled = false;
this.vertices = [];
this.currentFace = 0;
}
manageElement(element) {
switch (element.type) {
case Element.AddVertex:
this.vertices.push(element.value);
break;
case Element.AddFace:
let buf = this.geometry.attributes.position.array;
buf[this.currentFace * 9 ] = this.vertices[element.value.a - 1].x;
buf[this.currentFace * 9 + 1] = this.vertices[element.value.a - 1].y;
buf[this.currentFace * 9 + 2] = this.vertices[element.value.a - 1].z;
buf[this.currentFace * 9 + 3] = this.vertices[element.value.b - 1].x;
buf[this.currentFace * 9 + 4] = this.vertices[element.value.b - 1].y;
buf[this.currentFace * 9 + 5] = this.vertices[element.value.b - 1].z;
buf[this.currentFace * 9 + 6] = this.vertices[element.value.c - 1].x;
buf[this.currentFace * 9 + 7] = this.vertices[element.value.c - 1].y;
buf[this.currentFace * 9 + 8] = this.vertices[element.value.c - 1].z;
this.geometry.attributes.position.needsUpdate = true;
let normal =
this.vertices[element.value.b - 1].clone().sub(this.vertices[element.value.a - 1])
.cross(this.vertices[element.value.c - 1].clone().sub(this.vertices[element.value.a - 1]));
normal.normalize();
buf = this.geometry.attributes.normal.array;
buf[this.currentFace * 9 ] = normal.x;
buf[this.currentFace * 9 + 1] = normal.y;
buf[this.currentFace * 9 + 2] = normal.z;
buf[this.currentFace * 9 + 3] = normal.x;
buf[this.currentFace * 9 + 4] = normal.y;
buf[this.currentFace * 9 + 5] = normal.z;
buf[this.currentFace * 9 + 6] = normal.x;
buf[this.currentFace * 9 + 7] = normal.y;
buf[this.currentFace * 9 + 8] = normal.z;
this.geometry.attributes.normal.needsUpdate = true;
this.currentFace++;
break;
default:
throw new Error("unknown element type: " + element.type);
}
this.geometry.verticesNeedUpdate = true;
this.geometry.elementsNeedUpdate = true;
}
}

46
src/main.js Normal file
View File

@ -0,0 +1,46 @@
let camera, scene, renderer, loader, light1, light2, controls, model;
init();
animate();
function init() {
loader = new Loader('assets/bunny_low_res_scaled.obj');
loader.start(function(elements) {
for (let element of elements) {
if (element !== undefined) {
model.manageElement(element);
}
}
});
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.0001, 1000);
camera.position.z = 3;
scene = new THREE.Scene();
model = new Model();
scene.add(model);
light1 = new THREE.AmbientLight(0x999999);
scene.add(light1);
light2 = new THREE.DirectionalLight(0xffffff, 1.0);
light2.position.set(0.0, 1.0, 0.0);
scene.add(light2);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}