forked from severeon/node-stl-thumbnailer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
280 lines (246 loc) · 8.93 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
const fs = require("fs"); // load STLs from filesystem
const THREE = require("three");
const { createCanvas, Image } = require("canvas");
const _ = require("lodash");
// Assign this to global so that the subsequent modules can extend it:
global.THREE = THREE;
require("./threejs-extras/Projector.js");
require("./threejs-extras/CanvasRenderer.js");
require("./threejs-extras/STLLoader.js");
//require("three/examples/js/renderers/Projector");
// require("three/examples/js/loaders/STLLoader");
function StlThumbnailer(options) {
// we need a url or a filePath to get started
var url = options.url,
filePath = options.filePath,
that = this,
jobs;
if (
(typeof url !== "string" && typeof filePath !== "string") ||
(typeof url === "string" && typeof filePath === "string")
) {
throw new Error(
"StlThumbnailer must be initialized with either a url or filePath, but not both."
);
}
this._stlData = null;
// Thumbnail jobs were requested at init time. Validate, then hang on until the STL is loaded.
if (Array.isArray(options.requestThumbnails)) {
jobs = options.requestThumbnails.map((thumb) =>
that.validateThumbnailRequest(thumb)
);
}
function process(stlData) {
that._stlData = stlData;
return that.processJobs(jobs);
}
if (url) {
return this.loadFromUrl(url).then(process);
} else {
return this.loadFromFile(filePath).then(process);
}
}
StlThumbnailer.prototype.processJobs = function (jobs) {
// Return a promise of all the thumbnails
var that = this,
thumbnails = jobs.map((job) => that.processThumbnail(job));
return Promise.all(thumbnails);
};
StlThumbnailer.prototype.loadFromUrl = async function (url) {
try {
const response = await fetch(url, {
method: "GET",
});
if (response.status === 200) {
const stlData = URL.createObjectURL(response.blob());
return stlData;
// const buffer = await response.arrayBuffer()
// fs.writeFileSync(`file${i}.txt`, Buffer.from(buffer))
}
throw new Error();
} catch (error) {
throw new Error("Unable to retrieve " + url);
}
};
StlThumbnailer.prototype.loadFromFile = function (path) {
return new Promise(function (resolve, reject) {
fs.readFile(path, function (error, stlData) {
if (!error && stlData) {
resolve(stlData);
} else {
if (error) reject(error);
else reject("Unable to load " + path);
}
});
});
};
StlThumbnailer.prototype.validateThumbnailRequest = function (thumbnail) {
if (typeof thumbnail.width !== "number")
throw new Error("Please specify a thumbnail width");
if (typeof thumbnail.height !== "number")
throw new Error("Please specify a thumbnail width");
var defaults = this.getDefaults();
return _.extend(_.clone(defaults), thumbnail);
};
StlThumbnailer.prototype.getDefaults = function () {
return {
cameraAngle: [10, 50, 100], // optional: specify the angle of the view for thumbnailing. This is the camera's position vector, the opposite of the direction the camera is looking.
showMinorEdges: true, // optional: show all edges lightly, even ones on ~flat faces
metallicOpacity: 0, // optional: some models, particularly those with non-flat surfaces or very high-poly models will look good with this environment map
enhanceMajorEdges: true, // optional: major edges will appear more boldly than minor edges
shadeNormalsOpacity: 0.4, // optional: faces will be shaded lightly by their normal direction
backgroundColor: 0xffffff, // optional: background color (RGB) for the rendered image
baseOpacity: 0.7, // optional: translucency of the base material that lets you see through it
baseColor: 0xffffff, // optional: base color
baseLineweight: 0.7, // optional: lineweights will scale to image size, but this serves as a base for that scaling. Larger numbers = heavier lineweights
lineColor: 0x000000, // optional: color of the linework
};
};
StlThumbnailer.prototype.processThumbnail = function (thumbnailSpec) {
// Return a promise of a canvas with the thumbnail
var that = this;
return new Promise(function (resolve, reject) {
try {
// Very hacky to use the global here, but I need a few methods that aren't there yet and they include the width and height of the canvas
global.document = {
createElement: function (tag) {
if (tag === "img") {
return new Image();
} else if (tag === "canvas") {
return createCanvas(width, height);
}
},
createElementNS: function (namespace, tag) {
return this.createElement(tag);
},
};
// Prepare the scene, renderer, and camera
var width = thumbnailSpec.width,
height = thumbnailSpec.height,
camera = new THREE.PerspectiveCamera(30, width / height, 1, 1000),
scene = new THREE.Scene(),
renderer = new THREE.CanvasRenderer(),
geometry = that.getGeometry();
// Configure renderer
renderer.setSize(width, height, false);
renderer.setClearColor(0xffffff, 1);
// Configure camera with user-set position, then move it in-or-out depending on
// the size of the model that needs to display
camera.position.x = thumbnailSpec.cameraAngle[0];
camera.position.y = thumbnailSpec.cameraAngle[1];
camera.position.z = thumbnailSpec.cameraAngle[2];
camera.lookAt(new THREE.Vector3(0, 0, 0));
// (re)Position the camera
// See http://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
var fov = camera.fov * (Math.PI / 180);
var distance = Math.abs(
geometry.boundingSphere.radius / Math.sin(fov / 2)
);
var newPosition = camera.position
.clone()
.normalize()
.multiplyScalar(distance);
camera.position.set(newPosition.x, newPosition.y, newPosition.z);
camera.needsUpdate = true;
camera.updateProjectionMatrix();
// Get materials according to requested characteristics of the output render
// TODO: Blending Modes?
if (thumbnailSpec.metallicOpacity > 0)
scene.add(
that.getMetallicMesh(geometry, thumbnailSpec.metallicOpacity)
);
if (thumbnailSpec.baseOpacity > 0)
scene.add(
that.getBasicMesh(
geometry,
thumbnailSpec.baseOpacity,
thumbnailSpec.baseColor
)
);
if (thumbnailSpec.shadeNormalsOpacity > 0)
scene.add(
that.getNormalMesh(geometry, thumbnailSpec.shadeNormalsOpacity)
);
if (thumbnailSpec.enhanceMajorEdges > 0)
scene.add(
that.getEdgeLine(
geometry,
thumbnailSpec.baseLineweight,
thumbnailSpec.lineColor
)
);
renderer.render(scene, camera);
resolve(renderer.domElement);
} catch (e) {
reject(e);
}
});
};
StlThumbnailer.prototype.getMetallicMesh = function (geometry, opacity) {
var envMap = this.loadTexture("/textures/metal.jpg");
envMap.mapping = THREE.SphericalReflectionMapping;
var mat = new THREE.MeshLambertMaterial({
envMap: envMap,
overdraw: 0.5,
transparent: true,
side: THREE.DoubleSide,
opacity: opacity,
});
return new THREE.Mesh(geometry, mat);
};
StlThumbnailer.prototype.getBasicMesh = function (geometry, opacity, color) {
var material = new THREE.MeshBasicMaterial({
overdraw: 0.1,
side: THREE.DoubleSide,
transparent: true,
opacity: opacity,
color: color,
});
return new THREE.Mesh(geometry, material);
};
StlThumbnailer.prototype.getNormalMesh = function (geometry, opacity) {
var material = new THREE.MeshNormalMaterial({
overdraw: 0.2,
side: THREE.DoubleSide,
transparent: true,
opacity: opacity,
});
return new THREE.Mesh(geometry, material);
};
StlThumbnailer.prototype.getEdgeLine = function (geometry, weight, color) {
var edges = new THREE.EdgesGeometry(geometry);
return new THREE.LineSegments(
edges,
new THREE.LineBasicMaterial({
color: color,
linewidth: weight,
linecap: "round",
linejoin: "round",
})
);
};
StlThumbnailer.prototype.loadTexture = function (path) {
// work-around for not being able to use new THREE.TextureLoader().load( 'textures/metal.jpg' ), which depends on XHR
var texture = new THREE.Texture();
var jpegPath = __dirname + path;
var isJPEG =
jpegPath.search(/\.(jpg|jpeg)$/) > 0 ||
url.search(/^data\:image\/jpeg/) === 0;
var textureImage = fs.readFileSync(jpegPath);
var img = Image;
img.src = textureImage;
texture.format = isJPEG ? THREE.RGBFormat : THREE.RGBAFormat;
texture.image = img;
texture.needsUpdate = true;
return texture;
};
StlThumbnailer.prototype.getGeometry = function () {
var loader = new THREE.STLLoader(),
geometry = loader.parse(this._stlData);
geometry.computeVertexNormals();
geometry.computeBoundingSphere();
geometry.computeBoundingBox();
geometry.center();
return geometry;
};
module.exports = StlThumbnailer;