STL file viewer
This commit is contained in:
parent
49bdd8d63b
commit
e992799ce5
11 changed files with 318 additions and 1 deletions
147
app/assets/javascripts/blob/3d_viewer/index.js
Normal file
147
app/assets/javascripts/blob/3d_viewer/index.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
import * as THREE from 'three/build/three.module';
|
||||
import STLLoaderClass from 'three-stl-loader';
|
||||
import OrbitControlsClass from 'three-orbit-controls';
|
||||
import MeshObject from './mesh_object';
|
||||
|
||||
const STLLoader = STLLoaderClass(THREE);
|
||||
const OrbitControls = OrbitControlsClass(THREE);
|
||||
|
||||
export default class Renderer {
|
||||
constructor(container) {
|
||||
this.renderWrapper = this.render.bind(this);
|
||||
this.objects = [];
|
||||
|
||||
this.container = container;
|
||||
this.width = this.container.offsetWidth;
|
||||
this.height = 500;
|
||||
|
||||
this.loader = new STLLoader();
|
||||
|
||||
this.fov = 45;
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
this.fov,
|
||||
this.width / this.height,
|
||||
1,
|
||||
1000,
|
||||
);
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
this.scene.add(this.camera);
|
||||
|
||||
// Setup the viewer
|
||||
this.setupRenderer();
|
||||
this.setupGrid();
|
||||
this.setupLight();
|
||||
|
||||
// Setup OrbitControls
|
||||
this.controls = new OrbitControls(
|
||||
this.camera,
|
||||
this.renderer.domElement,
|
||||
);
|
||||
this.controls.minDistance = 5;
|
||||
this.controls.maxDistance = 30;
|
||||
this.controls.enableKeys = false;
|
||||
|
||||
this.loadFile();
|
||||
}
|
||||
|
||||
setupRenderer() {
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
});
|
||||
|
||||
this.renderer.setClearColor(0xFFFFFF);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setSize(
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
}
|
||||
|
||||
setupLight() {
|
||||
// Point light illuminates the object
|
||||
const pointLight = new THREE.PointLight(
|
||||
0xFFFFFF,
|
||||
2,
|
||||
0,
|
||||
);
|
||||
|
||||
pointLight.castShadow = true;
|
||||
|
||||
this.camera.add(pointLight);
|
||||
|
||||
// Ambient light illuminates the scene
|
||||
const ambientLight = new THREE.AmbientLight(
|
||||
0xFFFFFF,
|
||||
1,
|
||||
);
|
||||
this.scene.add(ambientLight);
|
||||
}
|
||||
|
||||
setupGrid() {
|
||||
this.grid = new THREE.GridHelper(
|
||||
20,
|
||||
20,
|
||||
0x000000,
|
||||
0x000000,
|
||||
);
|
||||
|
||||
this.scene.add(this.grid);
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
this.loader.load(this.container.dataset.endpoint, (geo) => {
|
||||
const obj = new MeshObject(geo);
|
||||
|
||||
this.objects.push(obj);
|
||||
this.scene.add(obj);
|
||||
|
||||
this.start();
|
||||
this.setDefaultCameraPosition();
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
// Empty the container first
|
||||
this.container.innerHTML = '';
|
||||
|
||||
// Add to DOM
|
||||
this.container.appendChild(this.renderer.domElement);
|
||||
|
||||
// Make controls visible
|
||||
this.container.parentNode.classList.remove('is-stl-loading');
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.renderer.render(
|
||||
this.scene,
|
||||
this.camera,
|
||||
);
|
||||
|
||||
requestAnimationFrame(this.renderWrapper);
|
||||
}
|
||||
|
||||
changeObjectMaterials(type) {
|
||||
this.objects.forEach((obj) => {
|
||||
obj.changeMaterial(type);
|
||||
});
|
||||
}
|
||||
|
||||
setDefaultCameraPosition() {
|
||||
const obj = this.objects[0];
|
||||
const radius = (obj.geometry.boundingSphere.radius / 1.5);
|
||||
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
|
||||
|
||||
this.camera.position.set(
|
||||
0,
|
||||
dist + 1,
|
||||
dist,
|
||||
);
|
||||
|
||||
this.camera.lookAt(this.grid);
|
||||
this.controls.update();
|
||||
}
|
||||
}
|
49
app/assets/javascripts/blob/3d_viewer/mesh_object.js
Normal file
49
app/assets/javascripts/blob/3d_viewer/mesh_object.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
Matrix4,
|
||||
MeshLambertMaterial,
|
||||
Mesh,
|
||||
} from 'three/build/three.module';
|
||||
|
||||
const defaultColor = 0xE24329;
|
||||
const materials = {
|
||||
default: new MeshLambertMaterial({
|
||||
color: defaultColor,
|
||||
}),
|
||||
wireframe: new MeshLambertMaterial({
|
||||
color: defaultColor,
|
||||
wireframe: true,
|
||||
}),
|
||||
};
|
||||
|
||||
export default class MeshObject extends Mesh {
|
||||
constructor(geo) {
|
||||
super(
|
||||
geo,
|
||||
materials.default,
|
||||
);
|
||||
|
||||
this.geometry.computeBoundingSphere();
|
||||
|
||||
this.rotation.set(-Math.PI / 2, 0, 0);
|
||||
|
||||
if (this.geometry.boundingSphere.radius > 4) {
|
||||
const scale = 4 / this.geometry.boundingSphere.radius;
|
||||
|
||||
this.geometry.applyMatrix(
|
||||
new Matrix4().makeScale(
|
||||
scale,
|
||||
scale,
|
||||
scale,
|
||||
),
|
||||
);
|
||||
this.geometry.computeBoundingSphere();
|
||||
|
||||
this.position.x = -this.geometry.boundingSphere.center.x;
|
||||
this.position.z = this.geometry.boundingSphere.center.y;
|
||||
}
|
||||
}
|
||||
|
||||
changeMaterial(type) {
|
||||
this.material = materials[type];
|
||||
}
|
||||
}
|
19
app/assets/javascripts/blob/stl_viewer.js
Normal file
19
app/assets/javascripts/blob/stl_viewer.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Renderer from './3d_viewer';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
|
||||
|
||||
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
document.querySelector('.js-material-changer.active').classList.remove('active');
|
||||
target.classList.add('active');
|
||||
target.blur();
|
||||
|
||||
viewer.changeObjectMaterials(target.dataset.type);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -275,3 +275,9 @@ span.idiff {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-stl-loading {
|
||||
.stl-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,10 @@ class Blob < SimpleDelegator
|
|||
binary? && extname.downcase.delete('.') == 'sketch'
|
||||
end
|
||||
|
||||
def stl?
|
||||
extname.downcase.delete('.') == 'stl'
|
||||
end
|
||||
|
||||
def size_within_svg_limits?
|
||||
size <= MAXIMUM_SVG_SIZE
|
||||
end
|
||||
|
@ -81,6 +85,8 @@ class Blob < SimpleDelegator
|
|||
'notebook'
|
||||
elsif sketch?
|
||||
'sketch'
|
||||
elsif stl?
|
||||
'stl'
|
||||
elsif text?
|
||||
'text'
|
||||
else
|
||||
|
|
12
app/views/projects/blob/_stl.html.haml
Normal file
12
app/views/projects/blob/_stl.html.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('stl_viewer')
|
||||
|
||||
.file-content.is-stl-loading
|
||||
.text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
|
||||
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
|
||||
.text-center.prepend-top-default.append-bottom-default.stl-controls
|
||||
.btn-group
|
||||
%button.btn.btn-default.btn-sm.js-material-changer{ data: { type: 'wireframe' } }
|
||||
Wireframe
|
||||
%button.btn.btn-default.btn-sm.active.js-material-changer{ data: { type: 'default' } }
|
||||
Solid
|
|
@ -42,6 +42,7 @@ var config = {
|
|||
profile: './profile/profile_bundle.js',
|
||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||
snippet: './snippet/snippet_bundle.js',
|
||||
stl_viewer: './blob/stl_viewer.js',
|
||||
terminal: './terminal/terminal_bundle.js',
|
||||
u2f: ['vendor/u2f'],
|
||||
users: './users/users_bundle.js',
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
"raw-loader": "^0.5.1",
|
||||
"select2": "3.5.2-browserify",
|
||||
"stats-webpack-plugin": "^0.4.3",
|
||||
"three": "^0.84.0",
|
||||
"three-orbit-controls": "^82.1.0",
|
||||
"three-stl-loader": "^1.0.4",
|
||||
"timeago.js": "^2.0.5",
|
||||
"underscore": "^1.8.3",
|
||||
"visibilityjs": "^1.2.4",
|
||||
|
|
42
spec/javascripts/blob/3d_viewer/mesh_object_spec.js
Normal file
42
spec/javascripts/blob/3d_viewer/mesh_object_spec.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
BoxGeometry,
|
||||
} from 'three/build/three.module';
|
||||
import MeshObject from '~/blob/3d_viewer/mesh_object';
|
||||
|
||||
describe('Mesh object', () => {
|
||||
it('defaults to non-wireframe material', () => {
|
||||
const object = new MeshObject(
|
||||
new BoxGeometry(10, 10, 10),
|
||||
);
|
||||
|
||||
expect(object.material.wireframe).toBeFalsy();
|
||||
});
|
||||
|
||||
it('changes to wirefame material', () => {
|
||||
const object = new MeshObject(
|
||||
new BoxGeometry(10, 10, 10),
|
||||
);
|
||||
|
||||
object.changeMaterial('wireframe');
|
||||
|
||||
expect(object.material.wireframe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('scales object down', () => {
|
||||
const object = new MeshObject(
|
||||
new BoxGeometry(10, 10, 10),
|
||||
);
|
||||
const radius = object.geometry.boundingSphere.radius;
|
||||
|
||||
expect(radius).not.toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
it('does not scale object down', () => {
|
||||
const object = new MeshObject(
|
||||
new BoxGeometry(1, 1, 1),
|
||||
);
|
||||
const radius = object.geometry.boundingSphere.radius;
|
||||
|
||||
expect(radius).toBeLessThan(1);
|
||||
});
|
||||
});
|
|
@ -111,6 +111,20 @@ describe Blob do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#stl?' do
|
||||
it 'is falsey with image extension' do
|
||||
git_blob = Gitlab::Git::Blob.new(name: 'file.png')
|
||||
|
||||
expect(described_class.decorate(git_blob)).not_to be_stl
|
||||
end
|
||||
|
||||
it 'is truthy with STL extension' do
|
||||
git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
|
||||
|
||||
expect(described_class.decorate(git_blob)).to be_stl
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_partial_path' do
|
||||
let(:project) { double(lfs_enabled?: true) }
|
||||
|
||||
|
@ -122,7 +136,8 @@ describe Blob do
|
|||
lfs_pointer?: false,
|
||||
svg?: false,
|
||||
text?: false,
|
||||
binary?: false
|
||||
binary?: false,
|
||||
stl?: false
|
||||
)
|
||||
|
||||
described_class.decorate(double).tap do |blob|
|
||||
|
@ -175,6 +190,11 @@ describe Blob do
|
|||
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
|
||||
expect(blob.to_partial_path(project)).to eq 'sketch'
|
||||
end
|
||||
|
||||
it 'handles STLs' do
|
||||
blob = stubbed_blob(text?: true, stl?: true)
|
||||
expect(blob.to_partial_path(project)).to eq 'stl'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#size_within_svg_limits?' do
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -4305,6 +4305,18 @@ text-table@~0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
|
||||
three-orbit-controls@^82.1.0:
|
||||
version "82.1.0"
|
||||
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
|
||||
|
||||
three-stl-loader@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/three-stl-loader/-/three-stl-loader-1.0.4.tgz#6b3319a31e3b910aab1883d19b00c81a663c3e03"
|
||||
|
||||
three@^0.84.0:
|
||||
version "0.84.0"
|
||||
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
|
||||
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
|
|
Loading…
Reference in a new issue