Interactive WebGL Goldberg polyhedron viewer and painter with PBR shading, adjustable environment lighting, paint tools (pen, brush, circle, fill, line, pick), undo/redo, colour palettes, and mesh relaxation. Added to the standalone experiments index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
3.3 KiB
JavaScript
89 lines
3.3 KiB
JavaScript
export const PAINT_BG = [0.10, 0.13, 0.17];
|
|
export const PAINT_BG_PBR = [0.0, 0.8];
|
|
export const PAINT_BG_NOISE = [0.0, 0.0, 4.0, 0.5]; // scale, strength, octaves, gain
|
|
|
|
export class Paint_State {
|
|
constructor() {
|
|
this.enabled = false;
|
|
this.tool = 'pen'; // 'pen' | 'fill' | 'line'
|
|
this.face_colors = new Map(); // fi → [r, g, b]
|
|
this.face_pbr = new Map(); // fi → [metallic, roughness]
|
|
this.face_noise = new Map(); // fi → [scale, strength, octaves, gain]
|
|
this.face_key = new Map(); // fi → material key string (pre-computed at paint time)
|
|
this.color_left = [0.85, 0.25, 0.12];
|
|
this.color_right = [0.18, 0.45, 0.85];
|
|
this.metallic_left = 0.0;
|
|
this.metallic_right = 0.0;
|
|
this.roughness_left = 0.6;
|
|
this.roughness_right = 0.6;
|
|
this.noise_left = [0.0, 0.0, 4.0, 0.5]; // scale, strength, octaves, gain
|
|
this.noise_right = [0.0, 0.0, 4.0, 0.5];
|
|
this.preview = null; // { faces: Set<fi>, color, pbr, noise } | null
|
|
}
|
|
|
|
_parse_hex(hex) {
|
|
return [parseInt(hex.slice(1,3),16)/255, parseInt(hex.slice(3,5),16)/255, parseInt(hex.slice(5,7),16)/255];
|
|
}
|
|
|
|
_to_hex(c) {
|
|
const h = v => Math.round(v*255).toString(16).padStart(2,'0');
|
|
return `#${h(c[0])}${h(c[1])}${h(c[2])}`;
|
|
}
|
|
|
|
set_hex_left(hex) { this.color_left = this._parse_hex(hex); }
|
|
set_hex_right(hex) { this.color_right = this._parse_hex(hex); }
|
|
hex_left() { return this._to_hex(this.color_left); }
|
|
hex_right() { return this._to_hex(this.color_right); }
|
|
hex(side) { return side === 'right' ? this.hex_right() : this.hex_left(); }
|
|
|
|
_make_key(color, pbr, noise) {
|
|
return `${color.map(v=>v.toFixed(5)).join(',')}|${pbr.map(v=>v.toFixed(5)).join(',')}|${noise.map(v=>v.toFixed(3)).join(',')}`;
|
|
}
|
|
|
|
paint(fi, button) {
|
|
const color = button === 2 ? this.color_right : this.color_left;
|
|
const metallic = button === 2 ? this.metallic_right : this.metallic_left;
|
|
const roughness= button === 2 ? this.roughness_right: this.roughness_left;
|
|
const noise = button === 2 ? this.noise_right : this.noise_left;
|
|
const pbr = [metallic, roughness];
|
|
this.face_colors.set(fi, [...color]);
|
|
this.face_pbr.set(fi, pbr);
|
|
this.face_noise.set(fi, [...noise]);
|
|
this.face_key.set(fi, this._make_key(color, pbr, noise));
|
|
}
|
|
|
|
clear() {
|
|
this.face_colors.clear();
|
|
this.face_pbr.clear();
|
|
this.face_noise.clear();
|
|
this.face_key.clear();
|
|
}
|
|
|
|
set_preview(faces, button) {
|
|
const color = button === 2 ? this.color_right : this.color_left;
|
|
const metallic = button === 2 ? this.metallic_right : this.metallic_left;
|
|
const roughness= button === 2 ? this.roughness_right: this.roughness_left;
|
|
const noise = button === 2 ? this.noise_right : this.noise_left;
|
|
this.preview = { faces: new Set(faces), color: [...color], pbr: [metallic, roughness], noise: [...noise] };
|
|
}
|
|
|
|
commit_preview() {
|
|
if (!this.preview) { return; }
|
|
const color = this.preview.color;
|
|
const pbr = this.preview.pbr ?? PAINT_BG_PBR;
|
|
const noise = this.preview.noise ?? PAINT_BG_NOISE;
|
|
const key = this._make_key(color, pbr, noise);
|
|
for (const fi of this.preview.faces) {
|
|
this.face_colors.set(fi, [...color]);
|
|
this.face_pbr.set(fi, [...pbr]);
|
|
this.face_noise.set(fi, [...noise]);
|
|
this.face_key.set(fi, key);
|
|
}
|
|
this.preview = null;
|
|
}
|
|
|
|
clear_preview() {
|
|
this.preview = null;
|
|
}
|
|
}
|