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'
|
binary? && extname.downcase.delete('.') == 'sketch'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stl?
|
||||||
|
extname.downcase.delete('.') == 'stl'
|
||||||
|
end
|
||||||
|
|
||||||
def size_within_svg_limits?
|
def size_within_svg_limits?
|
||||||
size <= MAXIMUM_SVG_SIZE
|
size <= MAXIMUM_SVG_SIZE
|
||||||
end
|
end
|
||||||
|
@ -81,6 +85,8 @@ class Blob < SimpleDelegator
|
||||||
'notebook'
|
'notebook'
|
||||||
elsif sketch?
|
elsif sketch?
|
||||||
'sketch'
|
'sketch'
|
||||||
|
elsif stl?
|
||||||
|
'stl'
|
||||||
elsif text?
|
elsif text?
|
||||||
'text'
|
'text'
|
||||||
else
|
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',
|
profile: './profile/profile_bundle.js',
|
||||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||||
snippet: './snippet/snippet_bundle.js',
|
snippet: './snippet/snippet_bundle.js',
|
||||||
|
stl_viewer: './blob/stl_viewer.js',
|
||||||
terminal: './terminal/terminal_bundle.js',
|
terminal: './terminal/terminal_bundle.js',
|
||||||
u2f: ['vendor/u2f'],
|
u2f: ['vendor/u2f'],
|
||||||
users: './users/users_bundle.js',
|
users: './users/users_bundle.js',
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"select2": "3.5.2-browserify",
|
"select2": "3.5.2-browserify",
|
||||||
"stats-webpack-plugin": "^0.4.3",
|
"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",
|
"timeago.js": "^2.0.5",
|
||||||
"underscore": "^1.8.3",
|
"underscore": "^1.8.3",
|
||||||
"visibilityjs": "^1.2.4",
|
"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
|
||||||
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
|
describe '#to_partial_path' do
|
||||||
let(:project) { double(lfs_enabled?: true) }
|
let(:project) { double(lfs_enabled?: true) }
|
||||||
|
|
||||||
|
@ -122,7 +136,8 @@ describe Blob do
|
||||||
lfs_pointer?: false,
|
lfs_pointer?: false,
|
||||||
svg?: false,
|
svg?: false,
|
||||||
text?: false,
|
text?: false,
|
||||||
binary?: false
|
binary?: false,
|
||||||
|
stl?: false
|
||||||
)
|
)
|
||||||
|
|
||||||
described_class.decorate(double).tap do |blob|
|
described_class.decorate(double).tap do |blob|
|
||||||
|
@ -175,6 +190,11 @@ describe Blob do
|
||||||
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
|
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
|
||||||
expect(blob.to_partial_path(project)).to eq 'sketch'
|
expect(blob.to_partial_path(project)).to eq 'sketch'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'handles STLs' do
|
||||||
|
blob = stubbed_blob(text?: true, stl?: true)
|
||||||
|
expect(blob.to_partial_path(project)).to eq 'stl'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#size_within_svg_limits?' do
|
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"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
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:
|
throttleit@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||||
|
|
Loading…
Reference in a new issue