Compare commits
No commits in common. "05d0e3fbc076e276bbcb1520d4674439c27f1e46" and "41a32b8f6843fe636f53529082b72c11dd6ae142" have entirely different histories.
05d0e3fbc0
...
41a32b8f68
10
README.md
|
|
@ -1,11 +1,3 @@
|
|||
# voronstl
|
||||
|
||||
Voron Project STL creating customer manifests where parts are easily discernible.
|
||||
|
||||
# Directories
|
||||
You will have three directory trees:
|
||||
1) Voron Directory - this is a directory you create and then perform "git clone..." for each of the Voron projects you need STL files from. The files of interest in the tree are the STL files (warning: not all STL files have lowercase .stl suffixes). This is a read-only (git updatable) directory and the STL files there are the source of truth.
|
||||
2) Staging Area - this is a directory you create where the links to the STL files within Voron Root Directory. The links are created under an identical tree structure. The Staging area also will also hold the *.glb files that are created from the STLs using Blender. This area will also hold the final HTML and its PNGs copied from the Project Code Area's pngs folder.
|
||||
3) Project Code Area - this project's git clone directory. Note: there are two working directories were interim links to glbs and files, generated PNG images, are stored:
|
||||
A) ...web/batch_glb_png/glbs - this is where links to the glb files (originals in the Staging Area) are staged
|
||||
B) ...web/batch_glb_png/pngs - these are the PNG files created from the glbs [links]
|
||||
Voron Project STL creating customer manifests where parts are easily discernible.
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# 20260228 ChatGPT
|
||||
# $Header$
|
||||
# $Id$
|
||||
#
|
||||
# SCRIPT_ID: render_glb_variants.py
|
||||
# SCRIPT_VERSION: 6
|
||||
#
|
||||
# Goal:
|
||||
# Headless-safe PNG rendering for GLB models in environments where EGL/OpenGL may crash.
|
||||
# Uses Cycles CPU (no Eevee/Workbench).
|
||||
#
|
||||
# Usage:
|
||||
# blender -b -P blender/render_glb_variants.py -- /path/to/part.glb /path/to/out_prefix
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
import math
|
||||
from mathutils import Vector
|
||||
|
||||
SCRIPT_ID = "render_glb_variants.py"
|
||||
SCRIPT_VERSION = 6
|
||||
|
||||
def log(msg):
|
||||
print(f"{SCRIPT_ID} v{SCRIPT_VERSION}: {msg}")
|
||||
|
||||
def die(msg):
|
||||
print(f"{SCRIPT_ID} v{SCRIPT_VERSION} ERROR: {msg}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
def argv_after_double_dash():
|
||||
if "--" not in sys.argv:
|
||||
return []
|
||||
return sys.argv[sys.argv.index("--") + 1:]
|
||||
|
||||
def reset_scene():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
scene = bpy.context.scene
|
||||
|
||||
scene.render.engine = 'CYCLES'
|
||||
# Force CPU
|
||||
scene.cycles.device = 'CPU'
|
||||
# Fast-ish defaults for documentation renders
|
||||
scene.cycles.samples = 32
|
||||
scene.cycles.preview_samples = 16
|
||||
scene.cycles.use_denoising = True
|
||||
|
||||
scene.render.resolution_x = 1200
|
||||
scene.render.resolution_y = 1200
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
scene.render.film_transparent = False
|
||||
scene.render.use_freestyle = False
|
||||
|
||||
return scene
|
||||
|
||||
def set_world_background_white(scene, strength=1.0):
|
||||
if scene.world is None:
|
||||
scene.world = bpy.data.worlds.new("World")
|
||||
world = scene.world
|
||||
world.use_nodes = True
|
||||
nt = world.node_tree
|
||||
nt.nodes.clear()
|
||||
out = nt.nodes.new("ShaderNodeOutputWorld")
|
||||
bg = nt.nodes.new("ShaderNodeBackground")
|
||||
bg.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0)
|
||||
bg.inputs["Strength"].default_value = strength
|
||||
nt.links.new(bg.outputs["Background"], out.inputs["Surface"])
|
||||
|
||||
def import_glb(path):
|
||||
before = set(bpy.data.objects)
|
||||
bpy.ops.import_scene.gltf(filepath=path)
|
||||
after = set(bpy.data.objects)
|
||||
new_objs = [o for o in (after - before)]
|
||||
meshes = [o for o in new_objs if o.type == 'MESH']
|
||||
if not meshes:
|
||||
meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||
if not meshes:
|
||||
die(f"Failed to import GLB (no mesh objects found): {path}")
|
||||
|
||||
if len(meshes) > 1:
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in meshes:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.view_layer.objects.active
|
||||
else:
|
||||
obj = meshes[0]
|
||||
|
||||
obj.name = "PART"
|
||||
return obj
|
||||
|
||||
def center_origin_and_location(obj):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
|
||||
obj.location = (0, 0, 0)
|
||||
|
||||
def bounds_world(obj):
|
||||
mw = obj.matrix_world
|
||||
corners = [mw @ Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]; ys = [c.y for c in corners]; zs = [c.z for c in corners]
|
||||
min_v = Vector((min(xs), min(ys), min(zs)))
|
||||
max_v = Vector((max(xs), max(ys), max(zs)))
|
||||
size = max_v - min_v
|
||||
center = (min_v + max_v) / 2.0
|
||||
return center, size
|
||||
|
||||
def ensure_camera(scene):
|
||||
cam_data = bpy.data.cameras.new("OrthoCam")
|
||||
cam_data.type = 'ORTHO'
|
||||
cam = bpy.data.objects.new("OrthoCam", cam_data)
|
||||
scene.collection.objects.link(cam)
|
||||
scene.camera = cam
|
||||
return cam
|
||||
|
||||
def ensure_light_rig(scene):
|
||||
# Cycles likes real lights
|
||||
def add_area(name, loc, power):
|
||||
ld = bpy.data.lights.new(name, type='AREA')
|
||||
ld.energy = power
|
||||
lo = bpy.data.objects.new(name, ld)
|
||||
lo.location = loc
|
||||
scene.collection.objects.link(lo)
|
||||
add_area("Key", (3, -3, 3), 800)
|
||||
add_area("Fill", (-3, 3, 2), 400)
|
||||
add_area("Top", (0, 0, 4), 300)
|
||||
|
||||
def set_isometric_camera(cam):
|
||||
cam.location = (3.0, -3.0, 3.0)
|
||||
cam.rotation_euler = (math.radians(55), 0, math.radians(45))
|
||||
|
||||
def fit_camera_to_object(cam, obj, margin=1.20):
|
||||
_, size = bounds_world(obj)
|
||||
max_dim = max(size.x, size.y, size.z)
|
||||
cam.data.ortho_scale = max_dim * margin
|
||||
|
||||
def set_input_if_exists(node, socket_name, value):
|
||||
if socket_name in node.inputs:
|
||||
node.inputs[socket_name].default_value = value
|
||||
return True
|
||||
return False
|
||||
|
||||
def make_material_clay(name="Clay", color=(0.83, 0.83, 0.83, 1.0)):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
nt = mat.node_tree
|
||||
nt.nodes.clear()
|
||||
|
||||
out = nt.nodes.new("ShaderNodeOutputMaterial")
|
||||
bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
|
||||
set_input_if_exists(bsdf, "Base Color", color)
|
||||
set_input_if_exists(bsdf, "Roughness", 1.0)
|
||||
if not set_input_if_exists(bsdf, "Specular", 0.0):
|
||||
set_input_if_exists(bsdf, "Specular IOR Level", 0.0)
|
||||
|
||||
nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
|
||||
return mat
|
||||
|
||||
def make_material_black_emission(name="WireBlack"):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
nt = mat.node_tree
|
||||
nt.nodes.clear()
|
||||
|
||||
out = nt.nodes.new("ShaderNodeOutputMaterial")
|
||||
em = nt.nodes.new("ShaderNodeEmission")
|
||||
em.inputs["Color"].default_value = (0, 0, 0, 1)
|
||||
em.inputs["Strength"].default_value = 1.0
|
||||
nt.links.new(em.outputs["Emission"], out.inputs["Surface"])
|
||||
return mat
|
||||
|
||||
def make_material_feature_ao(name="FeatureAO"):
|
||||
"""
|
||||
Cavity-like look using Ambient Occlusion node when available.
|
||||
Falls back to normal-vector shading if AO node isn't available.
|
||||
"""
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
nt = mat.node_tree
|
||||
nt.nodes.clear()
|
||||
|
||||
out = nt.nodes.new("ShaderNodeOutputMaterial")
|
||||
bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
set_input_if_exists(bsdf, "Roughness", 1.0)
|
||||
if not set_input_if_exists(bsdf, "Specular", 0.0):
|
||||
set_input_if_exists(bsdf, "Specular IOR Level", 0.0)
|
||||
|
||||
# Try AO node
|
||||
try:
|
||||
ao = nt.nodes.new("ShaderNodeAmbientOcclusion")
|
||||
ao.inputs["Distance"].default_value = 0.2
|
||||
ramp = nt.nodes.new("ShaderNodeValToRGB")
|
||||
# Slight contrast boost
|
||||
ramp.color_ramp.elements[0].position = 0.35
|
||||
ramp.color_ramp.elements[1].position = 0.85
|
||||
nt.links.new(ao.outputs["AO"], ramp.inputs["Fac"])
|
||||
nt.links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
|
||||
except Exception:
|
||||
# Fallback: normals shading
|
||||
geo = nt.nodes.new("ShaderNodeNewGeometry")
|
||||
add = nt.nodes.new("ShaderNodeVectorMath"); add.operation = 'ADD'
|
||||
add.inputs[1].default_value = (1.0, 1.0, 1.0)
|
||||
mul = nt.nodes.new("ShaderNodeVectorMath"); mul.operation = 'MULTIPLY'
|
||||
mul.inputs[1].default_value = (0.5, 0.5, 0.5)
|
||||
nt.links.new(geo.outputs["Normal"], add.inputs[0])
|
||||
nt.links.new(add.outputs["Vector"], mul.inputs[0])
|
||||
nt.links.new(mul.outputs["Vector"], bsdf.inputs["Base Color"])
|
||||
|
||||
nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
|
||||
return mat
|
||||
|
||||
def assign_single_material(obj, mat):
|
||||
obj.data.materials.clear()
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
def duplicate_object(obj, name):
|
||||
dup = obj.copy()
|
||||
dup.data = obj.data.copy()
|
||||
dup.name = name
|
||||
bpy.context.scene.collection.objects.link(dup)
|
||||
return dup
|
||||
|
||||
def delete_object(obj):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.ops.object.delete()
|
||||
|
||||
def enable_freestyle(scene, thickness=1.0):
|
||||
scene.render.use_freestyle = True
|
||||
vl = scene.view_layers[0]
|
||||
fs = vl.freestyle_settings
|
||||
if not fs.linesets:
|
||||
fs.linesets.new("LineSet")
|
||||
ls = fs.linesets[0]
|
||||
try:
|
||||
ls.linestyle.thickness = thickness
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def render(scene, outpath):
|
||||
scene.render.filepath = outpath
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
def main():
|
||||
log("starting")
|
||||
args = argv_after_double_dash()
|
||||
if len(args) != 2:
|
||||
die("Expected: <input.glb> <out_prefix>")
|
||||
|
||||
in_glb, out_prefix = args
|
||||
scene = reset_scene()
|
||||
set_world_background_white(scene)
|
||||
|
||||
obj = import_glb(in_glb)
|
||||
center_origin_and_location(obj)
|
||||
|
||||
ensure_light_rig(scene)
|
||||
cam = ensure_camera(scene)
|
||||
set_isometric_camera(cam)
|
||||
fit_camera_to_object(cam, obj, margin=1.20)
|
||||
|
||||
clay = make_material_clay()
|
||||
wire_black = make_material_black_emission()
|
||||
feature = make_material_feature_ao()
|
||||
|
||||
# 1) SOLID
|
||||
scene.render.use_freestyle = False
|
||||
assign_single_material(obj, clay)
|
||||
render(scene, f"{out_prefix}_solid.png")
|
||||
|
||||
# 2) SOLID + EDGES
|
||||
scene.render.use_freestyle = False
|
||||
assign_single_material(obj, clay)
|
||||
enable_freestyle(scene, thickness=1.0)
|
||||
render(scene, f"{out_prefix}_solid_edges.png")
|
||||
|
||||
# 3) PURE WIRE
|
||||
scene.render.use_freestyle = False
|
||||
wire_obj = duplicate_object(obj, "PART_WIRE")
|
||||
obj.hide_render = True
|
||||
wire_obj.hide_render = False
|
||||
assign_single_material(wire_obj, wire_black)
|
||||
mod = wire_obj.modifiers.new("Wireframe", type='WIREFRAME')
|
||||
mod.thickness = 0.0015
|
||||
mod.use_replace = True
|
||||
mod.use_boundary = True
|
||||
render(scene, f"{out_prefix}_wire.png")
|
||||
obj.hide_render = False
|
||||
delete_object(wire_obj)
|
||||
|
||||
# 4) FEATURE POP (AO/cavity-ish)
|
||||
scene.render.use_freestyle = False
|
||||
assign_single_material(obj, feature)
|
||||
render(scene, f"{out_prefix}_feature.png")
|
||||
|
||||
# 5) FREESTYLE ONLY
|
||||
scene.render.use_freestyle = False
|
||||
white = make_material_clay(name="WhiteClay", color=(1.0, 1.0, 1.0, 1.0))
|
||||
assign_single_material(obj, white)
|
||||
enable_freestyle(scene, thickness=1.2)
|
||||
render(scene, f"{out_prefix}_freestyle.png")
|
||||
|
||||
log("done")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# 20260228 ChatGPT
|
||||
# $Header$
|
||||
# $Id$
|
||||
#
|
||||
# Usage:
|
||||
# blender -b -P render_stl_variants.py -- /path/to/part.stl /path/to/out_prefix
|
||||
#
|
||||
# Output:
|
||||
# <out_prefix>_solid.png
|
||||
# <out_prefix>_solid_edges.png
|
||||
# <out_prefix>_wire.png
|
||||
# <out_prefix>_matcap.png
|
||||
# <out_prefix>_freestyle.png
|
||||
#
|
||||
# Notes:
|
||||
# - Uses orthographic camera + fit-to-bounds framing (consistent prints).
|
||||
# - Workbench engine for fast technical-looking renders.
|
||||
# - Freestyle enabled only for the freestyle variant.
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
import math
|
||||
from mathutils import Vector
|
||||
|
||||
def die(msg):
|
||||
print(msg, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
def argv_after_double_dash():
|
||||
if "--" not in sys.argv:
|
||||
return []
|
||||
return sys.argv[sys.argv.index("--") + 1:]
|
||||
|
||||
def reset_scene():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
scene = bpy.context.scene
|
||||
scene.unit_settings.system = 'NONE'
|
||||
scene.render.resolution_x = 1200
|
||||
scene.render.resolution_y = 1200
|
||||
scene.render.film_transparent = False
|
||||
scene.render.image_settings.file_format = 'PNG'
|
||||
return scene
|
||||
|
||||
def import_stl(path):
|
||||
bpy.ops.import_mesh.stl(filepath=path)
|
||||
objs = [o for o in bpy.context.selected_objects if o.type == 'MESH']
|
||||
if not objs:
|
||||
die(f"Failed to import STL: {path}")
|
||||
# join into one object for consistent bounds
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.view_layer.objects.active
|
||||
obj.name = "PART"
|
||||
return obj
|
||||
|
||||
def center_origin_and_location(obj):
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
|
||||
obj.location = (0, 0, 0)
|
||||
|
||||
def bounds_world(obj):
|
||||
# bounding box corners in world coordinates
|
||||
mw = obj.matrix_world
|
||||
corners = [mw @ Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]; ys = [c.y for c in corners]; zs = [c.z for c in corners]
|
||||
min_v = Vector((min(xs), min(ys), min(zs)))
|
||||
max_v = Vector((max(xs), max(ys), max(zs)))
|
||||
size = max_v - min_v
|
||||
center = (min_v + max_v) / 2.0
|
||||
return center, size
|
||||
|
||||
def ensure_camera(scene):
|
||||
cam_data = bpy.data.cameras.new("OrthoCam")
|
||||
cam_data.type = 'ORTHO'
|
||||
cam = bpy.data.objects.new("OrthoCam", cam_data)
|
||||
scene.collection.objects.link(cam)
|
||||
scene.camera = cam
|
||||
return cam
|
||||
|
||||
def ensure_light_rig(scene):
|
||||
# For workbench it barely matters, but keep a light for non-workbench fallback
|
||||
ld = bpy.data.lights.new("Key", type='AREA')
|
||||
lo = bpy.data.objects.new("Key", ld)
|
||||
lo.location = (3, -3, 3)
|
||||
scene.collection.objects.link(lo)
|
||||
|
||||
def set_isometric_camera(cam, target=(0,0,0), ortho_scale=2.0):
|
||||
cam.data.ortho_scale = ortho_scale
|
||||
cam.location = (3.0, -3.0, 3.0)
|
||||
cam.rotation_euler = (math.radians(55), 0, math.radians(45))
|
||||
|
||||
def fit_camera_to_object(cam, obj, margin=1.15):
|
||||
center, size = bounds_world(obj)
|
||||
max_dim = max(size.x, size.y, size.z)
|
||||
# ortho_scale roughly maps to view width; margin gives padding
|
||||
cam.data.ortho_scale = max_dim * margin
|
||||
|
||||
def set_world_background(scene, gray=1.0):
|
||||
# Simple white/gray background via World color
|
||||
if scene.world is None:
|
||||
scene.world = bpy.data.worlds.new("World")
|
||||
scene.world.use_nodes = False
|
||||
scene.world.color = (gray, gray, gray)
|
||||
|
||||
def workbench_common(scene):
|
||||
scene.render.engine = 'BLENDER_WORKBENCH'
|
||||
scene.display.shading.light = 'STUDIO'
|
||||
scene.display.shading.color_type = 'SINGLE'
|
||||
scene.display.shading.single_color = (0.82, 0.82, 0.82) # light gray “clay”
|
||||
scene.display.shading.show_backface_culling = False
|
||||
scene.display.shading.show_xray = False
|
||||
|
||||
def variant_solid(scene):
|
||||
workbench_common(scene)
|
||||
scene.display.shading.show_wireframes = False
|
||||
|
||||
def variant_wire(scene):
|
||||
workbench_common(scene)
|
||||
scene.display.shading.show_wireframes = True
|
||||
scene.display.shading.wireframe_color_type = 'SINGLE'
|
||||
scene.display.shading.wireframe_color = (0.0, 0.0, 0.0)
|
||||
scene.display.shading.wireframe_opacity = 1.0
|
||||
scene.display.shading.wireframe_thickness = 1
|
||||
|
||||
def variant_solid_edges(scene):
|
||||
# Workbench wire overlay on top of solid
|
||||
workbench_common(scene)
|
||||
scene.display.shading.show_wireframes = True
|
||||
scene.display.shading.wireframe_color_type = 'SINGLE'
|
||||
scene.display.shading.wireframe_color = (0.0, 0.0, 0.0)
|
||||
scene.display.shading.wireframe_opacity = 1.0
|
||||
scene.display.shading.wireframe_thickness = 1
|
||||
|
||||
def variant_matcap(scene):
|
||||
# Workbench “Matcap-like” studio + cavity (helps valleys/holes read on paper)
|
||||
workbench_common(scene)
|
||||
scene.display.shading.show_wireframes = False
|
||||
scene.display.shading.show_cavity = True
|
||||
scene.display.shading.cavity_type = 'WORLD'
|
||||
scene.display.shading.cavity_ridge_factor = 1.0
|
||||
scene.display.shading.cavity_valley_factor = 1.0
|
||||
|
||||
def variant_freestyle(scene):
|
||||
# Freestyle line drawing + light solid base
|
||||
# Works with Workbench in many cases, but Freestyle is historically tied to Eevee/Cycles.
|
||||
# Here we switch to Eevee for reliability.
|
||||
scene.render.engine = 'BLENDER_EEVEE'
|
||||
scene.eevee.use_gtao = True
|
||||
scene.render.film_transparent = False
|
||||
|
||||
# Setup simple material
|
||||
mat = bpy.data.materials.new("Clay")
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = (0.85, 0.85, 0.85, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = 1.0
|
||||
|
||||
obj = bpy.data.objects.get("PART")
|
||||
if obj and obj.data.materials:
|
||||
obj.data.materials[0] = mat
|
||||
elif obj:
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
# Enable Freestyle
|
||||
scene.render.use_freestyle = True
|
||||
# Default freestyle line set is OK; keep lines black and modest thickness
|
||||
if scene.view_layers:
|
||||
vl = scene.view_layers[0]
|
||||
vl.freestyle_settings.linesets.new("LineSet") if not vl.freestyle_settings.linesets else None
|
||||
# Render Properties -> Freestyle settings are somewhat verbose to tune; default is acceptable for v1.
|
||||
|
||||
def render(scene, outpath):
|
||||
scene.render.filepath = outpath
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
def main():
|
||||
args = argv_after_double_dash()
|
||||
if len(args) != 2:
|
||||
die("Expected: <input.stl> <out_prefix>")
|
||||
|
||||
in_stl, out_prefix = args
|
||||
scene = reset_scene()
|
||||
obj = import_stl(in_stl)
|
||||
center_origin_and_location(obj)
|
||||
ensure_light_rig(scene)
|
||||
cam = ensure_camera(scene)
|
||||
set_isometric_camera(cam)
|
||||
fit_camera_to_object(cam, obj, margin=1.20)
|
||||
set_world_background(scene, gray=1.0)
|
||||
|
||||
# SOLID
|
||||
variant_solid(scene)
|
||||
scene.render.use_freestyle = False
|
||||
render(scene, f"{out_prefix}_solid.png")
|
||||
|
||||
# SOLID + EDGES
|
||||
variant_solid_edges(scene)
|
||||
scene.render.use_freestyle = False
|
||||
render(scene, f"{out_prefix}_solid_edges.png")
|
||||
|
||||
# WIRE
|
||||
variant_wire(scene)
|
||||
scene.render.use_freestyle = False
|
||||
render(scene, f"{out_prefix}_wire.png")
|
||||
|
||||
# MATCAP-ish (cavity)
|
||||
variant_matcap(scene)
|
||||
scene.render.use_freestyle = False
|
||||
render(scene, f"{out_prefix}_matcap.png")
|
||||
|
||||
# FREESTYLE
|
||||
variant_freestyle(scene)
|
||||
render(scene, f"{out_prefix}_freestyle.png")
|
||||
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/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}")
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
This project/task is complex in that you have STL files marshalled from various repositories and then need to create derivative glb (glTF binary) versions and then create PNG images from the glb. I have desigend this so that the directory tree paths remain intact which helps identify the file's provenance. This project took several days and I had to winnow out the errors in my back-and-forth sessions with ChatGPT.
|
||||
|
||||
Another quirk is that i use Gentoo Linux which often complicates matters. Running this on Raspbian, Debian, or Ubuntu may have some steps such as installing certain packages which I have not accounted for. I've created these exercises to demonstrate and reproducible step so the read has an understanding of what is happening.
|
||||
|
||||
This started off as a desire to have more readable images as the 14 pages color printed manual had many of the black shapes undiscernable. I started off with having a 3D rendition which is great and allows me to study a part if I cannot match with what was shipped to me. Then I gave further thought about a printed manifest and the interactive 3D experience cannot be in that form of a deliverable. So, I built upon the glbs and create a workflow that generates PNGs.
|
||||
|
||||
One caveat: by placing all the PNGs in one directory, and creating soft links to all the properly staged glbs, I am assuming there will be no name collision. But given that the Voron project consists of many projects, the potential for file name collisions is very real. I leave that as an enhancement if anyone cares to build off this project.
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Goal
|
||||
Place soft links to the glb files created by Blender in the staging tree in a single directory, ```web/batch_glb_png/glbs,``` , so the glb->PNG converter can easily process a set of files in one directory and not have to navigate the staging tree.
|
||||
|
||||
# Introduction
|
||||
This is a simple run-a-script exercise. (Note: the name of the link has the complete directory path as part of the link name rather than a 1:1 match of the file name it links to.)
|
||||
|
||||
# Steps
|
||||
In a command console:
|
||||
|
||||
cd ...web/batch_glb_png
|
||||
date; time ./link_glbs.sh [PATH to the root directory of your GitHub staging area]
|
||||
|
||||
Example:
|
||||
|
||||
jlpoole@jp ~/work/Voron/voronstl/web/batch_glb_png $ date; time ./link_glbs.sh /home/jlpoole/work/Voron/test1
|
||||
Wed Mar 4 18:51:15 PST 2026
|
||||
ROOT: /home/jlpoole/work/Voron/test1
|
||||
OUTDIR: /home/jlpoole/work/Voron/voronstl/web/batch_glb_png/glbs
|
||||
LOG: link_glbs_20260304_185115.log
|
||||
|
||||
|
||||
DONE. linked=131 skipped=0
|
||||
0 (details in link_glbs_20260304_185115.log)
|
||||
|
||||
real 0m0.507s
|
||||
user 0m0.569s
|
||||
sys 0m0.263s
|
||||
jlpoole@jp ~/work/Voron/voronstl/web/batch_glb_png $ ls -la glbs/ |wc -l
|
||||
136
|
||||
jlpoole@jp ~/work/Voron/voronstl/web/batch_glb_png $
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Goal
|
||||
Create a PNG file from a glb (= glTF binary = Graphics Library Transmission Format https://en.wikipedia.org/wiki/GlTF).
|
||||
## Introduction
|
||||
This exercise will walk thorugh the creation of a PNG from the included glb. The purpose is to show the steps taken to create the PNG which can be duplicated in a mass conversion script.
|
||||
|
||||
You will be using the command console.
|
||||
|
||||
The scripts run will create a small server; however, it will automatically select a port needed for the process so you do not have to be concerned of a port conflict, e.g. 3001, if you have another server running elsewhere on the system. The kernel selects a currently unused high-numbered port (typically in the 32768–60999 range on Linux).
|
||||
|
||||
## Prerequisites
|
||||
Node package: puppeteer
|
||||
|
||||
## Steps
|
||||
Open a command console and change to the following subdirectory:
|
||||
|
||||
cd web/batch_glb_png/glbs
|
||||
|
||||
for this exercise, create a soft link within glbs to just one file, we're only goint to perform a single PNG to demonstrate the scripts. Normally, directory glbs will have links to the actual glbs staged (how those links are created is another exercise)
|
||||
|
||||
ln -s ../../'[o]_stealthburner_LED_carrier.stl.glb' .
|
||||
|
||||
Example:
|
||||
|
||||
~/work/Voron/voronstl/web/batch_glb_png/glbs $ ln -s ../../'[o]_stealthburner_LED_carrier.stl.glb' .
|
||||
|
||||
Change to to the upper directory from within ```web/batch_glb_png``` and then launch the conversion script. Note the run will use the job.json I created, when time comes when you want to use yours, then specify the path and filename to your PNG profile:
|
||||
|
||||
cd ../batch_glb_png
|
||||
date;time node batch_render.js job.json
|
||||
|
||||
Example:
|
||||
|
||||
jlpoole@jp ~/work/Voron/voronstl/web/batch_glb_png $ date;time node batch_render.js job.json
|
||||
Wed Mar 4 18:17:13 PST 2026
|
||||
PAGE console: error Failed to load resource: the server responded with a status of 404 (Not Found)
|
||||
PAGE console: warn [.WebGL-0x7a40014ea00]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
|
||||
PAGE console: warn [.WebGL-0x7a40014ea00]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
|
||||
PAGE console: warn [.WebGL-0x7a40014ea00]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
|
||||
WROTE /home/jlpoole/work/Voron/voronstl/web/batch_glb_png/pngs/[o]_stealthburner_LED_carrier.stl.png
|
||||
|
||||
real 0m1.667s
|
||||
user 0m1.327s
|
||||
sys 0m0.423s
|
||||
jlpoole@jp ~/work/Voron/voronstl/web/batch_glb_png $
|
||||
|
||||
Result: a PNG 1227 × 994:
|
||||

|
||||
|
||||
# Conclusion
|
||||
Using the steps above, you created a single PNG from a glb.
|
||||
|
||||
The next step is to fill the glbs directory with links to the glbs perviously generated by Blender and then execute the same/similar command from within the '''web/batch_glb_png ''' directory:
|
||||
|
||||
node batch_render.js [your PNG specification, if you want]
|
||||
|
||||
that will create a PNG for every glb.
|
||||
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
|
@ -1,260 +0,0 @@
|
|||
# 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.
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB |
38
mv_themes.js
|
|
@ -1,38 +0,0 @@
|
|||
// 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);
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
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]
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 567 B |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
|
@ -1,181 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* batch_render.js
|
||||
* 20260228 ChatGPT
|
||||
* $Header$
|
||||
*
|
||||
* Example:
|
||||
* node batch_render.js job.json
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import http from 'node:http';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
function die (msg) { console.error(msg); process.exit(1); }
|
||||
function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); }
|
||||
|
||||
function listFiles(dir, suffix) {
|
||||
const out = [];
|
||||
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, ent.name);
|
||||
if (ent.isDirectory()) out.push(...listFiles(full, suffix));
|
||||
else if (!suffix || ent.name.toLowerCase().endsWith(suffix)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sanitizeBasename(p) {
|
||||
// While sanitizing is generally a good idea, it causes the output to deviate
|
||||
// from the input, which makes it harder to verify that the right files were
|
||||
// processed. For now, just use the original filename making this step a nullity.
|
||||
|
||||
const b = path.basename(p);
|
||||
//return b.replace(/[^\w.\-]+/g, '_');
|
||||
return b; // No sanitization, just the original path
|
||||
}
|
||||
|
||||
function mimeTypeFor(p) {
|
||||
const ext = path.extname(p).toLowerCase();
|
||||
if (ext === '.html') return 'text/html; charset=utf-8';
|
||||
if (ext === '.js') return 'text/javascript; charset=utf-8';
|
||||
if (ext === '.json') return 'application/json; charset=utf-8';
|
||||
if (ext === '.glb') return 'model/gltf-binary';
|
||||
if (ext === '.gltf') return 'model/gltf+json; charset=utf-8';
|
||||
if (ext === '.bin') return 'application/octet-stream';
|
||||
if (ext === '.png') return 'image/png';
|
||||
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
||||
if (ext === '.css') return 'text/css; charset=utf-8';
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
// Serve static files from rootDir
|
||||
function startStaticServer(rootDir) {
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
||||
const safePath = path.normalize(urlPath).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||
const filePath = path.join(rootDir, safePath);
|
||||
|
||||
// Default route
|
||||
const finalPath = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
|
||||
? path.join(filePath, 'index.html')
|
||||
: filePath;
|
||||
|
||||
if (!fs.existsSync(finalPath) || !fs.statSync(finalPath).isFile()) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('404\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(finalPath);
|
||||
res.writeHead(200, { 'Content-Type': mimeTypeFor(finalPath) });
|
||||
res.end(data);
|
||||
} catch (e) {
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end(`500\n${String(e)}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const addr = server.address();
|
||||
resolve({ server, port: addr.port });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const jobPath = process.argv[2];
|
||||
if (!jobPath) die("Usage: node batch_render.js job.json");
|
||||
|
||||
const jobAbs = path.resolve(jobPath);
|
||||
const jobDir = path.dirname(jobAbs);
|
||||
const job = JSON.parse(fs.readFileSync(jobAbs, 'utf8'));
|
||||
|
||||
const profileAbs = path.resolve(jobDir, job.profile ?? die("job.profile missing"));
|
||||
const inputDirAbs = path.resolve(jobDir, job.input_dir ?? die("job.input_dir missing"));
|
||||
const outputDirAbs = path.resolve(jobDir, job.output_dir ?? die("job.output_dir missing"));
|
||||
const pattern = (job.pattern ?? ".glb").toLowerCase();
|
||||
|
||||
ensureDir(outputDirAbs);
|
||||
|
||||
const glbs = listFiles(inputDirAbs, pattern);
|
||||
if (glbs.length === 0) die(`No files ending with '${pattern}' found under ${inputDirAbs}`);
|
||||
|
||||
const renderHtmlAbs = path.resolve(jobDir, 'render.html');
|
||||
if (!fs.existsSync(renderHtmlAbs)) die(`Missing render.html at ${renderHtmlAbs}`);
|
||||
|
||||
// Start local server rooted at jobDir so render.html, libs/, glbs/, profile json are reachable
|
||||
const { server, port } = await startStaticServer(jobDir);
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
|
||||
// Load profile for viewport sizing
|
||||
const profile = JSON.parse(fs.readFileSync(profileAbs, 'utf8'));
|
||||
const vw = profile.output?.width ?? 1024;
|
||||
const vh = profile.output?.height ?? 768;
|
||||
const dpr = profile.output?.pixelRatio ?? 1;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--use-gl=swiftshader',
|
||||
'--enable-unsafe-swiftshader'
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Log page-side failures clearly
|
||||
page.on('console', (msg) => console.log('PAGE console:', msg.type(), msg.text()));
|
||||
page.on('pageerror', (err) => console.log('PAGE error:', err));
|
||||
page.on('requestfailed', (req) => console.log('REQ failed:', req.url(), req.failure()?.errorText));
|
||||
|
||||
await page.setViewport({ width: vw, height: vh, deviceScaleFactor: dpr });
|
||||
|
||||
// Make URLs relative to server root
|
||||
const renderUrl = `${base}/render.html`;
|
||||
const profileUrl = `${base}/${encodeURIComponent(path.relative(jobDir, profileAbs)).replace(/%2F/g,'/')}`;
|
||||
|
||||
for (const glbAbs of glbs) {
|
||||
const rel = path.relative(jobDir, glbAbs);
|
||||
const glbUrl = `${base}/${encodeURIComponent(rel).replace(/%2F/g,'/')}`;
|
||||
|
||||
const outName = sanitizeBasename(glbAbs).replace(/\.glb$/i, '.png');
|
||||
const outAbs = path.join(outputDirAbs, outName);
|
||||
|
||||
const url = `${renderUrl}?glb=${encodeURIComponent(glbUrl)}&profile=${encodeURIComponent(profileUrl)}`;
|
||||
|
||||
// Don’t use networkidle0 here; module imports + GLB load can keep network “busy”
|
||||
await page.goto(url, { waitUntil: 'load' });
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
return window.__RENDER_STATUS__ && window.__RENDER_STATUS__.ready === true;
|
||||
}, { timeout: 120000 }); // 2 minutes, GLBs can be chunky
|
||||
|
||||
const status = await page.evaluate(() => window.__RENDER_STATUS__);
|
||||
if (!status || !status.pngDataUrl) {
|
||||
throw new Error(`No pngDataUrl produced for ${glbAbs} (status=${JSON.stringify(status)})`);
|
||||
}
|
||||
|
||||
const b64 = status.pngDataUrl.replace(/^data:image\/png;base64,/, '');
|
||||
fs.writeFileSync(outAbs, Buffer.from(b64, 'base64'));
|
||||
console.log(`WROTE\t${outAbs}`);
|
||||
}
|
||||
|
||||
await page.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error("ERROR:", e); process.exit(2); });
|
||||
3
web/batch_glb_png/glbs/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"profile": "./three_profile_20260228_111725.json",
|
||||
"input_dir": "./glbs",
|
||||
"output_dir": "./pngs",
|
||||
"pattern": ".glb"
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
../vendor/three/addons/loaders/GLTFLoader.js
|
||||
|
|
@ -1 +0,0 @@
|
|||
../vendor/three/build/three.module.js
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
#!/bin/sh
|
||||
# 20260228 ChatGPT
|
||||
# $Header$
|
||||
#
|
||||
# Copy/paste:
|
||||
# cd ~/work/Voron/renderlab/web/batch_glb_png
|
||||
# chmod +x link_glbs.sh
|
||||
# ./link_glbs.sh /home/jlpoole/work/Voron/test1
|
||||
#
|
||||
# This creates symlinks under ./glbs/ pointing to every *.glb under ROOT.
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT="${1:-}"
|
||||
[ -n "$ROOT" ] || { echo "Usage: $0 /path/to/root" >&2; exit 2; }
|
||||
[ -d "$ROOT" ] || { echo "ERROR: not a directory: $ROOT" >&2; exit 2; }
|
||||
|
||||
OUTDIR="./glbs"
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
ts="$(date +%Y%m%d_%H%M%S)"
|
||||
log="link_glbs_${ts}.log"
|
||||
|
||||
# Make ROOT absolute and strip any trailing slash for consistent relpath math.
|
||||
ROOT_ABS="$(cd "$ROOT" && pwd)"
|
||||
ROOT_ABS="${ROOT_ABS%/}"
|
||||
|
||||
echo "ROOT: $ROOT_ABS" | tee "$log"
|
||||
echo "OUTDIR: $(cd "$OUTDIR" && pwd)" | tee -a "$log"
|
||||
echo "LOG: $log" | tee -a "$log"
|
||||
echo "" | tee -a "$log"
|
||||
|
||||
# Sanitize: turn a path into a filesystem-safe basename.
|
||||
# Example:
|
||||
# Klicky-Probe/Printers/Voron/.../Dock_mount_fixed_v2.stl.glb
|
||||
# becomes:
|
||||
# Klicky-Probe__Printers__Voron__...__Dock_mount_fixed_v2.stl.glb
|
||||
sanitize() {
|
||||
# shellcheck disable=SC2001
|
||||
echo "$1" \
|
||||
| sed 's|^/||' \
|
||||
| sed 's|/|__|g' \
|
||||
| sed 's|[^A-Za-z0-9._-]|_|g'
|
||||
}
|
||||
|
||||
count=0
|
||||
skipped=0
|
||||
collisions=0
|
||||
|
||||
# Use -print0 to handle spaces safely.
|
||||
find "$ROOT_ABS" -type f -name '*.glb' -print0 \
|
||||
| while IFS= read -r -d '' f; do
|
||||
# Compute relative path under ROOT_ABS
|
||||
rel="${f#$ROOT_ABS/}"
|
||||
|
||||
name="$(sanitize "$rel")"
|
||||
linkpath="$OUTDIR/$name"
|
||||
|
||||
# If the link exists and points to the same target, skip quietly.
|
||||
if [ -L "$linkpath" ]; then
|
||||
target="$(readlink "$linkpath" || true)"
|
||||
if [ "$target" = "$f" ]; then
|
||||
echo "SKIP (already linked): $linkpath -> $f" >> "$log"
|
||||
skipped=$((skipped+1))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# If the name already exists but points elsewhere, disambiguate.
|
||||
if [ -e "$linkpath" ]; then
|
||||
i=1
|
||||
base="$linkpath"
|
||||
while [ -e "$linkpath" ]; do
|
||||
linkpath="${base%.glb}_$i.glb"
|
||||
i=$((i+1))
|
||||
done
|
||||
collisions=$((collisions+1))
|
||||
fi
|
||||
|
||||
ln -s "$f" "$linkpath"
|
||||
echo "LINK: $linkpath -> $f" >> "$log"
|
||||
count=$((count+1))
|
||||
done
|
||||
|
||||
# The while loop runs in a subshell in many /bin/sh implementations,
|
||||
# so counters above may not update. Summarize by counting log lines instead.
|
||||
linked_lines="$(grep -c '^LINK: ' "$log" 2>/dev/null || echo 0)"
|
||||
skipped_lines="$(grep -c '^SKIP ' "$log" 2>/dev/null || echo 0)"
|
||||
|
||||
echo "" | tee -a "$log"
|
||||
echo "DONE. linked=$linked_lines skipped=$skipped_lines (details in $log)" | tee -a "$log"
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
ROOT: /home/jlpoole/work/Voron/test1
|
||||
OUTDIR: /home/jlpoole/work/Voron/renderlab/web/batch_glb_png/glbs
|
||||
LOG: link_glbs_20260228_152056.log
|
||||
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Dock_mount_fixed_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Dock_mount_fixed_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Dock_sidemount_fixed_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Dock_sidemount_fixed_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Dock_sidemount_left_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Dock_sidemount_left_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Dock_sidemount_right_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Dock_sidemount_right_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__KlickyProbe_AB_mount_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/KlickyProbe_AB_mount_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Mount_magnet_holder.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Mount_magnet_holder.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Mount_magnet_pressfit_helper.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Mount_magnet_pressfit_helper.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Printers__Voron__v1.8_v2.4_Legacy_Trident__v1.8_v2.4_Legacy_Trident_STL__Mount_pressfit_holder_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Printers/Voron/v1.8_v2.4_Legacy_Trident/v1.8_v2.4_Legacy_Trident_STL/Mount_pressfit_holder_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__1mm_Spacer.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/1mm_Spacer.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__KlickyProbe_v2.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/KlickyProbe_v2.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__Probe_Dock_v2.1.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/Probe_Dock_v2.1.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__Probe_magnet_holder.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/Probe_magnet_holder.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__Probe_magnet_pressfit_helper.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/Probe_magnet_pressfit_helper.stl.glb
|
||||
LINK: ./glbs/Klicky-Probe__Probes__KlickyProbe__STL__Probe_pressfit_holder.stl.glb -> /home/jlpoole/work/Voron/test1/Klicky-Probe/Probes/KlickyProbe/STL/Probe_pressfit_holder.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__COB_Light_Strip__cob_light_strip_mount_100mm.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/COB Light Strip/cob_light_strip_mount_100mm.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__COB_Light_Strip__cob_light_strip_mount_150mm.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/COB Light Strip/cob_light_strip_mount_150mm.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__COB_Light_Strip__cob_light_strip_mount_50mm.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/COB Light Strip/cob_light_strip_mount_50mm.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__LDO_Door__handle_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/LDO Door/handle_a_x2.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__LDO_Door__handle_b.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/LDO Door/handle_b.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__LDO_Door__handle_b_nameplate.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/LDO Door/handle_b_nameplate.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__LDO_Door__screw_hinge_x3.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/LDO Door/screw_hinge_x3.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__exhaust_cover.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/exhaust_cover.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__handlebar_spacer_x4.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/handlebar_spacer_x4.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__ldo_bestagon_insert.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/ldo_bestagon_insert.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__led_fan_pcb_spacer_x2.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/led_fan_pcb_spacer_x2.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__nozzle_probe_ldo.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/nozzle_probe_ldo.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__z_belt_cover_a_led.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/z_belt_cover_a_led.stl.glb
|
||||
LINK: ./glbs/LDOVoron2__STLs__z_rail_stop_x4.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoron2/STLs/z_rail_stop_x4.stl.glb
|
||||
LINK: ./glbs/LDOVoronTrident__STLs__BTT_Pi_TFT4.3_Mount___a__faceplate.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoronTrident/STLs/BTT Pi TFT4.3 Mount/[a]_faceplate.stl.glb
|
||||
LINK: ./glbs/LDOVoronTrident__STLs__BTT_Pi_TFT4.3_Mount__mount.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoronTrident/STLs/BTT Pi TFT4.3 Mount/mount.stl.glb
|
||||
LINK: ./glbs/LDOVoronTrident__STLs__bed_wago_mount.stl.glb -> /home/jlpoole/work/Voron/test1/LDOVoronTrident/STLs/bed_wago_mount.stl.glb
|
||||
LINK: ./glbs/Leviathan__STLs__Leviathan_bracket_set.stl.glb -> /home/jlpoole/work/Voron/test1/Leviathan/STLs/Leviathan_bracket_set.stl.glb
|
||||
LINK: ./glbs/Nitehawk-SB__STLs__cw2_captive_pcb_cover.stl.glb -> /home/jlpoole/work/Voron/test1/Nitehawk-SB/STLs/cw2_captive_pcb_cover.stl.glb
|
||||
LINK: ./glbs/Nitehawk-SB__STLs__usb_adapter_mount.stl.glb -> /home/jlpoole/work/Voron/test1/Nitehawk-SB/STLs/usb_adapter_mount.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Electronics_Bay__lrs_200_psu_bracket_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Electronics_Bay/lrs_200_psu_bracket_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Electronics_Bay__pcb_din_clip_x3.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Electronics_Bay/pcb_din_clip_x3.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Electronics_Bay__wago_221-415_mount_3by5.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Electronics_Bay/wago_221-415_mount_3by5.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Exhaust_Filter__exhaust_filter_grill.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Exhaust_Filter/exhaust_filter_grill.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units___a__cable_cover.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/[a]_cable_cover.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units___a__z_chain_retainer_bracket_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/[a]_z_chain_retainer_bracket_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units__a_drive_frame_lower.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/a_drive_frame_lower.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units__a_drive_frame_upper.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/a_drive_frame_upper.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units__b_drive_frame_lower.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/b_drive_frame_lower.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__AB_Drive_Units__b_drive_frame_upper.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/AB_Drive_Units/b_drive_frame_upper.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers___a__tensioner_left.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/[a]_tensioner_left.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers___a__tensioner_right.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/[a]_tensioner_right.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers__front_idler_left_lower.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/front_idler_left_lower.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers__front_idler_left_upper.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/front_idler_left_upper.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers__front_idler_right_lower.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/front_idler_right_lower.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Front_Idlers__front_idler_right_upper.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Front_Idlers/front_idler_right_upper.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints___a__endstop_pod_D2F_switch.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/[a]_endstop_pod_D2F_switch.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints___a__xy_joint_cable_bridge_2hole.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/[a]_xy_joint_cable_bridge_2hole.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints__xy_joint_left_lower_MGN12.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/xy_joint_left_lower_MGN12.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints__xy_joint_left_upper_MGN12.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/xy_joint_left_upper_MGN12.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints__xy_joint_right_lower_MGN12.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/xy_joint_right_lower_MGN12.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__XY_Joints__xy_joint_right_upper_MGN12.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/XY_Joints/xy_joint_right_upper_MGN12.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__X_Carriage__probe_retainer_bracket.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/X_Carriage/probe_retainer_bracket.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__X_Carriage__x_frame_V2TR_MGN12_left.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/X_Carriage/x_frame_V2TR_MGN12_left.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__X_Axis__X_Carriage__x_frame_V2TR_MGN12_right.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/X_Axis/X_Carriage/x_frame_V2TR_MGN12_right.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Z_Joints__z_joint_lower_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Z_Joints/z_joint_lower_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__Z_Joints__z_joint_upper_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/Z_Joints/z_joint_upper_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry___a__z_belt_clip_lower_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/[a]_z_belt_clip_lower_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry___a__z_belt_clip_upper_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/[a]_z_belt_clip_upper_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__z_chain_bottom_anchor.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/z_chain_bottom_anchor.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Gantry__z_chain_guide.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Gantry/z_chain_guide.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__Front_Doors__latch_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/Front_Doors/latch_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__bottom_panel_clip_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/bottom_panel_clip_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__bottom_panel_hinge_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/bottom_panel_hinge_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__corner_panel_clip_4mm_x8.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/corner_panel_clip_4mm_x8.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__corner_panel_clip_6mm_x8.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/corner_panel_clip_6mm_x8.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__deck_support_4mm_x8.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/deck_support_4mm_x8.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__midspan_panel_clip_4mm_x7.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/midspan_panel_clip_4mm_x7.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__midspan_panel_clip_6mm_x8.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/midspan_panel_clip_6mm_x8.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__z_belt_cover_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/z_belt_cover_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Panel_Mounting__z_belt_cover_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Panel_Mounting/z_belt_cover_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__300__front_skirt_a_300.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/300/front_skirt_a_300.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__300__front_skirt_b_300.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/300/front_skirt_b_300.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__300__rear_center_skirt_300.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/300/rear_center_skirt_300.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__300__side_skirt_a_300_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/300/side_skirt_a_300_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__300__side_skirt_b_300_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/300/side_skirt_b_300_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__350__front_skirt_a_350.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/350/front_skirt_a_350.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__350__front_skirt_b_350.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/350/front_skirt_b_350.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__350__rear_center_skirt_350.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/350/rear_center_skirt_350.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__350__side_skirt_a_350_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/350/side_skirt_a_350_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__350__side_skirt_b_350_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/350/side_skirt_b_350_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__belt_guard_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_belt_guard_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__belt_guard_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_belt_guard_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__fan_grill_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_fan_grill_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__fan_grill_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_fan_grill_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__fan_grill_open_optional_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_fan_grill_open_optional_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__fan_grill_retainer_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_fan_grill_retainer_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts___a__keystone_blank_insert.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/[a]_keystone_blank_insert.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__keystone_panel.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/keystone_panel.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__power_inlet_IECGS_1mm.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/power_inlet_IECGS_1mm.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Skirts__side_fan_support_x2.STL.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Skirts/side_fan_support_x2.STL.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Spool_Management__bowden_retainer.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Spool_Management/bowden_retainer.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Spool_Management__spool_holder.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Spool_Management/spool_holder.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Test_Prints__Heatset_Practice.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Test_Prints/Heatset_Practice.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Tools__MGN12_rail_guide_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Tools/MGN12_rail_guide_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Tools__MGN9_rail_guide_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Tools/MGN9_rail_guide_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Tools__pulley_jig.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Tools/pulley_jig.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive___a__belt_tensioner_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/[a]_belt_tensioner_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive___a__belt_tensioner_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/[a]_belt_tensioner_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive___a__z_drive_baseplate_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/[a]_z_drive_baseplate_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive___a__z_drive_baseplate_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/[a]_z_drive_baseplate_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_drive_main_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_drive_main_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_drive_main_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_drive_main_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_drive_retainer_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_drive_retainer_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_drive_retainer_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_drive_retainer_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_motor_mount_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_motor_mount_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Drive__z_motor_mount_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Drive/z_motor_mount_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Idlers___a__z_tensioner_9mm_x4.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Idlers/[a]_z_tensioner_9mm_x4.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Idlers__z_tensioner_bracket_a_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Idlers/z_tensioner_bracket_a_x2.stl.glb
|
||||
LINK: ./glbs/Voron-2__STLs__Z_Idlers__z_tensioner_bracket_b_x2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-2/STLs/Z_Idlers/z_tensioner_bracket_b_x2.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive___a__guidler_a.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/[a]_guidler_a.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive___a__guidler_b.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/[a]_guidler_b.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive___a__latch.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/[a]_latch.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive___a__latch_shuttle.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/[a]_latch_shuttle.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive__main_body.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/main_body.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__Direct_Drive__motor_plate.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/Direct_Drive/motor_plate.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2___a__pcb_spacer.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/[a]_pcb_spacer.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Clockwork2__chain_anchor_2hole.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Clockwork2/chain_anchor_2hole.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner__Printheads__revo_voron__stealthburner_printhead_revo_voron_front.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/Printheads/revo_voron/stealthburner_printhead_revo_voron_front.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner__Printheads__revo_voron__stealthburner_printhead_revo_voron_rear_cw2.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/Printheads/revo_voron/stealthburner_printhead_revo_voron_rear_cw2.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner___a__stealthburner_main_body.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/[a]_stealthburner_main_body.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner___c__stealthburner_LED_diffuser.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/[c]_stealthburner_LED_diffuser.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner___o__stealthburner_LED_carrier.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/[o]_stealthburner_LED_carrier.stl.glb
|
||||
LINK: ./glbs/Voron-Stealthburner__STLs__Stealthburner___o__stealthburner_LED_diffuser_mask.stl.glb -> /home/jlpoole/work/Voron/test1/Voron-Stealthburner/STLs/Stealthburner/[o]_stealthburner_LED_diffuser_mask.stl.glb
|
||||
|
||||
DONE. linked=128 skipped=0
|
||||
0 (details in link_glbs_20260228_152056.log)
|
||||
5
web/batch_glb_png/node_modules/.gitignore
generated
vendored
|
|
@ -1,5 +0,0 @@
|
|||
.bin
|
||||
.modules.yaml
|
||||
.pnpm
|
||||
.pnpm-workspace-state-v1.json
|
||||
puppeteer
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "batch_glb_png",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"puppeteer": "^24.37.5"
|
||||
}
|
||||
}
|
||||
3
web/batch_glb_png/pngs/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!README.md
|
||||
!.gitignore
|
||||
868
web/batch_glb_png/pnpm-lock.yaml
generated
|
|
@ -1,868 +0,0 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
puppeteer:
|
||||
specifier: ^24.37.5
|
||||
version: 24.37.5
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@puppeteer/browsers@2.13.0':
|
||||
resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@tootallnate/quickjs-emscripten@0.23.0':
|
||||
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
|
||||
|
||||
'@types/node@25.3.2':
|
||||
resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
ast-types@0.13.4:
|
||||
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
b4a@1.8.0:
|
||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
||||
peerDependencies:
|
||||
react-native-b4a: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
bare-events@2.8.2:
|
||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||
peerDependencies:
|
||||
bare-abort-controller: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-abort-controller:
|
||||
optional: true
|
||||
|
||||
bare-fs@4.5.5:
|
||||
resolution: {integrity: sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==}
|
||||
engines: {bare: '>=1.16.0'}
|
||||
peerDependencies:
|
||||
bare-buffer: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-buffer:
|
||||
optional: true
|
||||
|
||||
bare-os@3.7.0:
|
||||
resolution: {integrity: sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==}
|
||||
engines: {bare: '>=1.14.0'}
|
||||
|
||||
bare-path@3.0.0:
|
||||
resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
|
||||
|
||||
bare-stream@2.8.0:
|
||||
resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==}
|
||||
peerDependencies:
|
||||
bare-buffer: '*'
|
||||
bare-events: '*'
|
||||
peerDependenciesMeta:
|
||||
bare-buffer:
|
||||
optional: true
|
||||
bare-events:
|
||||
optional: true
|
||||
|
||||
bare-url@2.3.2:
|
||||
resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
|
||||
|
||||
basic-ftp@5.2.0:
|
||||
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
|
||||
callsites@3.1.0:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
chromium-bidi@14.0.0:
|
||||
resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==}
|
||||
peerDependencies:
|
||||
devtools-protocol: '*'
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
cosmiconfig@9.0.0:
|
||||
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.9.5'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
data-uri-to-buffer@6.0.2:
|
||||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
degenerator@5.0.1:
|
||||
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
devtools-protocol@0.0.1566079:
|
||||
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escodegen@2.1.0:
|
||||
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
|
||||
engines: {node: '>=6.0'}
|
||||
hasBin: true
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
estraverse@5.3.0:
|
||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
events-universal@1.0.1:
|
||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
hasBin: true
|
||||
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-stream@5.2.0:
|
||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
get-uri@6.0.5:
|
||||
resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
lru-cache@7.18.3:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
netmask@2.0.2:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
pac-proxy-agent@7.2.0:
|
||||
resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
pac-resolver@7.0.1:
|
||||
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-json@5.2.0:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
progress@2.0.3:
|
||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
proxy-agent@6.5.0:
|
||||
resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
|
||||
puppeteer-core@24.37.5:
|
||||
resolution: {integrity: sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
puppeteer@24.37.5:
|
||||
resolution: {integrity: sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
smart-buffer@4.2.0:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
socks@2.8.7:
|
||||
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
streamx@2.23.0:
|
||||
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
tar-fs@3.1.1:
|
||||
resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
|
||||
|
||||
tar-stream@3.1.8:
|
||||
resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==}
|
||||
|
||||
teex@1.0.1:
|
||||
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
|
||||
|
||||
text-decoder@1.2.7:
|
||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typed-query-selector@2.12.1:
|
||||
resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==}
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
webdriver-bidi-protocol@0.4.1:
|
||||
resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@8.19.0:
|
||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yauzl@2.10.0:
|
||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
'@puppeteer/browsers@2.13.0':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
extract-zip: 2.0.1
|
||||
progress: 2.0.3
|
||||
proxy-agent: 6.5.0
|
||||
semver: 7.7.4
|
||||
tar-fs: 3.1.1
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
|
||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||
|
||||
'@types/node@25.3.2':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
optional: true
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.3.2
|
||||
optional: true
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
ast-types@0.13.4:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
b4a@1.8.0: {}
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
|
||||
bare-fs@4.5.5:
|
||||
dependencies:
|
||||
bare-events: 2.8.2
|
||||
bare-path: 3.0.0
|
||||
bare-stream: 2.8.0(bare-events@2.8.2)
|
||||
bare-url: 2.3.2
|
||||
fast-fifo: 1.3.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
bare-os@3.7.0: {}
|
||||
|
||||
bare-path@3.0.0:
|
||||
dependencies:
|
||||
bare-os: 3.7.0
|
||||
|
||||
bare-stream@2.8.0(bare-events@2.8.2):
|
||||
dependencies:
|
||||
streamx: 2.23.0
|
||||
teex: 1.0.1
|
||||
optionalDependencies:
|
||||
bare-events: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
bare-url@2.3.2:
|
||||
dependencies:
|
||||
bare-path: 3.0.0
|
||||
|
||||
basic-ftp@5.2.0: {}
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
chromium-bidi@14.0.0(devtools-protocol@0.0.1566079):
|
||||
dependencies:
|
||||
devtools-protocol: 0.0.1566079
|
||||
mitt: 3.0.1
|
||||
zod: 3.25.76
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
cosmiconfig@9.0.0:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.1.1
|
||||
parse-json: 5.2.0
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
degenerator@5.0.1:
|
||||
dependencies:
|
||||
ast-types: 0.13.4
|
||||
escodegen: 2.1.0
|
||||
esprima: 4.0.1
|
||||
|
||||
devtools-protocol@0.0.1566079: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escodegen@2.1.0:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
estraverse: 5.3.0
|
||||
esutils: 2.0.3
|
||||
optionalDependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
events-universal@1.0.1:
|
||||
dependencies:
|
||||
bare-events: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
get-stream: 5.2.0
|
||||
yauzl: 2.10.0
|
||||
optionalDependencies:
|
||||
'@types/yauzl': 2.10.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-stream@5.2.0:
|
||||
dependencies:
|
||||
pump: 3.0.4
|
||||
|
||||
get-uri@6.0.5:
|
||||
dependencies:
|
||||
basic-ftp: 5.2.0
|
||||
data-uri-to-buffer: 6.0.2
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
resolve-from: 4.0.0
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
pac-proxy-agent@7.2.0:
|
||||
dependencies:
|
||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
get-uri: 6.0.5
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
pac-resolver: 7.0.1
|
||||
socks-proxy-agent: 8.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
pac-resolver@7.0.1:
|
||||
dependencies:
|
||||
degenerator: 5.0.1
|
||||
netmask: 2.0.2
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-json@5.2.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
error-ex: 1.3.4
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
|
||||
proxy-agent@6.5.0:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
lru-cache: 7.18.3
|
||||
pac-proxy-agent: 7.2.0
|
||||
proxy-from-env: 1.1.0
|
||||
socks-proxy-agent: 8.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
pump@3.0.4:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
|
||||
puppeteer-core@24.37.5:
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.13.0
|
||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079)
|
||||
debug: 4.4.3
|
||||
devtools-protocol: 0.0.1566079
|
||||
typed-query-selector: 2.12.1
|
||||
webdriver-bidi-protocol: 0.4.1
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- bufferutil
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
puppeteer@24.37.5:
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.13.0
|
||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079)
|
||||
cosmiconfig: 9.0.0
|
||||
devtools-protocol: 0.0.1566079
|
||||
puppeteer-core: 24.37.5
|
||||
typed-query-selector: 2.12.1
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- bufferutil
|
||||
- react-native-b4a
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
socks: 2.8.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
socks@2.8.7:
|
||||
dependencies:
|
||||
ip-address: 10.1.0
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
source-map@0.6.1:
|
||||
optional: true
|
||||
|
||||
streamx@2.23.0:
|
||||
dependencies:
|
||||
events-universal: 1.0.1
|
||||
fast-fifo: 1.3.2
|
||||
text-decoder: 1.2.7
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
tar-fs@3.1.1:
|
||||
dependencies:
|
||||
pump: 3.0.4
|
||||
tar-stream: 3.1.8
|
||||
optionalDependencies:
|
||||
bare-fs: 4.5.5
|
||||
bare-path: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
|
||||
tar-stream@3.1.8:
|
||||
dependencies:
|
||||
b4a: 1.8.0
|
||||
bare-fs: 4.5.5
|
||||
fast-fifo: 1.3.2
|
||||
streamx: 2.23.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
|
||||
teex@1.0.1:
|
||||
dependencies:
|
||||
streamx: 2.23.0
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
text-decoder@1.2.7:
|
||||
dependencies:
|
||||
b4a: 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
typed-query-selector@2.12.1: {}
|
||||
|
||||
undici-types@7.18.2:
|
||||
optional: true
|
||||
|
||||
webdriver-bidi-protocol@0.4.1: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yauzl@2.10.0:
|
||||
dependencies:
|
||||
buffer-crc32: 0.2.13
|
||||
fd-slicer: 1.1.0
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
onlyBuiltDependencies:
|
||||
- puppeteer
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Headless GLB Renderer</title>
|
||||
<style>
|
||||
html, body { margin:0; padding:0; background:#fff; overflow:hidden; }
|
||||
canvas { display:block; }
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "./libs/three.module.js",
|
||||
"three/addons/": "./vendor/three/addons/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
|
||||
function qs (k) { return new URLSearchParams(location.search).get(k); }
|
||||
|
||||
const glbUrl = qs('glb');
|
||||
const profileUrl = qs('profile');
|
||||
|
||||
window.__RENDER_STATUS__ = { ready:false, error:null, pngDataUrl:null };
|
||||
|
||||
if (!glbUrl || !profileUrl) {
|
||||
window.__RENDER_STATUS__.error = "Missing ?glb=... or ?profile=...";
|
||||
throw new Error(window.__RENDER_STATUS__.error);
|
||||
}
|
||||
|
||||
const profile = await (await fetch(profileUrl)).json();
|
||||
|
||||
// Renderer
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true,
|
||||
alpha: false
|
||||
});
|
||||
renderer.setPixelRatio(profile.output?.pixelRatio ?? 1);
|
||||
renderer.setSize(profile.output?.width ?? 1024, profile.output?.height ?? 768, false);
|
||||
|
||||
// Three r152+ color management (safe-guard)
|
||||
if (THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
const bg = profile.scene?.background ?? 0xffffff;
|
||||
renderer.setClearColor(bg, 1);
|
||||
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Scene
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
// Camera
|
||||
const cam = new THREE.PerspectiveCamera(
|
||||
profile.camera?.fov ?? 50,
|
||||
(profile.output?.width ?? 1024) / (profile.output?.height ?? 768),
|
||||
profile.camera?.near ?? 0.1,
|
||||
profile.camera?.far ?? 1000
|
||||
);
|
||||
cam.position.fromArray(profile.camera?.position ?? [0,0,10]);
|
||||
cam.up.fromArray(profile.camera?.up ?? [0,1,0]);
|
||||
|
||||
// We won’t use OrbitControls in headless; we just mimic its target by looking at it.
|
||||
const target = new THREE.Vector3().fromArray(profile.controls?.target ?? [0,0,0]);
|
||||
cam.lookAt(target);
|
||||
cam.updateProjectionMatrix();
|
||||
|
||||
// Lights
|
||||
const dir = new THREE.DirectionalLight(0xffffff, profile.lights?.directional?.intensity ?? 2.0);
|
||||
dir.position.fromArray(profile.lights?.directional?.position ?? [5,5,5]);
|
||||
scene.add(dir);
|
||||
|
||||
const amb = new THREE.AmbientLight(0xffffff, profile.lights?.ambient?.intensity ?? 0.5);
|
||||
scene.add(amb);
|
||||
|
||||
// Load GLB
|
||||
const loader = new GLTFLoader();
|
||||
const gltf = await loader.loadAsync(glbUrl);
|
||||
const root = gltf.scene;
|
||||
scene.add(root);
|
||||
|
||||
// --- Fit-to-frame while keeping the profile's viewing direction ---
|
||||
const box = new THREE.Box3().setFromObject(root);
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
|
||||
// Use profile target if present, but still re-center model so target makes sense
|
||||
const profTarget = new THREE.Vector3().fromArray(profile.controls?.target ?? [0, 0, 0]);
|
||||
|
||||
// Shift the model so its center sits at the profile target (usually [0,0,0] or your saved target)
|
||||
root.position.sub(center).add(profTarget);
|
||||
|
||||
// Recompute bounds after shifting
|
||||
const box2 = new THREE.Box3().setFromObject(root);
|
||||
const size2 = box2.getSize(new THREE.Vector3());
|
||||
const center2 = box2.getCenter(new THREE.Vector3());
|
||||
|
||||
// Keep the same view direction as the profile camera->target vector
|
||||
const profCamPos = new THREE.Vector3().fromArray(profile.camera?.position ?? [0, 0, 10]);
|
||||
const viewDir = profCamPos.clone().sub(profTarget).normalize();
|
||||
|
||||
// Compute distance so the whole object fits the camera FOV
|
||||
|
||||
const maxDim = Math.max(size2.x, size2.y, size2.z, 1e-6);
|
||||
const fov = cam.fov * (Math.PI / 180);
|
||||
const fitPadding = profile.camera?.fitPadding ?? 1.15; // you can add this to JSON later
|
||||
const distance = (maxDim / 2) / Math.tan(fov / 2) * fitPadding;
|
||||
|
||||
// Position camera and aim
|
||||
cam.position.copy(center2).add(viewDir.multiplyScalar(distance));
|
||||
cam.lookAt(center2);
|
||||
cam.updateProjectionMatrix();
|
||||
|
||||
// Adjust near/far safely
|
||||
cam.near = Math.max(0.01, distance / 100);
|
||||
cam.far = distance * 100;
|
||||
cam.updateProjectionMatrix();
|
||||
|
||||
// Apply renderParams
|
||||
const wireframe = !!profile.renderParams?.wireframe;
|
||||
root.traverse((obj) => {
|
||||
if (obj.isMesh && obj.material) {
|
||||
// If multi-material
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach(m => { m.wireframe = wireframe; m.needsUpdate = true; });
|
||||
} else {
|
||||
obj.material.wireframe = wireframe;
|
||||
obj.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: edges overlay if you later add it (kept off unless you implement lines).
|
||||
// profile.renderParams.edgeAngle is available; current lab.html uses it to build edges.
|
||||
|
||||
renderer.render(scene, cam);
|
||||
|
||||
// Give the GPU a beat (helps on some headless setups)
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
|
||||
window.__RENDER_STATUS__.pngDataUrl = renderer.domElement.toDataURL('image/png');
|
||||
window.__RENDER_STATUS__.ready = true;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"provenance": "lab.html exportProfile Sat Feb 28 2026 11:17:25 GMT-0800 (Pacific Standard Time)",
|
||||
"output": {
|
||||
"width": 1227,
|
||||
"height": 994,
|
||||
"pixelRatio": 1
|
||||
},
|
||||
"scene": {
|
||||
"background": 16777215
|
||||
},
|
||||
"camera": {
|
||||
"type": "PerspectiveCamera",
|
||||
"fov": 50,
|
||||
"near": 0.1,
|
||||
"far": 1000,
|
||||
"position": [
|
||||
-12.932153617745264,
|
||||
18.85116776875012,
|
||||
19.470685732446455
|
||||
],
|
||||
"up": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"controls": {
|
||||
"target": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"renderParams": {
|
||||
"wireframe": false,
|
||||
"edgeAngle": 30,
|
||||
"lightIntensity": 3
|
||||
},
|
||||
"lights": {
|
||||
"directional": {
|
||||
"position": [
|
||||
5,
|
||||
5,
|
||||
5
|
||||
],
|
||||
"intensity": 3
|
||||
},
|
||||
"ambient": {
|
||||
"intensity": 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
../vendor
|
||||
BIN
web/favicon.ico
|
Before Width: | Height: | Size: 105 KiB |
202
web/lab.html
|
|
@ -1,202 +0,0 @@
|
|||
<!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>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RenderLab: PNG Profile Exporter</title>
|
||||
<style>
|
||||
body { margin:0; background:white; }
|
||||
canvas { display:block; }
|
||||
</style>
|
||||
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "./vendor/three/build/three.module.js",
|
||||
"three/addons/": "./vendor/three/addons/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import GUI from 'three/addons/libs/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,
|
||||
preserveDrawingBuffer: true, // <-- critical for toDataURL on many setups
|
||||
alpha: false // ensure opaque canvas
|
||||
});
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setClearColor(0xffffff, 1); // opaque white
|
||||
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();
|
||||
|
||||
//
|
||||
// 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.material = new THREE.MeshStandardMaterial({
|
||||
color: 0xdddddd,
|
||||
roughness: 0.9,
|
||||
metalness: 0
|
||||
});
|
||||
|
||||
scene.add(mesh);
|
||||
addEdges(30);
|
||||
|
||||
});
|
||||
|
||||
function ts_yyyymmdd_hhmiss () {
|
||||
const d = new Date();
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
return (
|
||||
d.getFullYear() +
|
||||
pad(d.getMonth()+1) +
|
||||
pad(d.getDate()) + '_' +
|
||||
pad(d.getHours()) +
|
||||
pad(d.getMinutes()) +
|
||||
pad(d.getSeconds())
|
||||
);
|
||||
}
|
||||
|
||||
function downloadTextFile (filename, text) {
|
||||
const blob = new Blob([text], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
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: () => {
|
||||
renderer.render(scene, camera); // recommended before capture
|
||||
const link = document.createElement('a');
|
||||
link.download = 'render.png';
|
||||
link.href = renderer.domElement.toDataURL("image/png");
|
||||
link.click();
|
||||
},
|
||||
|
||||
exportProfile: () => {
|
||||
controls.update(); // ensure target is current
|
||||
|
||||
const profile = {
|
||||
provenance: `lab.html exportProfile ${new Date().toString()}`,
|
||||
|
||||
output: {
|
||||
width: renderer.domElement.width,
|
||||
height: renderer.domElement.height,
|
||||
pixelRatio: renderer.getPixelRatio()
|
||||
},
|
||||
|
||||
scene: {
|
||||
background: scene.background?.getHex?.() ?? null
|
||||
},
|
||||
|
||||
camera: {
|
||||
type: 'PerspectiveCamera',
|
||||
fov: camera.fov,
|
||||
near: camera.near,
|
||||
far: camera.far,
|
||||
position: camera.position.toArray(),
|
||||
up: camera.up.toArray()
|
||||
},
|
||||
|
||||
controls: {
|
||||
target: controls.target.toArray()
|
||||
},
|
||||
|
||||
renderParams: {
|
||||
wireframe: params.wireframe,
|
||||
edgeAngle: params.edgeAngle,
|
||||
lightIntensity: params.lightIntensity
|
||||
},
|
||||
|
||||
lights: {
|
||||
directional: { position: light.position.toArray(), intensity: light.intensity },
|
||||
ambient: { intensity: ambient.intensity }
|
||||
}
|
||||
};
|
||||
|
||||
const fn = `three_profile_${ts_yyyymmdd_hhmiss()}.json`;
|
||||
downloadTextFile(fn, JSON.stringify(profile, null, 2));
|
||||
console.log('Exported profile:', profile);
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
gui.add(params, 'exportProfile');
|
||||
|
||||
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>
|
||||
2
web/out/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
8
web/threejs/lil-gui.module.min.js
vendored
1417
web/vendor/three/addons/controls/OrbitControls.js
vendored
4663
web/vendor/three/addons/loaders/GLTFLoader.js
vendored
1375
web/vendor/three/addons/utils/BufferGeometryUtils.js
vendored
53044
web/vendor/three/build/three.module.js
vendored
|
|
@ -1,78 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GLB Wireframe Viewer</title>
|
||||
<style>
|
||||
body { margin:0; background:white; }
|
||||
canvas { display:block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
|
||||
import * as THREE from './three.module.js';
|
||||
import { GLTFLoader } from './GLTFLoader.js';
|
||||
import { OrbitControls } from './OrbitControls.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);
|
||||
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);
|
||||
|
||||
let mesh, edgeLines;
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
loader.load('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 (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);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key === 'w') mesh.material.wireframe = !mesh.material.wireframe;
|
||||
if (e.key === 'e') addEdges(25);
|
||||
});
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||