safety
71
create_glb.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# create_glb.py — Blender headless STL -> GLB
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# blender-bin-5.0.0 --background --python create_glb.py -- input.stl [output.glb]
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
def die(msg, rc=2):
|
||||||
|
print(f"ERROR: {msg}")
|
||||||
|
raise SystemExit(rc)
|
||||||
|
|
||||||
|
# args after "--"
|
||||||
|
argv = sys.argv
|
||||||
|
argv = argv[argv.index("--") + 1:] if "--" in argv else []
|
||||||
|
|
||||||
|
if len(argv) == 1:
|
||||||
|
inp = argv[0]
|
||||||
|
base, _ = os.path.splitext(inp)
|
||||||
|
outp = base + ".glb"
|
||||||
|
elif len(argv) >= 2:
|
||||||
|
inp, outp = argv[0], argv[1]
|
||||||
|
else:
|
||||||
|
die("USAGE: blender --background --python create_glb.py -- input.stl [output.glb]")
|
||||||
|
|
||||||
|
if not os.path.exists(inp):
|
||||||
|
die(f"Input not found: {inp}")
|
||||||
|
|
||||||
|
# Empty scene
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
|
||||||
|
# Import STL (Blender 4/5 operator)
|
||||||
|
res = bpy.ops.wm.stl_import(filepath=inp)
|
||||||
|
if 'FINISHED' not in res:
|
||||||
|
die(f"STL import failed for: {inp}")
|
||||||
|
|
||||||
|
# Gather imported mesh objects
|
||||||
|
objs = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||||
|
if not objs:
|
||||||
|
die("No mesh objects after import (unexpected)")
|
||||||
|
|
||||||
|
# Compute combined bounding box center in world space
|
||||||
|
min_v = Vector(( 1e30, 1e30, 1e30))
|
||||||
|
max_v = Vector((-1e30, -1e30, -1e30))
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
# object bound_box is in local coords; transform to world
|
||||||
|
for corner in o.bound_box:
|
||||||
|
v = o.matrix_world @ Vector(corner)
|
||||||
|
min_v.x = min(min_v.x, v.x); min_v.y = min(min_v.y, v.y); min_v.z = min(min_v.z, v.z)
|
||||||
|
max_v.x = max(max_v.x, v.x); max_v.y = max(max_v.y, v.y); max_v.z = max(max_v.z, v.z)
|
||||||
|
|
||||||
|
center = (min_v + max_v) * 0.5
|
||||||
|
|
||||||
|
# Translate all meshes so center is at origin
|
||||||
|
for o in objs:
|
||||||
|
o.location -= center
|
||||||
|
|
||||||
|
# Export GLB
|
||||||
|
res = bpy.ops.export_scene.gltf(
|
||||||
|
filepath=outp,
|
||||||
|
export_format='GLB',
|
||||||
|
export_apply=True,
|
||||||
|
)
|
||||||
|
if 'FINISHED' not in res:
|
||||||
|
die(f"GLB export failed: {outp}")
|
||||||
|
|
||||||
|
print(f"Wrote: {outp}")
|
||||||
14
creating_glbs.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Here is a command to create a single glb:
|
||||||
|
|
||||||
|
blender-bin-5.0.0 --background --python create_glb.py -- \
|
||||||
|
/usr/local/src/Voron-Stealthburner/STLs/Stealthburner/'[o]_stealthburner_LED_carrier.stl' \
|
||||||
|
/tmp/out.glb
|
||||||
|
|
||||||
|
The git repository for Voron-Stealthburner was staged under /usr/local/src. The above command
|
||||||
|
selects a specific STL and then places the STL under /tmp.
|
||||||
|
|
||||||
|
In a production mode, we want to be able to point to a directory tree of STLs and then
|
||||||
|
generate glb equivalents in the similar tree structure.
|
||||||
0
exercises/create_png_from_glb/README.md
Normal file
BIN
exercises/create_png_profile/20260304_101254_Wed.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
exercises/create_png_profile/20260304_101754_Wed.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
exercises/create_png_profile/20260304_102737_Wed.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
exercises/create_png_profile/20260304_102919_Wed.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
260
exercises/create_png_profile/README.md
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
# Goal
|
||||||
|
Create a "profile", i.e. a JSON file, to be used by the script that mass converts *.glb to *.png
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
This exercise only requires that you launch a small HTTP server in a console. Otherwise, everything involved is handled through the HTML page lab.html. You will interact with lab.html's 3D rendering of a glb file that is included with this project.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
Open a browser or a new window of the browser (Ctrl-n in Firefox) and resize the browser to a small rectangle. You are reducing the size so as to mimic what the PNG cell in the manifest table will look like. For example, this reduced window
|
||||||
|
is 521 × 432 pixels.
|
||||||
|

|
||||||
|
|
||||||
|
In a console:
|
||||||
|
|
||||||
|
cd ~/work/Voron/voronstl/web
|
||||||
|
python3 -m http.server 8001
|
||||||
|
|
||||||
|
You should have a console that looks like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It is necessary to start the web server within the "web" directory as that directory
|
||||||
|
will be servers "root".
|
||||||
|
|
||||||
|
|
||||||
|
Visit:
|
||||||
|
|
||||||
|
http://localhost:8001/lab.html
|
||||||
|
|
||||||
|
|
||||||
|
You will see a zoomed-in image:
|
||||||
|

|
||||||
|
|
||||||
|
Zoom out until the entire part fits within the window.
|
||||||
|
|
||||||
|
Click the Controls bar to collapse the sub menus.
|
||||||
|

|
||||||
|
Move the object to center it in the window: Shift + left mouse button. You want to have the entire part fit within the view and be cenetered.
|
||||||
|
|
||||||
|
Click Controls bar to open the sub menus. Adjust the lighintensity to a high value, if not the maximum values. This will cause the image to go lighter allowing for contrast with shadows that help discern the part.
|
||||||
|

|
||||||
|
Optional: Save the PNG for your own reference.
|
||||||
|
|
||||||
|
Click "Export Profile" and save your current settings.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You now have a specification on sizing and angles which may work well for all of the other parts. Note: I took mine and applied the specifications saved above for a mass PNG creation and all the others looked very good.
|
||||||
|
|
||||||
|
### Additional Information:
|
||||||
|
Here's what a JSON file looks like:
|
||||||
|
|
||||||
|
<table style="width:100%; table-layout:fixed;">
|
||||||
|
<tr>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="width:50%; vertical-align:top;">
|
||||||
|
<img src="20260304_102919_Wed.png" >
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style="width:50%; vertical-align:top;"><pre style="white-space:pre-wrap; overflow:auto; max-width:100%;">jlpoole@jp ~/work/Voron/voronstl/web $ jq . out/three_profile_20260304_102657.json
|
||||||
|
{
|
||||||
|
"provenance": "lab.html exportProfile Wed Mar 04 2026 10:26:57 GMT-0800 (Pacific Standard Time)",
|
||||||
|
"output": {
|
||||||
|
"width": 500,
|
||||||
|
"height": 315,
|
||||||
|
"pixelRatio": 1
|
||||||
|
},
|
||||||
|
"scene": {
|
||||||
|
"background": 16777215
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"type": "PerspectiveCamera",
|
||||||
|
"fov": 50,
|
||||||
|
"near": 0.1,
|
||||||
|
"far": 1000,
|
||||||
|
"position": [
|
||||||
|
11.93432933230491,
|
||||||
|
11.71721921868296,
|
||||||
|
13.265804443912849
|
||||||
|
],
|
||||||
|
"up": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"target": [
|
||||||
|
2.075876663103527,
|
||||||
|
-2.147814989328729,
|
||||||
|
0.7122034073683343
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renderParams": {
|
||||||
|
"wireframe": false,
|
||||||
|
"edgeAngle": 30,
|
||||||
|
"lightIntensity": 1.5
|
||||||
|
},
|
||||||
|
"lights": {
|
||||||
|
"directional": {
|
||||||
|
"position": [
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"intensity": 1.5
|
||||||
|
},
|
||||||
|
"ambient": {
|
||||||
|
"intensity": 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jlpoole@jp ~/work/Voron/voronstl/web $
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## ChatGPT's analysis of what is important for consistency
|
||||||
|
|
||||||
|
Here is a response from ChatGPT that summarizes what variables are captured and saved so that they can be used to repeat the same type of transformation of *.glb to PNG.
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
Good — now you’re at the point where it’s worth making the capture **repeatable** across a whole directory of GLBs.
|
||||||
|
|
||||||
|
In practice, you want to “freeze” *everything that affects the pixels* and treat the GLB as the only variable.
|
||||||
|
|
||||||
|
## What to save (so every PNG is consistent)
|
||||||
|
|
||||||
|
### 1) Camera pose and projection
|
||||||
|
|
||||||
|
Save:
|
||||||
|
|
||||||
|
* `camera.position` (x,y,z)
|
||||||
|
* `camera.up` (x,y,z)
|
||||||
|
* `controls.target` (x,y,z) if you’re using OrbitControls
|
||||||
|
* `camera.fov`, `camera.near`, `camera.far`
|
||||||
|
* (optional) whether you used perspective vs orthographic
|
||||||
|
|
||||||
|
These define “where the viewer is” and are the biggest determinant of repeatability.
|
||||||
|
|
||||||
|
### 2) Output framing rules (per-model normalization)
|
||||||
|
|
||||||
|
You need a deterministic rule for “how big the part appears in frame”, otherwise parts with different sizes will look random.
|
||||||
|
|
||||||
|
Two common strategies:
|
||||||
|
|
||||||
|
**A. Fit-to-frame (recommended for mass renders)**
|
||||||
|
For each GLB:
|
||||||
|
|
||||||
|
* compute `Box3` bounds
|
||||||
|
* compute `center` and `size`
|
||||||
|
* set `controls.target = center`
|
||||||
|
* set camera distance so the whole bounding box fits (using FOV + max dimension)
|
||||||
|
* optionally lock azimuth/elevation (so every part is viewed from the same angle)
|
||||||
|
|
||||||
|
This yields consistent “fill” and eliminates manual tuning.
|
||||||
|
|
||||||
|
**B. Fixed camera (only if all parts are same scale and you want exact same viewpoint)**
|
||||||
|
Works if your GLBs are all in the same coordinate system/scale and similarly sized.
|
||||||
|
|
||||||
|
### 3) Lighting rig
|
||||||
|
|
||||||
|
Save:
|
||||||
|
|
||||||
|
* light types (Directional/Hemisphere/Ambient)
|
||||||
|
* each light’s `position`, `intensity`, `color`
|
||||||
|
* shadow settings if used
|
||||||
|
|
||||||
|
Your current screenshot shows a simple shaded render; keep lighting stable or you’ll get different highlight/shadow shapes across parts.
|
||||||
|
|
||||||
|
### 4) Renderer and capture settings
|
||||||
|
|
||||||
|
Save:
|
||||||
|
|
||||||
|
* `width`, `height`
|
||||||
|
* `devicePixelRatio` you used (often set explicitly for determinism)
|
||||||
|
* `renderer.toneMapping`, `outputColorSpace` (Three.js r152+ uses `renderer.outputColorSpace = THREE.SRGBColorSpace`)
|
||||||
|
* `renderer.setClearColor()` (white) and alpha behavior
|
||||||
|
* `preserveDrawingBuffer: true` (for browser capture)
|
||||||
|
* whether you use `physicallyCorrectLights` (if you do, keep it constant)
|
||||||
|
|
||||||
|
### 5) Material overrides / wireframe settings
|
||||||
|
|
||||||
|
If you’re overriding materials for “clay render” or wireframe, save:
|
||||||
|
|
||||||
|
* your override material parameters (`MeshStandardMaterial` vs `MeshPhongMaterial`, roughness/metalness)
|
||||||
|
* edge/wire settings (`EdgesGeometry` threshold angle, line material linewidth/color)
|
||||||
|
|
||||||
|
### 6) Background / staging
|
||||||
|
|
||||||
|
Save:
|
||||||
|
|
||||||
|
* background color (white)
|
||||||
|
* whether you add a ground plane, axes, environment map, etc.
|
||||||
|
|
||||||
|
Even “invisible” helpers can affect auto-framing if they’re included in bounds—so exclude them from bounds computation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The best thing to save: a small “render profile” JSON
|
||||||
|
|
||||||
|
Put this in a file like `render_profile.json`:
|
||||||
|
|
||||||
|
* canvas size
|
||||||
|
* camera angle (azimuth/elevation) OR explicit camera position
|
||||||
|
* fit-to-frame padding factor (e.g., 1.15)
|
||||||
|
* light positions/intensities
|
||||||
|
* material mode (`source`, `clay`, `wireframe`, `edges`)
|
||||||
|
* background color
|
||||||
|
|
||||||
|
Then your batch renderer (browser or headless) loads profile + GLB and produces PNG.
|
||||||
|
|
||||||
|
Example shape (conceptually):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"output": { "w": 1600, "h": 1200, "pixelRatio": 1 },
|
||||||
|
"background": "#ffffff",
|
||||||
|
"camera": { "fov": 35, "near": 0.01, "far": 1000, "azimuth": -35, "elevation": 20, "fitPadding": 1.15 },
|
||||||
|
"lights": [
|
||||||
|
{ "type": "dir", "pos": [3,5,2], "intensity": 2.8 },
|
||||||
|
{ "type": "hem", "sky": "#ffffff", "ground": "#666666", "intensity": 0.6 }
|
||||||
|
],
|
||||||
|
"materialMode": "clay"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to mass-create PNGs (two viable routes)
|
||||||
|
|
||||||
|
### Route 1: Headless batch (best)
|
||||||
|
|
||||||
|
Use **node + headless WebGL** (or Puppeteer driving Chrome) to render each GLB with the profile and write PNGs. This is what you want if you’re doing hundreds/thousands.
|
||||||
|
|
||||||
|
### Route 2: Browser batch page (simpler)
|
||||||
|
|
||||||
|
A page that:
|
||||||
|
|
||||||
|
* loads a list of GLB URLs
|
||||||
|
* applies the profile
|
||||||
|
* renders each, downloads PNG automatically (or zips them)
|
||||||
|
|
||||||
|
This is simpler but less pleasant at scale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## One key question that determines the “right” profile
|
||||||
|
|
||||||
|
Do you want your PNGs to be:
|
||||||
|
|
||||||
|
* **same camera angle + fit-to-frame** (parts all fill the image similarly), or
|
||||||
|
* **same absolute camera** (so relative size differences are visible)?
|
||||||
|
|
||||||
|
If you tell me which you want, I’ll give you a concrete “profile JSON + camera fit math + batch driver” that matches it.
|
||||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
104
lab.html
|
|
@ -1,104 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>RenderLab v1</title>
|
|
||||||
<style>
|
|
||||||
body { margin:0; background:white; }
|
|
||||||
canvas { display:block; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="module">
|
|
||||||
|
|
||||||
import * as THREE from './threejs/three.module.js';
|
|
||||||
import { GLTFLoader } from './threejs/GLTFLoader.js';
|
|
||||||
import { OrbitControls } from './threejs/OrbitControls.js';
|
|
||||||
import GUI from './threejs/lil-gui.module.min.js';
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
|
||||||
scene.background = new THREE.Color(0xffffff);
|
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 1000);
|
|
||||||
camera.position.set(3,3,3);
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias:true });
|
|
||||||
renderer.setSize(innerWidth, innerHeight);
|
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
|
||||||
document.body.appendChild(renderer.domElement);
|
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
|
||||||
|
|
||||||
const light = new THREE.DirectionalLight(0xffffff, 1.5);
|
|
||||||
light.position.set(5,5,5);
|
|
||||||
scene.add(light);
|
|
||||||
|
|
||||||
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
||||||
scene.add(ambient);
|
|
||||||
|
|
||||||
let mesh, edgeLines;
|
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
|
||||||
loader.load('./glb/part.glb', gltf => {
|
|
||||||
|
|
||||||
mesh = gltf.scene.children[0];
|
|
||||||
|
|
||||||
mesh.material = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xdddddd,
|
|
||||||
roughness: 0.9,
|
|
||||||
metalness: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
scene.add(mesh);
|
|
||||||
addEdges(30);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function addEdges(angle) {
|
|
||||||
if (!mesh) return;
|
|
||||||
if (edgeLines) scene.remove(edgeLines);
|
|
||||||
|
|
||||||
const edges = new THREE.EdgesGeometry(mesh.geometry, angle);
|
|
||||||
edgeLines = new THREE.LineSegments(
|
|
||||||
edges,
|
|
||||||
new THREE.LineBasicMaterial({ color: 0x000000 })
|
|
||||||
);
|
|
||||||
|
|
||||||
scene.add(edgeLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
wireframe: false,
|
|
||||||
edgeAngle: 30,
|
|
||||||
lightIntensity: 1.5,
|
|
||||||
savePNG: () => {
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = 'render.png';
|
|
||||||
link.href = renderer.domElement.toDataURL("image/png");
|
|
||||||
link.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const gui = new GUI();
|
|
||||||
gui.add(params, 'wireframe').onChange(v => {
|
|
||||||
if (mesh) mesh.material.wireframe = v;
|
|
||||||
});
|
|
||||||
gui.add(params, 'edgeAngle', 1, 90).onChange(v => addEdges(v));
|
|
||||||
gui.add(params, 'lightIntensity', 0, 3).onChange(v => light.intensity = v);
|
|
||||||
gui.add(params, 'savePNG');
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
camera.aspect = innerWidth/innerHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
renderer.setSize(innerWidth, innerHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}
|
|
||||||
animate();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
38
mv_themes.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// mv_theme.js
|
||||||
|
// Apply consistent colors for manifest viewing.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// <model-viewer class="mv" data-theme="primary" ...></model-viewer>
|
||||||
|
// <model-viewer class="mv" data-theme="accent" ...></model-viewer>
|
||||||
|
|
||||||
|
const THEMES = {
|
||||||
|
primary: { rgba: [0.10, 0.45, 1.00, 1.0] }, // diagnostic blue
|
||||||
|
accent: { rgba: [0.78, 0.33, 0.10, 1.0] }, // burnt orange-ish
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyTheme(mv) {
|
||||||
|
const key = (mv.dataset.theme || "primary").toLowerCase();
|
||||||
|
const theme = THEMES[key] || THEMES.primary;
|
||||||
|
|
||||||
|
// Lighting-related material choices that improve face contrast
|
||||||
|
const metallic = 0.0;
|
||||||
|
const roughness = 0.35;
|
||||||
|
|
||||||
|
for (const mat of mv.model.materials) {
|
||||||
|
mat.pbrMetallicRoughness.setBaseColorFactor(theme.rgba);
|
||||||
|
mat.pbrMetallicRoughness.setMetallicFactor(metallic);
|
||||||
|
mat.pbrMetallicRoughness.setRoughnessFactor(roughness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hook(mv) {
|
||||||
|
if (mv.model) {
|
||||||
|
applyTheme(mv);
|
||||||
|
} else {
|
||||||
|
mv.addEventListener("load", () => applyTheme(mv), { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mv of document.querySelectorAll("model-viewer.mv")) {
|
||||||
|
hook(mv);
|
||||||
|
}
|
||||||
6
simple_server.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Use this command to run a simple little server for the HTML:
|
||||||
|
|
||||||
|
python3 -m http.server 8000
|
||||||
|
|
||||||
|
The directory the command above is run in is the root directory. Then
|
||||||
|
http://127.0.0.1:8000/[FILE NAME]
|
||||||
BIN
web/[o]_stealthburner_LED_carrier.stl.glb
Normal file
73
web/assets/beaker.svg
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 -0.08 45 45"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="beaker.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.09625"
|
||||||
|
inkscape:cx="399.5439"
|
||||||
|
inkscape:cy="400"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1128"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
id="Path_74"
|
||||||
|
data-name="Path 74"
|
||||||
|
d="m 15.693226,14.498953 h 16.333 v 6.166 l 4.907,8.793 6.139,11 -5.379,1.71 H 3.6932258 l 3.5,-7 7.2280002,-13.417 z"
|
||||||
|
fill="#d1d3d4"
|
||||||
|
style="fill:#ff413f;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
id="Path_75"
|
||||||
|
data-name="Path 75"
|
||||||
|
d="M 43,42.832 31,21.332 V 2 H 14 v 19.335 l -12,21.5 z"
|
||||||
|
fill="none"
|
||||||
|
stroke="#231f20"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="4" />
|
||||||
|
<line
|
||||||
|
id="Line_37"
|
||||||
|
data-name="Line 37"
|
||||||
|
x2="35.332001"
|
||||||
|
fill="#ffffff"
|
||||||
|
stroke="#231f20"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="4"
|
||||||
|
x1="9.665"
|
||||||
|
y1="2"
|
||||||
|
y2="2" />
|
||||||
|
<path
|
||||||
|
id="Path_76"
|
||||||
|
data-name="Path 76"
|
||||||
|
d="M 18.5,34.16 A 2.333,2.333 0 1 1 16.166,31.827 2.334,2.334 0 0 1 18.5,34.16 Z"
|
||||||
|
fill="#ffffff" />
|
||||||
|
<path
|
||||||
|
id="Path_77"
|
||||||
|
data-name="Path 77"
|
||||||
|
d="m 29.907,29.701 a 3.7,3.7 0 1 1 -3.7,-3.7 3.7,3.7 0 0 1 3.7,3.7 z"
|
||||||
|
fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
web/assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
web/assets/favicon-16.png
Normal file
|
After Width: | Height: | Size: 567 B |
BIN
web/assets/favicon-256.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
web/assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
web/assets/favicon-48.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web/assets/favicon-64.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
web/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 105 KiB |
3
web/batch_glb_png/glbs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
!README.md
|
||||||
3
web/batch_glb_png/pngs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!README.md
|
||||||
|
!.gitignore
|
||||||
BIN
web/favicon.ico
Normal file
|
After Width: | Height: | Size: 105 KiB |
14
web/lab.html
|
|
@ -1,8 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
<!-- To run, start a local server:
|
||||||
|
python3 -m http.server 8001
|
||||||
|
Then open http://localhost:8001/lab.html in a modern browser.
|
||||||
|
-->
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>RenderLab v1</title>
|
<title>RenderLab: PNG Profile Exporter</title>
|
||||||
<style>
|
<style>
|
||||||
body { margin:0; background:white; }
|
body { margin:0; background:white; }
|
||||||
canvas { display:block; }
|
canvas { display:block; }
|
||||||
|
|
@ -54,7 +58,13 @@ scene.add(ambient);
|
||||||
let mesh, edgeLines;
|
let mesh, edgeLines;
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
loader.load('./glb/part.glb', gltf => {
|
|
||||||
|
//
|
||||||
|
// This part is particularly tricky to view, so it makes an excellent test case.
|
||||||
|
//
|
||||||
|
// ./Voron-Stealthburner/STLs/Stealthburner/[o]_stealthburner_LED_carrier.stl.glb
|
||||||
|
//
|
||||||
|
loader.load('[o]_stealthburner_LED_carrier.stl.glb', gltf => {
|
||||||
|
|
||||||
mesh = gltf.scene.children[0];
|
mesh = gltf.scene.children[0];
|
||||||
|
|
||||||
|
|
|
||||||
2
web/out/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||