PRE-REFINEMENTS

This commit is contained in:
2026-07-05 20:00:13 -04:00
parent 4314087557
commit 6dcb10c68b
36 changed files with 2056 additions and 0 deletions

112
src/render/stack.js Normal file
View File

@@ -0,0 +1,112 @@
/* ============================================================
stack.js — split one scene across the printable planes of a
physical acrylic stack (front + back of several sheets, air
gaps, backlit). The scene model's per-track z and age stop
being opacity tricks and become actual millimetres.
A PLAN describes the piece:
{ id, seed, size, geometry:{thickness,gap}, params:{...},
planes: [ { sheet:'A', face:'back', label,
tracks: false | true | {kinds, z:[lo,hi], age:[lo,hi]},
disk, boundary, optics, damage, // flags
media: false | true | {reseau,grease,film,splice},
fiducials, header, // flags
params: {...per-plane overrides},
fx: { blur, opacity, scale } }, ... ] }
The scene is generated ONCE from the merged params; each plane
renders a filtered copy through the untouched renderSVG with
transparentPaper (the backlight is the paper). Bloom/core face
pairs must use IDENTICAL track selectors and equal density/size
so their salted bubble draws match — same filtered list, same
positions; only fx differ.
Per-plane depth/aging default LOW: the physical stack now
carries depth; double-encoding it as opacity fights the medium.
============================================================ */
import { generateScene } from '../scene/scene.js';
import { paramsFromSeed } from '../scene/params.js';
import { renderSVG } from './svgVector.js';
const inRange = (v, r) => !r || (v >= r[0] && v <= r[1]);
function filterTracks(tracks, sel) {
if (!sel) return [];
if (sel === true) return tracks;
return tracks.filter(t =>
(!sel.kinds || sel.kinds.includes(t.kind)) &&
inRange(t.z ?? 0, sel.z) &&
inRange(t.age ?? 0, sel.age));
}
function filterMedia(media, sel) {
if (!media || !sel) return null;
if (sel === true) return media;
return {
reseau: sel.reseau ? media.reseau : null,
grease: sel.grease ? media.grease : [],
film: sel.film ? media.film : null,
splice: sel.splice ? media.splice : null,
};
}
/* wrap a rendered plane in optional blur / opacity / centre-scale
(blur is in u = px per 1000; scale is for deliberate moiré offsets) */
function applyFx(svg, fx, size) {
const { blur = 0, opacity = 1, scale = 1 } = fx || {};
if (!blur && opacity === 1 && scale === 1) return svg;
const u = size / 1000, c = size / 2;
const filter = blur
? `<filter id="planefx" x="-15%" y="-15%" width="130%" height="130%"><feGaussianBlur stdDeviation="${(blur * u).toFixed(2)}"/></filter>`
: '';
const g = `<defs>${filter}</defs><g${blur ? ' filter="url(#planefx)"' : ''}`
+ `${opacity !== 1 ? ` opacity="${opacity}"` : ''}`
+ `${scale !== 1 ? ` transform="translate(${c} ${c}) scale(${scale}) translate(${-c} ${-c})"` : ''}>`;
return svg.replace(/(<svg[^>]*>)/, `$1\n${g}`).replace(/<\/svg>\s*$/, '</g></svg>\n');
}
export function renderStack(plan) {
const base = { ...paramsFromSeed(plan.seed), ...(plan.params || {}), invert: true };
const scene = generateScene(base);
const size = plan.size || 1400;
const planes = plan.planes.map((plane) => {
const emit = [];
if (plane.disk) emit.push('disk');
if (plane.tracks || plane.optics || plane.damage || plane.boundary) emit.push('bubble');
if (plane.media || plane.fiducials || plane.header) emit.push('fiduciaries');
const ps = {
...base,
transparentPaper: true, vign: 0,
showBoundary: !!plane.boundary,
showFiducials: !!plane.fiducials,
showHeader: !!plane.header,
depth: 0.15, aging: 0.2, // the stack is the depth model now
emit,
...(plane.params || {}),
};
const sc = {
...scene,
tracks: filterTracks(scene.tracks, plane.tracks ?? false),
shock: plane.disk ? scene.shock : null,
instrument: plane.optics ? scene.instrument : null,
artifacts: plane.damage ? scene.artifacts : null,
media: filterMedia(scene.media, plane.media),
};
const svg = applyFx(renderSVG(sc, ps, size), plane.fx, size);
return { sheet: plane.sheet, face: plane.face, label: plane.label || '', svg };
});
return { plan, scene, planes };
}
/* physical z (mm) of each plane from stack geometry; sheet order = first
appearance in the plan (back of the stack first) */
export function planeDepths(plan) {
const t = plan.geometry?.thickness ?? 3;
const g = plan.geometry?.gap ?? 6;
const sheets = [...new Set(plan.planes.map(p => p.sheet))];
return plan.planes.map(p =>
sheets.indexOf(p.sheet) * (t + g) + (p.face === 'front' ? t : 0));
}