Compare commits
No commits in common. "876ca92f077350bba4fc26019b654ac8c8c004a4" and "beac39b2bbd6789d9ea7d5e4acd935eb900973d1" have entirely different histories.
876ca92f07
...
beac39b2bb
5 changed files with 0 additions and 827 deletions
1
glb/.gitignore
vendored
1
glb/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.glb
|
||||
255
scad/wedge.scad
255
scad/wedge.scad
|
|
@ -1,255 +0,0 @@
|
|||
//
|
||||
// colorpie.scad
|
||||
// 2026-03-06 ChatGPT
|
||||
// $Header$
|
||||
//
|
||||
// Preview:
|
||||
// openscad colorpie.scad
|
||||
//
|
||||
// STL export:
|
||||
// openscad -o output.stl wedge.scad
|
||||
//
|
||||
|
||||
$fn = 90;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Global parameters
|
||||
// ------------------------------------------------------------
|
||||
|
||||
slice_count = 10;
|
||||
slice_angle = 360 / slice_count; // 36 degrees
|
||||
explode_gap = 10;
|
||||
|
||||
// Footprint
|
||||
inner_r = 22;
|
||||
outer_r = 82;
|
||||
thickness = 6;
|
||||
|
||||
// Crown profile across the wedge width
|
||||
crown_height = 10;
|
||||
table_frac = 0.42; // fraction of width occupied by table
|
||||
shoulder_frac = 0.18; // fraction on each side used by bevel/shoulder
|
||||
stations = 13; // more stations = more facet slices across width
|
||||
|
||||
|
||||
// Labeling
|
||||
label_size = 8;
|
||||
label_depth = 1.0;
|
||||
label_font = "Liberation Sans:style=Bold";
|
||||
|
||||
// Small geometry tolerance
|
||||
eps = 0.01;
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Helper functions
|
||||
// ------------------------------------------------------------
|
||||
|
||||
function deg(a) = a;
|
||||
function lerp(a,b,t) = a + (b-a)*t;
|
||||
|
||||
// u is normalized from 0 at left cut face to 1 at right cut face
|
||||
// This defines the outside-elevation style profile:
|
||||
//
|
||||
// low edge -> bevel up -> flat table -> bevel down -> low edge
|
||||
//
|
||||
function crown_profile(u) =
|
||||
let(
|
||||
t0 = shoulder_frac,
|
||||
t1 = 0.5 - table_frac/2,
|
||||
t2 = 0.5 + table_frac/2,
|
||||
t3 = 1.0 - shoulder_frac
|
||||
)
|
||||
(u <= t0) ? lerp(0.00, 0.70, u / t0) :
|
||||
(u <= t1) ? lerp(0.70, 1.00, (u - t0) / max(eps, t1 - t0)) :
|
||||
(u <= t2) ? 1.00 :
|
||||
(u <= t3) ? lerp(1.00, 0.70, (u - t2) / max(eps, t3 - t2)) :
|
||||
lerp(0.70, 0.00, (u - t3) / max(eps, 1.0 - t3));
|
||||
|
||||
// Z value of top surface at station u
|
||||
function top_z(u) = thickness + crown_height * crown_profile(u);
|
||||
|
||||
// Angle at station i
|
||||
function station_angle(i) = slice_angle * i / (stations - 1);
|
||||
|
||||
// Polar point helpers
|
||||
function p_inner(a,z) = [ inner_r * cos(a), inner_r * sin(a), z ];
|
||||
function p_outer(a,z) = [ outer_r * cos(a), outer_r * sin(a), z ];
|
||||
|
||||
// Indices for point grid
|
||||
// per station:
|
||||
// 0 = bottom inner
|
||||
// 1 = bottom outer
|
||||
// 2 = top inner
|
||||
// 3 = top outer
|
||||
function idx_bi(i) = 4*i + 0;
|
||||
function idx_bo(i) = 4*i + 1;
|
||||
function idx_ti(i) = 4*i + 2;
|
||||
function idx_to(i) = 4*i + 3;
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Master wedge as a polyhedron
|
||||
// Top shape traverses the wedge width
|
||||
// ------------------------------------------------------------
|
||||
|
||||
module wedge_body() {
|
||||
|
||||
pts = [
|
||||
for (i = [0:stations-1])
|
||||
let(
|
||||
a = station_angle(i),
|
||||
u = i / (stations - 1),
|
||||
zt = top_z(u)
|
||||
)
|
||||
each [
|
||||
p_inner(a, 0), // bottom inner
|
||||
p_outer(a, 0), // bottom outer
|
||||
p_inner(a, zt), // top inner
|
||||
p_outer(a, zt) // top outer
|
||||
]
|
||||
];
|
||||
|
||||
faces = concat(
|
||||
|
||||
// bottom, triangulated
|
||||
[
|
||||
for (i = [0:stations-2]) each [
|
||||
[ idx_bi(i), idx_bi(i+1), idx_bo(i+1) ],
|
||||
[ idx_bi(i), idx_bo(i+1), idx_bo(i) ]
|
||||
]
|
||||
],
|
||||
|
||||
// top, triangulated
|
||||
[
|
||||
for (i = [0:stations-2]) each [
|
||||
[ idx_ti(i), idx_to(i), idx_to(i+1) ],
|
||||
[ idx_ti(i), idx_to(i+1), idx_ti(i+1) ]
|
||||
]
|
||||
],
|
||||
|
||||
// inner radius face, triangulated
|
||||
[
|
||||
for (i = [0:stations-2]) each [
|
||||
[ idx_bi(i), idx_ti(i), idx_ti(i+1) ],
|
||||
[ idx_bi(i), idx_ti(i+1), idx_bi(i+1) ]
|
||||
]
|
||||
],
|
||||
|
||||
// outer radius face, triangulated
|
||||
[
|
||||
for (i = [0:stations-2]) each [
|
||||
[ idx_bo(i), idx_bo(i+1), idx_to(i+1) ],
|
||||
[ idx_bo(i), idx_to(i+1), idx_to(i) ]
|
||||
]
|
||||
],
|
||||
|
||||
// left cut face, triangulated
|
||||
[
|
||||
[ idx_bi(0), idx_bo(0), idx_to(0) ],
|
||||
[ idx_bi(0), idx_to(0), idx_ti(0) ]
|
||||
],
|
||||
|
||||
// right cut face, triangulated
|
||||
[
|
||||
[ idx_bi(stations-1), idx_ti(stations-1), idx_to(stations-1) ],
|
||||
[ idx_bi(stations-1), idx_to(stations-1), idx_bo(stations-1) ]
|
||||
]
|
||||
);
|
||||
|
||||
polyhedron(points = pts, faces = faces, convexity = 12);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// raised bottom label so readable and not engulged/hidden
|
||||
// ------------------------------------------------------------
|
||||
module wedge_label_raise_OLD(label_txt="0") {
|
||||
mid_r = (inner_r + outer_r) / 2;
|
||||
mid_a = slice_angle / 2;
|
||||
|
||||
x = mid_r * cos(mid_a);
|
||||
y = mid_r * sin(mid_a);
|
||||
|
||||
translate([x, y, 0])
|
||||
rotate([0, 0, mid_a - 90])
|
||||
linear_extrude(height = 1.2)
|
||||
text(label_txt,
|
||||
size = 18,
|
||||
font = label_font,
|
||||
halign = "center",
|
||||
valign = "center");
|
||||
}
|
||||
|
||||
// If the text is too close to the middle or too large, use this instead:
|
||||
|
||||
|
||||
module wedge_label_raise_optional(label_txt="0") {
|
||||
mid_r = inner_r + (outer_r - inner_r) * 0.63;
|
||||
mid_a = slice_angle / 2;
|
||||
|
||||
x = mid_r * cos(mid_a);
|
||||
y = mid_r * sin(mid_a);
|
||||
|
||||
translate([x, y, 0])
|
||||
rotate([0, 0, mid_a - 90])
|
||||
linear_extrude(height = 10.2)
|
||||
text(label_txt,
|
||||
size = 18,
|
||||
font = label_font,
|
||||
halign = "center",
|
||||
valign = "center");
|
||||
}
|
||||
|
||||
module wedge_label_raise(label_txt="0") {
|
||||
mid_r = inner_r + (outer_r - inner_r) * 0.63;
|
||||
mid_a = slice_angle / 2;
|
||||
|
||||
x = mid_r * cos(mid_a);
|
||||
y = mid_r * sin(mid_a);
|
||||
|
||||
translate([x, y, -3.2])
|
||||
rotate([0, 0, mid_a - 90])
|
||||
linear_extrude(height = 3)
|
||||
text(label_txt,
|
||||
size = 16,
|
||||
font = label_font,
|
||||
halign = "center",
|
||||
valign = "center");
|
||||
}
|
||||
// ------------------------------------------------------------
|
||||
// One numbered wedge
|
||||
// ------------------------------------------------------------
|
||||
|
||||
module wedge_unit(idx=0) {
|
||||
wedge_body();
|
||||
wedge_label_raise(str(idx));
|
||||
//wedge_label_raise_optional(str(idx));
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Assemble 10 wedges around the center
|
||||
// Each wedge is exploded outward along its centerline
|
||||
// ------------------------------------------------------------
|
||||
|
||||
module wheel_10() {
|
||||
for (i = [0:slice_count-1]) {
|
||||
a = i * slice_angle;
|
||||
mid_a = a + slice_angle/2;
|
||||
|
||||
dx = explode_gap * cos(mid_a);
|
||||
dy = explode_gap * sin(mid_a);
|
||||
|
||||
translate([dx, dy, 0])
|
||||
rotate([0,0,a])
|
||||
wedge_unit(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Top level
|
||||
// ------------------------------------------------------------
|
||||
|
||||
wheel_10();
|
||||
BIN
scad/wedge.stl
BIN
scad/wedge.stl
Binary file not shown.
|
|
@ -1,453 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# create_glb_colorpie.py — Blender headless STL -> GLB for colorpie
|
||||
#
|
||||
# Example:
|
||||
# blender-bin-5.0.0 --background --python create_glb_colorpie.py -- \
|
||||
# wedge.stl wedge.glb \
|
||||
# --hexes FFFFFF,D9D9D9,BFBFBF,808080,606060,404040,202020,000000,FF6600,CC5500
|
||||
#
|
||||
# Optional:
|
||||
# --roughness 0.6
|
||||
# --metallic 0.0
|
||||
#
|
||||
# Notes:
|
||||
# - Expects one STL containing 10 disconnected wedge solids.
|
||||
# - Separates loose parts after import.
|
||||
# - Sorts parts by centroid angle around the origin.
|
||||
# - Assigns colors 0..9 in that angular order.
|
||||
#
|
||||
# 2026-03-06 ChatGPT
|
||||
# $Header$
|
||||
|
||||
import bpy
|
||||
import bmesh
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def delete_other_meshes(keep_objs):
|
||||
keep_names = {o.name for o in keep_objs}
|
||||
victims = [
|
||||
o for o in bpy.context.scene.objects
|
||||
if o.type == 'MESH' and o.name not in keep_names
|
||||
]
|
||||
|
||||
if victims:
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in victims:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = victims[0]
|
||||
bpy.ops.object.delete()
|
||||
|
||||
log(f"Deleted {len(victims)} extra mesh objects; keeping {len(keep_objs)}")
|
||||
|
||||
def part_size_score(obj):
|
||||
"""
|
||||
Score a part by bounding-box volume.
|
||||
Good enough to distinguish the main wedge from tiny numeral residue.
|
||||
"""
|
||||
d = obj.dimensions
|
||||
return d.x * d.y * d.z
|
||||
|
||||
|
||||
def group_parts_by_angle(parts, tolerance_deg=10.0):
|
||||
"""
|
||||
Group parts whose centroid angles are close together.
|
||||
For colorpie, each wedge and its tiny residue should land in the same group.
|
||||
"""
|
||||
parts_sorted = sort_parts_by_angle(parts)
|
||||
groups = []
|
||||
|
||||
for obj in parts_sorted:
|
||||
a = angle_deg_for_obj(obj)
|
||||
placed = False
|
||||
|
||||
for g in groups:
|
||||
ga = g["angle"]
|
||||
diff = abs(a - ga)
|
||||
diff = min(diff, 360.0 - diff)
|
||||
if diff <= tolerance_deg:
|
||||
g["parts"].append(obj)
|
||||
# keep a simple running average angle
|
||||
g["angle"] = sum(angle_deg_for_obj(p) for p in g["parts"]) / len(g["parts"])
|
||||
placed = True
|
||||
break
|
||||
|
||||
if not placed:
|
||||
groups.append({
|
||||
"angle": a,
|
||||
"parts": [obj],
|
||||
})
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def pick_slot_pairs(parts, expected_groups=10, tolerance_deg=10.0):
|
||||
groups = group_parts_by_angle(parts, tolerance_deg=tolerance_deg)
|
||||
|
||||
log(f"Grouped {len(parts)} raw parts into {len(groups)} angle groups")
|
||||
|
||||
slot_pairs = []
|
||||
|
||||
for gi, g in enumerate(groups):
|
||||
g["parts"] = sorted(g["parts"], key=part_size_score, reverse=True)
|
||||
log(f" group {gi:02d}: angle≈{g['angle']:7.3f} count={len(g['parts'])}")
|
||||
for pj, obj in enumerate(g["parts"]):
|
||||
dims = obj.dimensions
|
||||
log(
|
||||
f" part {pj}: name={obj.name!r} "
|
||||
f"score={part_size_score(obj):10.3f} "
|
||||
f"dims=({dims.x:8.3f},{dims.y:8.3f},{dims.z:8.3f})"
|
||||
)
|
||||
|
||||
if len(g["parts"]) < 2:
|
||||
die(f"Expected at least 2 parts in group {gi}, found {len(g['parts'])}")
|
||||
|
||||
body = g["parts"][0]
|
||||
text = g["parts"][1]
|
||||
|
||||
slot_pairs.append({
|
||||
"angle": angle_deg_for_obj(body),
|
||||
"body": body,
|
||||
"text": text,
|
||||
})
|
||||
|
||||
if len(slot_pairs) != expected_groups:
|
||||
die(f"Expected {expected_groups} angle groups; found {len(slot_pairs)}")
|
||||
|
||||
slot_pairs = sorted(slot_pairs, key=lambda s: s["angle"])
|
||||
return slot_pairs
|
||||
|
||||
def pick_main_parts(parts, expected_groups=10, tolerance_deg=10.0):
|
||||
groups = group_parts_by_angle(parts, tolerance_deg=tolerance_deg)
|
||||
|
||||
log(f"Grouped {len(parts)} raw parts into {len(groups)} angle groups")
|
||||
|
||||
for gi, g in enumerate(groups):
|
||||
g["parts"] = sorted(g["parts"], key=part_size_score, reverse=True)
|
||||
log(f" group {gi:02d}: angle≈{g['angle']:7.3f} count={len(g['parts'])}")
|
||||
for pj, obj in enumerate(g["parts"]):
|
||||
dims = obj.dimensions
|
||||
log(
|
||||
f" part {pj}: name={obj.name!r} "
|
||||
f"score={part_size_score(obj):10.3f} "
|
||||
f"dims=({dims.x:8.3f},{dims.y:8.3f},{dims.z:8.3f})"
|
||||
)
|
||||
|
||||
if len(groups) != expected_groups:
|
||||
die(f"Expected {expected_groups} angle groups; found {len(groups)}")
|
||||
|
||||
main_parts = [g["parts"][0] for g in groups]
|
||||
return sort_parts_by_angle(main_parts)
|
||||
|
||||
def die(msg, rc=2):
|
||||
print(f"ERROR: {msg}")
|
||||
raise SystemExit(rc)
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg)
|
||||
|
||||
|
||||
def hex_to_srgb_rgba(hexstr):
|
||||
"""
|
||||
Accepts:
|
||||
'CC5500', '#CC5500', 'C50', '#C50', 'CC5500FF'
|
||||
Returns:
|
||||
(r, g, b, a) in sRGB 0..1 floats
|
||||
"""
|
||||
h = hexstr.strip()
|
||||
if h.startswith("#"):
|
||||
h = h[1:]
|
||||
if len(h) == 3:
|
||||
h = "".join([c * 2 for c in h])
|
||||
if len(h) == 6:
|
||||
h = h + "FF"
|
||||
if len(h) != 8:
|
||||
die(f"Bad hex color '{hexstr}'. Use RRGGBB or #RRGGBB.")
|
||||
r = int(h[0:2], 16) / 255.0
|
||||
g = int(h[2:4], 16) / 255.0
|
||||
b = int(h[4:6], 16) / 255.0
|
||||
a = int(h[6:8], 16) / 255.0
|
||||
return (r, g, b, a)
|
||||
|
||||
|
||||
def srgb_chan_to_linear(c):
|
||||
# IEC 61966-2-1
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def srgb_rgba_to_linear(rgba):
|
||||
r, g, b, a = rgba
|
||||
return (
|
||||
srgb_chan_to_linear(r),
|
||||
srgb_chan_to_linear(g),
|
||||
srgb_chan_to_linear(b),
|
||||
a,
|
||||
)
|
||||
|
||||
|
||||
def ensure_material(name, rgba, roughness=0.6, metallic=0.0):
|
||||
mat = bpy.data.materials.get(name)
|
||||
if not mat:
|
||||
mat = bpy.data.materials.new(name=name)
|
||||
mat.use_nodes = True
|
||||
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
if bsdf:
|
||||
bsdf.inputs["Base Color"].default_value = rgba
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
|
||||
return mat
|
||||
|
||||
|
||||
def clear_scene():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
pos = []
|
||||
hexes = None
|
||||
roughness = 0.6
|
||||
metallic = 0.0
|
||||
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
a = argv[i]
|
||||
|
||||
if a == "--hexes":
|
||||
if i + 1 >= len(argv):
|
||||
die("Missing value after --hexes")
|
||||
hexes = [x.strip() for x in argv[i + 1].split(",") if x.strip()]
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if a == "--roughness":
|
||||
if i + 1 >= len(argv):
|
||||
die("Missing value after --roughness")
|
||||
roughness = float(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if a == "--metallic":
|
||||
if i + 1 >= len(argv):
|
||||
die("Missing value after --metallic")
|
||||
metallic = float(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
|
||||
pos.append(a)
|
||||
i += 1
|
||||
|
||||
if len(pos) == 1:
|
||||
inp = pos[0]
|
||||
base, _ = os.path.splitext(inp)
|
||||
outp = base + ".glb"
|
||||
elif len(pos) >= 2:
|
||||
inp, outp = pos[0], pos[1]
|
||||
else:
|
||||
die(
|
||||
"USAGE: blender --background --python create_glb_colorpie.py -- "
|
||||
"input.stl [output.glb] "
|
||||
"[--hexes h1,h2,...,h10] [--roughness 0.6] [--metallic 0.0]"
|
||||
)
|
||||
|
||||
if not os.path.exists(inp):
|
||||
die(f"Input not found: {inp}")
|
||||
|
||||
if hexes is None:
|
||||
hexes = [
|
||||
"FFFFFF",
|
||||
"D9D9D9",
|
||||
"BFBFBF",
|
||||
"808080",
|
||||
"606060",
|
||||
"404040",
|
||||
"202020",
|
||||
"000000",
|
||||
"FF6600",
|
||||
"CC5500",
|
||||
]
|
||||
|
||||
if len(hexes) != 10:
|
||||
die(f"--hexes must contain exactly 10 entries; got {len(hexes)}")
|
||||
|
||||
return inp, outp, hexes, roughness, metallic
|
||||
|
||||
|
||||
def import_stl(inp):
|
||||
res = bpy.ops.wm.stl_import(filepath=inp)
|
||||
if 'FINISHED' not in res:
|
||||
die(f"STL import failed for: {inp}")
|
||||
|
||||
objs = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||
if not objs:
|
||||
die("No mesh objects after import")
|
||||
return objs
|
||||
|
||||
|
||||
def select_only(obj):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
|
||||
def separate_loose_parts_from_single_object(obj):
|
||||
"""
|
||||
Blender STL import often creates one mesh object containing
|
||||
many disconnected shells. Separate them into individual objects.
|
||||
"""
|
||||
select_only(obj)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.separate(type='LOOSE')
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
parts = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||
if not parts:
|
||||
die("No mesh objects after Separate by Loose Parts")
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
def world_bbox_center(obj):
|
||||
min_v = Vector((1e30, 1e30, 1e30))
|
||||
max_v = Vector((-1e30, -1e30, -1e30))
|
||||
|
||||
for corner in obj.bound_box:
|
||||
v = obj.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)
|
||||
|
||||
return (min_v + max_v) * 0.5
|
||||
|
||||
|
||||
def angle_deg_for_obj(obj):
|
||||
c = world_bbox_center(obj)
|
||||
a = math.degrees(math.atan2(c.y, c.x))
|
||||
if a < 0:
|
||||
a += 360.0
|
||||
return a
|
||||
|
||||
|
||||
def sort_parts_by_angle(parts):
|
||||
return sorted(parts, key=angle_deg_for_obj)
|
||||
|
||||
|
||||
def assign_material(obj, mat):
|
||||
if obj.type != 'MESH':
|
||||
return
|
||||
if not obj.data.materials:
|
||||
obj.data.materials.append(mat)
|
||||
else:
|
||||
obj.data.materials[0] = mat
|
||||
|
||||
|
||||
def center_all_meshes(objs):
|
||||
min_v = Vector((1e30, 1e30, 1e30))
|
||||
max_v = Vector((-1e30, -1e30, -1e30))
|
||||
|
||||
for o in objs:
|
||||
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
|
||||
|
||||
for o in objs:
|
||||
o.location -= center
|
||||
|
||||
|
||||
def export_glb(outp, objs):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
|
||||
res = bpy.ops.export_scene.gltf(
|
||||
filepath=outp,
|
||||
export_format='GLB',
|
||||
export_apply=True,
|
||||
use_selection=True,
|
||||
)
|
||||
if 'FINISHED' not in res:
|
||||
die(f"GLB export failed: {outp}")
|
||||
|
||||
def ensure_text_material(name="PIE_TEXT_BLACK"):
|
||||
black = srgb_rgba_to_linear((0.0, 0.0, 0.0, 1.0))
|
||||
return ensure_material(name, black, roughness=0.7, metallic=0.0)
|
||||
|
||||
def main():
|
||||
log("create_glb_colorpie.py starting")
|
||||
log(f"Script path: {os.path.abspath(__file__)}")
|
||||
log(f"Python executable: {sys.executable}")
|
||||
log(f"argv: {sys.argv}")
|
||||
|
||||
argv = sys.argv
|
||||
argv = argv[argv.index("--") + 1:] if "--" in argv else []
|
||||
|
||||
inp, outp, hexes, roughness, metallic = parse_args(argv)
|
||||
|
||||
clear_scene()
|
||||
|
||||
imported = import_stl(inp)
|
||||
|
||||
if len(imported) == 1:
|
||||
parts = separate_loose_parts_from_single_object(imported[0])
|
||||
else:
|
||||
parts = imported
|
||||
|
||||
slot_pairs = pick_slot_pairs(parts, expected_groups=10, tolerance_deg=10.0)
|
||||
text_mat = ensure_text_material()
|
||||
export_objs = []
|
||||
|
||||
for i, slot in enumerate(slot_pairs):
|
||||
body = slot["body"]
|
||||
text = slot["text"]
|
||||
|
||||
hexval = hexes[i]
|
||||
srgb = hex_to_srgb_rgba(hexval)
|
||||
linear = srgb_rgba_to_linear(srgb)
|
||||
|
||||
body_mat_name = f"PIE_{i}_{hexval.strip().lstrip('#').upper()}"
|
||||
body_mat = ensure_material(
|
||||
body_mat_name,
|
||||
linear,
|
||||
roughness=roughness,
|
||||
metallic=metallic,
|
||||
)
|
||||
|
||||
assign_material(body, body_mat)
|
||||
assign_material(text, text_mat)
|
||||
|
||||
body.name = f"wedge_{i}"
|
||||
text.name = f"label_{i}"
|
||||
|
||||
export_objs.extend([body, text])
|
||||
|
||||
log(
|
||||
f"Assigned wedge_{i}: angle={angle_deg_for_obj(body):7.3f} "
|
||||
f"hex=#{hexval.strip().lstrip('#').upper()} "
|
||||
f"text={text.name}"
|
||||
)
|
||||
|
||||
center_all_meshes(export_objs)
|
||||
export_glb(outp, export_objs)
|
||||
log(f"Wrote: {outp}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
118
scripts/viewglb
118
scripts/viewglb
|
|
@ -1,118 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# 20260305 ChatGPT
|
||||
# $Header$
|
||||
#
|
||||
# Copy/paste examples:
|
||||
#
|
||||
# ./viewglb model.glb
|
||||
|
||||
FILE="$1"
|
||||
|
||||
if [ -z "$FILE" ]; then
|
||||
echo "Usage: $0 /path/to/file.glb" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "ERROR: file not found: $FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIR=$(dirname "$FILE")
|
||||
BASE=$(basename "$FILE")
|
||||
VIEWER="__glbviewer__.html"
|
||||
LOG="/tmp/viewglb_httpserver.log"
|
||||
|
||||
urlencode() {
|
||||
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$1"
|
||||
}
|
||||
|
||||
find_free_port() {
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
s = socket.socket()
|
||||
s.bind(('127.0.0.1', 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()
|
||||
PY
|
||||
}
|
||||
|
||||
PORT=$(find_free_port)
|
||||
BASE_URL=$(urlencode "$BASE")
|
||||
|
||||
cd "$DIR" || exit 1
|
||||
|
||||
cat >"$VIEWER" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>$BASE</title>
|
||||
</head>
|
||||
<body style="margin:0;background:#222;overflow:hidden;">
|
||||
|
||||
<div style="
|
||||
position:absolute;
|
||||
bottom:10px;
|
||||
left:50%;
|
||||
transform:translateX(-50%);
|
||||
z-index:10;
|
||||
padding:6px 12px;
|
||||
background:rgba(0,0,0,0.65);
|
||||
color:#fff;
|
||||
font-family:monospace;
|
||||
font-size:14px;
|
||||
border-radius:4px;
|
||||
max-width:80vw;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
">$BASE</div>
|
||||
|
||||
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
|
||||
|
||||
<model-viewer src="$BASE_URL"
|
||||
camera-controls
|
||||
auto-rotate
|
||||
style="width:100vw;height:100vh;">
|
||||
</model-viewer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
python3 -m http.server "$PORT" >"$LOG" 2>&1 &
|
||||
SERVER=$!
|
||||
|
||||
# wait up to ~5 seconds for server readiness
|
||||
READY=0
|
||||
for _i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if ! kill -0 "$SERVER" 2>/dev/null; then
|
||||
echo "ERROR: http.server exited early." >&2
|
||||
echo "See log: $LOG" >&2
|
||||
rm -f "$VIEWER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if curl -s "http://127.0.0.1:$PORT/$VIEWER" >/dev/null 2>&1; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ "$READY" -ne 1 ]; then
|
||||
echo "ERROR: server did not become ready on port $PORT" >&2
|
||||
echo "See log: $LOG" >&2
|
||||
kill "$SERVER" 2>/dev/null
|
||||
rm -f "$VIEWER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
firefox-bin "http://127.0.0.1:$PORT/$VIEWER" >/dev/null 2>&1 &
|
||||
|
||||
read -p "Press Enter to stop server..."
|
||||
|
||||
kill "$SERVER" 2>/dev/null
|
||||
rm -f "$VIEWER"
|
||||
Loading…
Add table
Add a link
Reference in a new issue