Public
Edited
Nov 17, 2024
Importers
Insert cell
Insert cell
Insert cell
Insert cell
modelBuffer = await FileAttachment("Bunny-LowPoly.stl").arrayBuffer()
//modelBuffer = await FileAttachment("canyon2.stl").arrayBuffer()
//modelBuffer = await FileAttachment("Rotor.stl").arrayBuffer()
//modelBuffer = await FileAttachment("30x40_full_buildplate.stl").arrayBuffer()
Insert cell
Insert cell
model = parseBinarySTL(modelBuffer)
Insert cell
prepareModel = (model) => {
const EPSILON = 1e-10; //Number.EPSILON ?
const valEqual = (v1, v2) => Math.abs(v2 - v1) < EPSILON;
const ptEqual = ([x1, y1, z1], [x2, y2, z2]) =>
valEqual(x1, x2) && valEqual(y1, y2) && valEqual(z1, z2);
const sgmEqual = ([p11, p12], [p21, p22]) =>
(p11 == p21 && p12 == p22) || (p11 == p22 && p12 == p21);

const [[xmin, xmax], [ymin, ymax], [zmin, zmax]] = model.bounds;
const offset = [(xmin + xmax) / 2, (ymin + ymax) / 2, -(zmin + zmax) / 2];

const points = [];
const pointId = (point) => {
const apoint = m4.subtractVectors(point, offset);
const existingPoint = points.find((pt) => ptEqual(pt, apoint));
return existingPoint || (points.push(apoint), apoint);
};
const segments = [];
const sgmId = (pts, facet) => {
const existingSegment = segments.find((sgm) => sgmEqual(sgm.pts, pts));
if (existingSegment) {
existingSegment.facets.push(facet);
return existingSegment;
} else {
const segment = { pts, facets: [facet] };
segments.push(segment);
return segment;
}
};
const facets = model.facets.map(({ pts, normal }) => {
const facet = { normal };
const [pt1, pt2, pt3] = pts.map(pointId);
facet.segments = [
sgmId([pt1, pt2], facet),
sgmId([pt2, pt3], facet),
sgmId([pt3, pt1], facet)
];
return facet;
});
segments.forEach((segment) => {
const [f1, f2] = segment.facets;
segment.dot = f2 && m4.dot(f1.normal, f2.normal);
});
return { facets, segments, points, bounds: model.bounds };
}
Insert cell
Insert cell
Insert cell
Insert cell
projection = (position, target, up, aspect, near, far, fov) => {
const projection = m4.perspective(fov, aspect, near, far);
const view = m4.inverse(m4.lookAt(position, target, up));
const worldViewProjection = m4.multiply(projection, view);
return {
project: (pt) => m4.transformPoint(worldViewProjection, pt),
distance: (pt) => m4.distance(position, pt),
visible: (pt, normal) =>
m4.dot(normal, m4.subtractVectors(pt, position)) < 0
};
}
Insert cell
isoProjection = ([xo, yo, zo]) => {
const s2 = Math.sqrt(2) / 2;
const f = Math.sqrt(3) / 2;
return {
project: ([x, y, z]) => [
(x - xo - (y - yo)) * f,
-(x - xo + (y - yo)) * 0.5 - (z - zo) * f
],
distance: ([x, y, z]) => (x - xo + (y - yo)) / 2 - (z - zo) * s2,
visible: (pt, [nx, ny, nz]) => (nx + ny) / 2 - nz * s2 < 0
};
}
Insert cell
Insert cell
plot = (model, P, params) => {
const { facets, segments, points } = model;
const { cos, sin } = Math;
const { cos: cosf, culling, revert, hatching } = params;
const facetPts = ({ segments: [s1, s2] }) => {
const [[p1, p2], [pa, pb]] = [s1.pts, s2.pts];
return [p1, p2, pa == p1 || pa == p2 ? pb : pa];
};
const dist = (facet) => {
const [p1, p2, p3] = facetPts(facet);
const [x1, y1, z1] = p1;
const [x2, y2, z2] = p2;
const [x3, y3, z3] = p3;
const point = [(x1 + x2 + x3) / 3, (y1 + y2 + y3) / 3, (z1 + z2 + z3) / 3];
return P.distance(point);
};
const plot = Plot();
const pts2d = new Map(
points.map((pt) => {
const [x, y] = P.project(pt);
return [pt, [x, -y]];
})
);
const polygons = new Polygons();
const drawn = new Set();
const addSegIfNotDrawn = (polygon, segId) => {
if (!drawn.has(segId)) {
const { facets, pts, dot } = segId;
if (
!facets[1] ||
!facets[0].isVisible ||
!facets[1].isVisible ||
dot <= cosf
) {
polygon.addSegments(pts2d.get(pts[0]), pts2d.get(pts[1]));
drawn.add(segId);
}
}
};
let pos = [0, 0];
const vec = ([x1, y1], [x2, y2]) => [x2 - x1, y2 - y1];
facets.forEach(
(facet) =>
(facet.isVisible = P.visible(facet.segments[0].pts[0], facet.normal))
);
facets
// only render visible facets
.filter(
(facet) => !culling || (revert ? !facet.isVisible : facet.isVisible)
)
// compute distance to centroid for each facet
.map((facet) => Object.assign(facet, { dist: dist(facet) }))
// sort facets based on distance to observer
.sort((f1, f2) => f1.dist - f2.dist)
.forEach((facet, i) => {
const [p1, p2, p3] = facetPts(facet);
if (culling) {
const polygon = polygons.create();
polygon.addPoints(pts2d.get(p1), pts2d.get(p2), pts2d.get(p3));
facet.segments.map((s) => addSegIfNotDrawn(polygon, s));
if (hatching) {
const cos = m4.dot(facet.normal, hatching);
if (cos > 0.75) polygon.addHatching(Math.PI / 4, 0.01);
else if (cos > 0.5) polygon.addHatching(Math.PI / 4, 0.02);
}
polygons.draw(plot, polygon, true);
} else {
plot.move(...vec(pos, (pos = pts2d.get(p1))));
plot.draw(...vec(pos, (pos = pts2d.get(p2))));
plot.draw(...vec(pos, (pos = pts2d.get(p3))));
plot.draw(...vec(pos, (pos = pts2d.get(p1))));
}
});

return plot;
}
Insert cell
{
const { elevation, azimuth, radius, fov, cos, culling, revert, hatching } =
params;
const [ce, se] = [Math.cos(elevation), Math.sin(elevation)];
const [ca, sa] = [Math.cos(azimuth), Math.sin(azimuth)];
const position = [ce * ca * radius, ce * sa * radius, se * radius];
const target = [0, 0, 0];
const up = [0, 0, 1];
const [aspect, near, far] = [1, 1, 50];
const preparedModel = prepareModel(model);
const P = projection(position, target, up, aspect, near, far, fov);
const params_ = { cos, culling, revert, hatching: hatching && [0, 1, 0] };
const attrs = { "stroke-width": 0.005, viewBox: "-1 -2 2 2" };
return viewer([width, 600]).view(plot(preparedModel, P, params_), attrs);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more