forked from mikael-lovqvist/websperiments
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>
147 lines
4.9 KiB
JavaScript
147 lines
4.9 KiB
JavaScript
import { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE } from './paint_state.mjs';
|
|
|
|
const MAX_HISTORY = 64;
|
|
|
|
// Ensure key→value is in map/palette; return index.
|
|
function _intern(map, palette, key, val) {
|
|
if (!map.has(key)) { map.set(key, palette.length); palette.push(val); }
|
|
return map.get(key);
|
|
}
|
|
|
|
// Brush/tool settings at snapshot time.
|
|
class Tool_Snapshot {
|
|
constructor(active_brush_side, paint) {
|
|
this.active_brush_side = active_brush_side;
|
|
this.enabled = paint.enabled;
|
|
this.tool = paint.tool;
|
|
this.color_left = [...paint.color_left];
|
|
this.color_right = [...paint.color_right];
|
|
this.metallic_left = paint.metallic_left;
|
|
this.metallic_right = paint.metallic_right;
|
|
this.roughness_left = paint.roughness_left;
|
|
this.roughness_right = paint.roughness_right;
|
|
this.noise_left = [...paint.noise_left];
|
|
this.noise_right = [...paint.noise_right];
|
|
}
|
|
}
|
|
|
|
// Compressed face-paint snapshot.
|
|
// face_mat[fi] = 0 means default (unpainted); >0 = index into mat_palette.
|
|
// mat_palette entries = [color_idx, pbr_idx, noise_idx].
|
|
class Paint_Snapshot {
|
|
constructor(paint, face_count, tool) {
|
|
this.tool = tool;
|
|
|
|
const color_map = new Map();
|
|
const pbr_map = new Map();
|
|
const noise_map = new Map();
|
|
const mat_map = new Map();
|
|
|
|
this.color_palette = [];
|
|
this.pbr_palette = [];
|
|
this.noise_palette = [];
|
|
this.mat_palette = [];
|
|
|
|
// Index 0 reserved for default background material.
|
|
color_map.set(PAINT_BG.join(','), 0); this.color_palette.push([...PAINT_BG]);
|
|
pbr_map.set(PAINT_BG_PBR.join(','), 0); this.pbr_palette.push([...PAINT_BG_PBR]);
|
|
noise_map.set(PAINT_BG_NOISE.join(','), 0); this.noise_palette.push([...PAINT_BG_NOISE]);
|
|
mat_map.set('0|0|0', 0); this.mat_palette.push([0, 0, 0]);
|
|
|
|
this.face_mat = new Uint32Array(face_count); // default 0
|
|
|
|
for (let fi = 0; fi < face_count; fi++) {
|
|
const c = paint.face_colors.get(fi);
|
|
const pbr = paint.face_pbr.get(fi);
|
|
const noise = paint.face_noise.get(fi);
|
|
if (c === undefined && pbr === undefined && noise === undefined) { continue; }
|
|
|
|
const cv = c ?? PAINT_BG;
|
|
const pv = pbr ?? PAINT_BG_PBR;
|
|
const nv = noise ?? PAINT_BG_NOISE;
|
|
|
|
const c_idx = _intern(color_map, this.color_palette, cv.join(','), [...cv]);
|
|
const pbr_idx = _intern(pbr_map, this.pbr_palette, pv.join(','), [...pv]);
|
|
const noi_idx = _intern(noise_map, this.noise_palette, nv.join(','), [...nv]);
|
|
const mat_key = `${c_idx}|${pbr_idx}|${noi_idx}`;
|
|
const mat_idx = _intern(mat_map, this.mat_palette, mat_key, [c_idx, pbr_idx, noi_idx]);
|
|
|
|
// If the material happens to hash to default (all three indices = 0), leave face_mat = 0.
|
|
if (mat_idx !== 0) { this.face_mat[fi] = mat_idx; }
|
|
}
|
|
}
|
|
|
|
// Restore paint Maps from this snapshot. Returns the saved active_brush_side.
|
|
restore(paint) {
|
|
paint.face_colors.clear();
|
|
paint.face_pbr.clear();
|
|
paint.face_noise.clear();
|
|
paint.face_key.clear();
|
|
|
|
for (let fi = 0; fi < this.face_mat.length; fi++) {
|
|
const mat_idx = this.face_mat[fi];
|
|
if (mat_idx === 0) { continue; }
|
|
|
|
const [c_idx, pbr_idx, noi_idx] = this.mat_palette[mat_idx];
|
|
const c = this.color_palette[c_idx];
|
|
const pbr = this.pbr_palette[pbr_idx];
|
|
const noise = this.noise_palette[noi_idx];
|
|
|
|
paint.face_colors.set(fi, [...c]);
|
|
paint.face_pbr.set(fi, [...pbr]);
|
|
paint.face_noise.set(fi, [...noise]);
|
|
paint.face_key.set(fi, paint._make_key(c, pbr, noise));
|
|
}
|
|
|
|
const t = this.tool;
|
|
paint.enabled = t.enabled;
|
|
paint.tool = t.tool;
|
|
paint.color_left = [...t.color_left];
|
|
paint.color_right = [...t.color_right];
|
|
paint.metallic_left = t.metallic_left;
|
|
paint.metallic_right = t.metallic_right;
|
|
paint.roughness_left = t.roughness_left;
|
|
paint.roughness_right = t.roughness_right;
|
|
paint.noise_left = [...t.noise_left];
|
|
paint.noise_right = [...t.noise_right];
|
|
|
|
return t.active_brush_side;
|
|
}
|
|
|
|
byte_size() { return this.face_mat.byteLength; }
|
|
}
|
|
|
|
export class Undo_State {
|
|
constructor() {
|
|
this.history = [];
|
|
this.pos = -1;
|
|
}
|
|
|
|
// Push a new snapshot; truncates redo history.
|
|
push(paint, face_count, active_brush_side) {
|
|
this.history.splice(this.pos + 1);
|
|
this.history.push(new Paint_Snapshot(paint, face_count, new Tool_Snapshot(active_brush_side, paint)));
|
|
if (this.history.length > MAX_HISTORY) { this.history.shift(); } else { this.pos++; }
|
|
}
|
|
|
|
can_undo() { return this.pos > 0; }
|
|
can_redo() { return this.pos < this.history.length - 1; }
|
|
|
|
// Restore previous snapshot; returns active_brush_side or null if nothing to undo.
|
|
undo(paint) {
|
|
if (!this.can_undo()) { return null; }
|
|
this.pos--;
|
|
return this.history[this.pos].restore(paint);
|
|
}
|
|
|
|
// Restore next snapshot; returns active_brush_side or null if nothing to redo.
|
|
redo(paint) {
|
|
if (!this.can_redo()) { return null; }
|
|
this.pos++;
|
|
return this.history[this.pos].restore(paint);
|
|
}
|
|
|
|
// Drop all history (e.g. when mesh topology changes).
|
|
invalidate() { this.history = []; this.pos = -1; }
|
|
}
|