{"id":164,"date":"2026-09-08T09:45:22","date_gmt":"2026-09-08T06:45:22","guid":{"rendered":"https:\/\/kraphik3d.com\/wordpress\/?p=164"},"modified":"2026-09-09T09:34:27","modified_gmt":"2026-09-09T06:34:27","slug":"stl-loader","status":"publish","type":"post","link":"https:\/\/kraphik3d.com\/wordpress\/2026\/09\/08\/stl-loader\/","title":{"rendered":"STL web viewer"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This is a STL model loader test made with Chat GPT and Claude AI coding. Currently I am trainee in Fab Lab Oulu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can download code from my <a href=\"https:\/\/github.com\/kimmorp-777\/STL-web-viewer\">github page<\/a>.<\/p>\n\n\n\n<div id=\"stl-viewer-box\" style=\"max-width:100%;margin:0 auto;font-family:Arial,sans-serif;background:#222;color:#fff;border-radius:6px;overflow:hidden\">\n  <div style=\"padding:12px 15px;background:#000;display:flex;gap:15px;align-items:center;flex-wrap:wrap\">\n    <b>STL web viewer<\/b>\n    <label>Load STL: <input type=\"file\" id=\"stl-input\" accept=\".stl\" style=\"font-size:15px\"><\/label>\n  <\/div>\n  <div style=\"display:flex;flex-wrap:wrap\">\n    <div id=\"stl-canvas\" style=\"flex:1 1 400px;min-width:0;height:500px !important;min-height:500px !important;display:block !important\"><\/div>\n    <div style=\"flex:0 0 220px;background:#1a1a1a;padding:15px;display:flex;flex-direction:column;gap:14px;height:500px;box-sizing:border-box;overflow-y:auto\">\n\n      <div>\n        <label style=\"font-size:13px;display:block;margin-bottom:4px\">Select object<\/label>\n        <select id=\"object-select\" style=\"width:100%;background:#111;color:#fff;border:1px solid #444;border-radius:3px;padding:5px;box-sizing:border-box\">\n          <option value=\"\">\u2014 no object \u2014<\/option>\n        <\/select>\n      <\/div>\n\n      <div style=\"background:#111;border:1px solid #333;border-radius:4px;padding:10px\">\n        <table style=\"width:100%;border-collapse:collapse;font-size:13px\">\n          <tbody><tr>\n            <td><\/td>\n            <td style=\"text-align:center;color:#999;padding:2px 0\">X<\/td>\n            <td style=\"text-align:center;color:#999;padding:2px 0\">Y<\/td>\n            <td style=\"text-align:center;color:#999;padding:2px 0\">Z<\/td>\n          <\/tr>\n          <tr>\n            <td style=\"color:#999;white-space:nowrap;padding:2px 4px 2px 0\">Size [mm]<\/td>\n            <td><input readonly=\"\" id=\"size-x\" style=\"width:100%;background:#222;color:#fff;border:1px solid #444;border-radius:3px;padding:3px;box-sizing:border-box;text-align:center\"><\/td>\n            <td><input readonly=\"\" id=\"size-y\" style=\"width:100%;background:#222;color:#fff;border:1px solid #444;border-radius:3px;padding:3px;box-sizing:border-box;text-align:center\"><\/td>\n            <td><input readonly=\"\" id=\"size-z\" style=\"width:100%;background:#222;color:#fff;border:1px solid #444;border-radius:3px;padding:3px;box-sizing:border-box;text-align:center\"><\/td>\n          <\/tr>\n        <\/tbody><\/table>\n        <div style=\"margin-top:10px;font-size:13px;display:flex;flex-direction:column;gap:6px\">\n          <div style=\"display:flex;justify-content:space-between\"><span style=\"color:#999\">Volume<\/span><span id=\"stat-volume\">\u2014<\/span><\/div>\n          <div style=\"display:flex;justify-content:space-between\"><span style=\"color:#999\">Area<\/span><span id=\"stat-area\">\u2014<\/span><\/div>\n          <div style=\"display:flex;justify-content:space-between\"><span style=\"color:#999\">Triangles<\/span><span id=\"stat-triangles\">\u2014<\/span><\/div>\n        <\/div>\n      <\/div>\n\n      <hr style=\"width:100%;border-color:#444;margin:0\">\n\n      <b style=\"font-size:14px\">Rotate<\/b>\n      <div>\n        <label style=\"font-size:13px\">X<\/label>\n        <input type=\"range\" id=\"rot-x\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%\">\n        <input type=\"number\" id=\"num-x\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%;margin-top:4px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;padding:3px 6px;box-sizing:border-box\">\n      <\/div>\n      <div>\n        <label style=\"font-size:13px\">Y<\/label>\n        <input type=\"range\" id=\"rot-y\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%\">\n        <input type=\"number\" id=\"num-y\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%;margin-top:4px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;padding:3px 6px;box-sizing:border-box\">\n      <\/div>\n      <div>\n        <label style=\"font-size:13px\">Z<\/label>\n        <input type=\"range\" id=\"rot-z\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%\">\n        <input type=\"number\" id=\"num-z\" min=\"-180\" max=\"180\" step=\"1\" value=\"0\" style=\"width:100%;margin-top:4px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;padding:3px 6px;box-sizing:border-box\">\n      <\/div>\n      <button id=\"rot-reset\" style=\"padding:8px;background:#333;color:#fff;border:1px solid #555;border-radius:4px;cursor:pointer\">Reset rotate<\/button>\n\n      <hr style=\"width:100%;border-color:#444;margin:0\">\n\n      <b style=\"font-size:14px\">Material<\/b>\n      <div>\n        <label style=\"display:flex;justify-content:space-between;font-size:13px\">Reflection <span id=\"refl-val\">30%<\/span><\/label>\n        <input type=\"range\" id=\"reflection\" min=\"0\" max=\"100\" step=\"1\" value=\"30\" style=\"width:100%\">\n      <\/div>\n      <div>\n        <label style=\"font-size:13px;display:block;margin-bottom:4px\">V\u00e4ri<\/label>\n        <input type=\"color\" id=\"color-picker\" value=\"#aaaaaa\" style=\"width:100%;height:38px;border:1px solid #444;border-radius:3px;background:#111;cursor:pointer;padding:0\">\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script type=\"module\">\nimport * as THREE from \"https:\/\/esm.sh\/three@0.160.0\";\nimport { OrbitControls } from \"https:\/\/esm.sh\/three@0.160.0\/examples\/jsm\/controls\/OrbitControls.js\";\nimport { STLLoader } from \"https:\/\/esm.sh\/three@0.160.0\/examples\/jsm\/loaders\/STLLoader.js\";\nimport { RoomEnvironment } from \"https:\/\/esm.sh\/three@0.160.0\/examples\/jsm\/environments\/RoomEnvironment.js\";\n\nconst container = document.getElementById(\"stl-canvas\");\n\nfunction getSize() {\n  let w = container.clientWidth;\n  let h = container.clientHeight;\n  if (!w || !h) { w = container.offsetWidth || 600; h = 500; }\n  return { w, h };\n}\n\nconst scene = new THREE.Scene();\nscene.background = new THREE.Color(0x222222);\n\nconst { w, h } = getSize();\nconst camera = new THREE.PerspectiveCamera(50, w \/ h, 0.1, 10000);\ncamera.position.set(100, 100, 150);\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(w, h);\nrenderer.domElement.style.display = \"block\";\ncontainer.appendChild(renderer.domElement);\n\nconst pmremGenerator = new THREE.PMREMGenerator(renderer);\nscene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;\npmremGenerator.dispose();\n\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\n\nscene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 2));\nconst light = new THREE.DirectionalLight(0xffffff, 2);\nlight.position.set(100, 200, 150);\nscene.add(light);\nscene.add(new THREE.GridHelper(200, 20));\n\nlet mesh = null;\n\n\/\/ --- rotation controls ---\nconst axes = [\n  { slider: document.getElementById(\"rot-x\"), num: document.getElementById(\"num-x\") },\n  { slider: document.getElementById(\"rot-y\"), num: document.getElementById(\"num-y\") },\n  { slider: document.getElementById(\"rot-z\"), num: document.getElementById(\"num-z\") }\n];\n\nfunction clamp180(v) {\n  if (isNaN(v)) return 0;\n  return Math.max(-180, Math.min(180, v));\n}\n\nfunction applyRotation() {\n  if (!mesh) return;\n  mesh.rotation.x = THREE.MathUtils.degToRad(parseFloat(axes[0].slider.value));\n  mesh.rotation.y = THREE.MathUtils.degToRad(parseFloat(axes[1].slider.value));\n  mesh.rotation.z = THREE.MathUtils.degToRad(parseFloat(axes[2].slider.value));\n}\n\naxes.forEach(({ slider, num }) => {\n  slider.addEventListener(\"input\", () => { num.value = slider.value; applyRotation(); });\n  num.addEventListener(\"input\", () => {\n    const v = clamp180(parseFloat(num.value));\n    slider.value = v;\n    applyRotation();\n  });\n  num.addEventListener(\"blur\", () => { num.value = slider.value; });\n});\n\ndocument.getElementById(\"rot-reset\").addEventListener(\"click\", () => {\n  axes.forEach(({ slider, num }) => { slider.value = 0; num.value = 0; });\n  applyRotation();\n});\n\n\/\/ --- material controls ---\nconst reflectionSlider = document.getElementById(\"reflection\");\nconst reflVal = document.getElementById(\"refl-val\");\nconst colorPicker = document.getElementById(\"color-picker\");\n\nfunction applyMaterial() {\n  if (!mesh) return;\n  const refl = parseFloat(reflectionSlider.value) \/ 100;\n  mesh.material.metalness = refl;\n  mesh.material.roughness = 1 - refl * 0.7;\n  mesh.material.color.set(colorPicker.value);\n  mesh.material.needsUpdate = true;\n}\n\nreflectionSlider.addEventListener(\"input\", () => {\n  reflVal.textContent = reflectionSlider.value + \"%\";\n  applyMaterial();\n});\ncolorPicker.addEventListener(\"input\", applyMaterial);\n\n\/\/ --- geometry stats: size \/ volume \/ area \/ triangles ---\nfunction computeStats(geometry) {\n  geometry.computeBoundingBox();\n  const bb = geometry.boundingBox;\n  const sizeX = bb.max.x - bb.min.x;\n  const sizeY = bb.max.y - bb.min.y;\n  const sizeZ = bb.max.z - bb.min.z;\n\n  const pos = geometry.attributes.position;\n  const triCount = pos.count \/ 3;\n\n  let volume = 0;\n  let area = 0;\n  const pA = new THREE.Vector3(), pB = new THREE.Vector3(), pC = new THREE.Vector3();\n  const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), cross = new THREE.Vector3();\n\n  for (let i = 0; i < pos.count; i += 3) {\n    pA.fromBufferAttribute(pos, i);\n    pB.fromBufferAttribute(pos, i + 1);\n    pC.fromBufferAttribute(pos, i + 2);\n\n    \/\/ signed tetrahedron volume (origin, A, B, C)\n    volume += pA.dot(pB.clone().cross(pC)) \/ 6;\n\n    \/\/ triangle area\n    e1.subVectors(pB, pA);\n    e2.subVectors(pC, pA);\n    cross.crossVectors(e1, e2);\n    area += cross.length() \/ 2;\n  }\n  volume = Math.abs(volume);\n\n  return { sizeX, sizeY, sizeZ, volume, area, triCount };\n}\n\nfunction formatNum(v) {\n  return v.toLocaleString(\"en-US\", { minimumFractionDigits: 3, maximumFractionDigits: 3 });\n}\n\nfunction updateStatsPanel(geometry, filename) {\n  const s = computeStats(geometry);\n  document.getElementById(\"size-x\").value = s.sizeX.toFixed(3);\n  document.getElementById(\"size-y\").value = s.sizeY.toFixed(3);\n  document.getElementById(\"size-z\").value = s.sizeZ.toFixed(3);\n  document.getElementById(\"stat-volume\").textContent = formatNum(s.volume) + \" [mm\u00b3]\";\n  document.getElementById(\"stat-area\").textContent = formatNum(s.area) + \" [mm\u00b2]\";\n  document.getElementById(\"stat-triangles\").textContent = s.triCount.toLocaleString(\"en-US\");\n\n  const select = document.getElementById(\"object-select\");\n  select.innerHTML = \"\";\n  const opt = document.createElement(\"option\");\n  opt.value = filename;\n  opt.textContent = filename;\n  select.appendChild(opt);\n  select.value = filename;\n}\n\nfunction clearStatsPanel() {\n  [\"size-x\", \"size-y\", \"size-z\"].forEach(id => document.getElementById(id).value = \"\");\n  document.getElementById(\"stat-volume\").textContent = \"\u2014\";\n  document.getElementById(\"stat-area\").textContent = \"\u2014\";\n  document.getElementById(\"stat-triangles\").textContent = \"\u2014\";\n  const select = document.getElementById(\"object-select\");\n  select.innerHTML = '<option value=\"\">\u2014 no object \u2014<\/option>';\n}\n\n\/\/ --- STL loading ---\ndocument.getElementById(\"stl-input\").addEventListener(\"change\", e => {\n  const file = e.target.files[0];\n  if (!file) return;\n  const reader = new FileReader();\n  reader.onload = () => {\n    const loader = new STLLoader();\n    const geometry = loader.parse(reader.result);\n    geometry.center();\n    geometry.computeVertexNormals();\n    if (mesh) scene.remove(mesh);\n    mesh = new THREE.Mesh(\n      geometry,\n      new THREE.MeshStandardMaterial({\n        color: colorPicker.value,\n        metalness: parseFloat(reflectionSlider.value) \/ 100,\n        roughness: 1 - (parseFloat(reflectionSlider.value) \/ 100) * 0.7\n      })\n    );\n    scene.add(mesh);\n    axes.forEach(({ slider, num }) => { slider.value = 0; num.value = 0; });\n\n    updateStatsPanel(geometry, file.name);\n\n    geometry.computeBoundingSphere();\n    const r = geometry.boundingSphere.radius || 50;\n    camera.position.set(r * 1.8, r * 1.8, r * 2.5);\n    controls.target.set(0, 0, 0);\n    camera.near = r \/ 100;\n    camera.far = r * 100;\n    camera.updateProjectionMatrix();\n  };\n  reader.readAsArrayBuffer(file);\n});\n\nclearStatsPanel();\n\nfunction animate() {\n  requestAnimationFrame(animate);\n  controls.update();\n  renderer.render(scene, camera);\n}\nanimate();\n\nnew ResizeObserver(() => {\n  const { w, h } = getSize();\n  camera.aspect = w \/ h;\n  camera.updateProjectionMatrix();\n  renderer.setSize(w, h);\n}).observe(container);\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>This is a STL model loader test made with Chat GPT and Claude AI coding. Currently I am trainee in Fab Lab Oulu. You can download code from my github page. STL web viewer Load STL: Select object \u2014 no object \u2014 X Y Z Size [mm] Volume\u2014 Area\u2014 Triangles\u2014 Rotate X Y Z Reset [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-164","post","type-post","status-publish","format-standard","hentry","category-yleinen"],"_links":{"self":[{"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/posts\/164","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/comments?post=164"}],"version-history":[{"count":22,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/posts\/164\/revisions"}],"predecessor-version":[{"id":187,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/posts\/164\/revisions\/187"}],"wp:attachment":[{"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/media?parent=164"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/categories?post=164"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kraphik3d.com\/wordpress\/wp-json\/wp\/v2\/tags?post=164"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}