(() => {
'use strict';
let clamp, clamp01, shineStyle, imageUrlToDataUrl, FLAT_COLORS;
function ensureShared() {
if (clamp) return true;
if (!window.shared) {
console.error('Wall.js requires shared functions from script.js');
return false;
}
({ clamp, clamp01, shineStyle, imageUrlToDataUrl, FLAT_COLORS } = window.shared);
return true;
}
const WALL_STATE_KEY = 'wallDesigner:state:v2';
const WALL_FALLBACK_COLOR = '#e5e7eb';
let wallState = null;
let selectedColorIdx = 0; // This should be synced with organic's selectedColorIdx
let wallToolMode = 'paint';
// DOM elements
const wallDisplay = document.getElementById('wall-display');
const wallPaletteEl = document.getElementById('wall-palette');
const wallRowsInput = document.getElementById('wall-rows');
const wallColsInput = document.getElementById('wall-cols');
const wallPatternSelect = document.getElementById('wall-pattern');
const wallGridLabel = document.getElementById('wall-grid-label');
const wallShowWireCb = document.getElementById('wall-show-wire');
const wallOutlineCb = document.getElementById('wall-outline');
const wallClearBtn = document.getElementById('wall-clear');
const wallFillAllBtn = document.getElementById('wall-fill-all');
const wallUsedPaletteEl = document.getElementById('wall-used-palette');
const wallRemoveUnusedBtn = document.getElementById('wall-remove-unused');
const wallReplaceFromSel = document.getElementById('wall-replace-from');
const wallReplaceToSel = document.getElementById('wall-replace-to');
const wallReplaceBtn = document.getElementById('wall-replace-btn');
const wallReplaceMsg = document.getElementById('wall-replace-msg');
const wallReplaceFromChip = document.getElementById('wall-replace-from-chip');
const wallReplaceToChip = document.getElementById('wall-replace-to-chip');
const wallReplaceCount = document.getElementById('wall-replace-count');
const wallToolPaintBtn = document.getElementById('wall-tool-paint');
const wallToolEraseBtn = document.getElementById('wall-tool-erase');
let wallReplaceFromIdx = null;
let wallReplaceToIdx = null;
const wallSpacingLabel = document.getElementById('wall-spacing-label');
const wallSizeLabel = document.getElementById('wall-size-label');
const wallPaintLinksBtn = document.getElementById('wall-paint-links');
const wallPaintSmallBtn = document.getElementById('wall-paint-small');
const wallPaintGapsBtn = document.getElementById('wall-paint-gaps');
const wallActiveChip = document.getElementById('wall-active-color-chip');
const wallActiveLabel = document.getElementById('wall-active-color-label');
const patternKey = () => (wallState.pattern === 'x' ? 'x' : 'grid');
const autoGapColorIdx = () =>
Number.isInteger(wallState?.activeColorIdx) && wallState.activeColorIdx >= 0
? wallState.activeColorIdx
: 0;
const ensurePatternStore = () => {
if (!wallState.patternStore || typeof wallState.patternStore !== 'object') wallState.patternStore = {};
['grid', 'x'].forEach(k => {
if (!wallState.patternStore[k]) {
wallState.patternStore[k] = { colors: [], customColors: {}, fillGaps: false, showWireframes: false, outline: false };
}
});
};
const cloneColors = (colors = []) => colors.map(row => Array.isArray(row) ? [...row] : []);
function saveActivePatternState() {
ensurePatternStore();
const key = patternKey();
wallState.patternStore[key] = {
colors: cloneColors(wallState.colors),
customColors: { ...(wallState.customColors || {}) },
showWireframes: wallState.showWireframes,
outline: wallState.outline
};
}
function loadPatternState(key) {
ensurePatternStore();
const st = wallState.patternStore[key] || {};
wallState.colors = Array.isArray(st.colors) ? cloneColors(st.colors) : [];
wallState.customColors = (st.customColors && typeof st.customColors === 'object') ? { ...st.customColors } : {};
if (typeof st.showWireframes === 'boolean') wallState.showWireframes = st.showWireframes;
if (typeof st.outline === 'boolean') wallState.outline = st.outline;
}
function wallDefaultState() {
// Default to wireframes on so empty cells are visible/clickable.
return { rows: 7, cols: 9, spacing: 75, bigSize: 52, pattern: 'grid', fillGaps: false, showWireframes: true, outline: true, colors: [], customColors: {}, patternStore: {}, activeColorIdx: 0 };
}
// Build FLAT_COLORS locally if shared failed to populate (e.g., palette not ready)
function ensureFlatColors() {
if (Array.isArray(FLAT_COLORS) && FLAT_COLORS.length > 0) return;
if (!Array.isArray(window.PALETTE)) return;
console.warn('[Wall] FLAT_COLORS missing; rebuilding from window.PALETTE');
let idx = 0;
window.PALETTE.forEach(group => {
(group.colors || []).forEach(c => {
if (!c?.hex) return;
const item = { ...c, family: group.family, _idx: idx++ };
FLAT_COLORS.push(item);
});
});
}
function loadWallState() {
const base = wallDefaultState();
try {
const saved = JSON.parse(localStorage.getItem(WALL_STATE_KEY));
if (saved && typeof saved === 'object') {
base.rows = clamp(saved.rows ?? base.rows, 2, 20);
base.cols = clamp(saved.cols ?? base.cols, 2, 20);
base.spacing = 75; // fixed
base.bigSize = 52; // fixed
base.pattern = saved.pattern === 'x' ? 'x' : 'grid';
base.fillGaps = false;
base.showWireframes = saved.showWireframes !== false;
base.patternStore = (saved.patternStore && typeof saved.patternStore === 'object') ? saved.patternStore : {};
base.customColors = (saved.customColors && typeof saved.customColors === 'object') ? saved.customColors : {};
if (Number.isInteger(saved.activeColorIdx)) base.activeColorIdx = saved.activeColorIdx;
if (Array.isArray(saved.colors)) base.colors = cloneColors(saved.colors);
if (typeof saved.outline === 'boolean') base.outline = saved.outline;
}
} catch {}
return base;
}
function saveWallState() {
try { localStorage.setItem(WALL_STATE_KEY, JSON.stringify(wallState)); } catch {}
}
function ensureWallGridSize(rows, cols) {
const r = clamp(Math.round(rows || 0), 2, 20);
const c = clamp(Math.round(cols || 0), 2, 20);
wallState.rows = r;
wallState.cols = c;
const mainRows = wallState.pattern === 'grid' ? Math.max(1, r - 1) : r;
const mainCols = wallState.pattern === 'grid' ? Math.max(1, c - 1) : c;
if (!Array.isArray(wallState.colors)) wallState.colors = [];
while (wallState.colors.length < mainRows) wallState.colors.push([]);
if (wallState.colors.length > mainRows) wallState.colors.length = mainRows;
for (let i = 0; i < mainRows; i++) {
const row = wallState.colors[i] || [];
while (row.length < mainCols) row.push(-1);
if (row.length > mainCols) row.length = mainCols;
wallState.colors[i] = row;
}
if (!wallState.customColors || typeof wallState.customColors !== 'object') wallState.customColors = {};
const keys = Object.keys(wallState.customColors);
keys.forEach(k => {
const parts = k.split('-');
const type = parts[0];
const parseRC = (ri, ci) => ({ rVal: parseInt(ri, 10), cVal: parseInt(ci, 10) });
if (type === 'g' && (parts[1] === 'h' || parts[1] === 'v')) {
const { rVal, cVal } = parseRC(parts[2], parts[3]);
if (!Number.isInteger(rVal) || !Number.isInteger(cVal)) { delete wallState.customColors[k]; return; }
if (parts[1] === 'h' && (rVal >= r || cVal >= c - 1)) delete wallState.customColors[k];
else if (parts[1] === 'v' && (rVal >= r - 1 || cVal >= c)) delete wallState.customColors[k];
return;
}
if (type === 'f') {
// f-h/f-v/f-x
if (parts[1] === 'h' || parts[1] === 'v') {
const { rVal, cVal } = parseRC(parts[2], parts[3]);
if (!Number.isInteger(rVal) || !Number.isInteger(cVal)) { delete wallState.customColors[k]; return; }
if (parts[1] === 'h' && (rVal >= r - 1 || cVal >= c - 1)) delete wallState.customColors[k];
else if (parts[1] === 'v' && (rVal >= r - 1 || cVal >= c - 1)) delete wallState.customColors[k];
return;
}
if (parts[1] === 'x') {
const { rVal, cVal } = parseRC(parts[2], parts[3]);
if (!Number.isInteger(rVal) || !Number.isInteger(cVal)) { delete wallState.customColors[k]; return; }
if (rVal <= 0 || cVal <= 0 || rVal >= r - 1 || cVal >= c - 1) delete wallState.customColors[k];
return;
}
}
const { rVal, cVal } = parseRC(parts[1], parts[2]);
if (!Number.isInteger(rVal) || !Number.isInteger(cVal)) { delete wallState.customColors[k]; return; }
if (type === 'h' && (rVal >= r || cVal >= c - 1)) delete wallState.customColors[k];
else if (type === 'v' && (rVal >= r - 1 || cVal >= c)) delete wallState.customColors[k];
else if (type === 'g' && (rVal >= r - 1 || cVal >= c - 1)) delete wallState.customColors[k];
else if ((type === 'c' || type.startsWith('l') || type === 'f') && (rVal >= r - 1 || cVal >= c - 1)) delete wallState.customColors[k];
});
}
const wallColorMeta = (idx) => {
const meta = (Number.isInteger(idx) && idx >= 0 && FLAT_COLORS[idx]) ? FLAT_COLORS[idx] : { hex: WALL_FALLBACK_COLOR };
return meta;
};
async function buildWallSvgPayload(forExport = false, customColorsOverride = null) {
ensureFlatColors();
const customColors = customColorsOverride || wallState.customColors;
if (!ensureShared()) throw new Error('Wall designer shared helpers missing.');
if (!wallState) wallState = loadWallState();
ensurePatternStore();
ensureWallGridSize(wallState.rows, wallState.cols);
const rows = wallState.rows;
const cols = wallState.cols;
const r11 = Math.max(12, (Number(wallState.bigSize) || 54) / 2);
const r5 = Math.max(6, Math.round(r11 * 0.42));
const isGrid = wallState.pattern === 'grid';
const isX = wallState.pattern === 'x';
const spacingBase = Number(wallState.spacing) || 75;
const spacing = clamp(
isGrid
? Math.max(2 * r11 + r5 * 0.15, spacingBase * 0.7)
: spacingBase,
30,
140
); // tighter for grid, nearly touching
const bigR = r11;
const smallR = r5;
const linkDims = { rx: bigR * 0.8, ry: bigR * 0.6 }; // Fatter oval
const fiveInchDims = { rx: smallR, ry: smallR };
const labelPad = 30;
const margin = Math.max(bigR + smallR + 18, 28);
const offsetX = margin + labelPad;
const offsetY = margin + labelPad;
const showWireframes = !!wallState.showWireframes;
const showOutline = !!wallState.outline;
const colSpacing = spacing;
const rowStep = spacing;
const showGaps = false;
const uniqueImages = new Set();
wallState.colors.forEach(row => row.forEach(idx => {
const meta = wallColorMeta(idx);
if (meta.image) uniqueImages.add(meta.image);
}));
Object.values(customColors || {}).forEach(idx => {
const meta = wallColorMeta(idx);
if (meta.image) uniqueImages.add(meta.image);
});
const dataUrlMap = new Map();
if (forExport && uniqueImages.size) {
await Promise.all([...uniqueImages].map(async (url) => dataUrlMap.set(url, await imageUrlToDataUrl(url))));
}
const defs = [];
const patterns = new Map();
const SVG_PATTERN_ZOOM = 2.5;
const offset = (1 - SVG_PATTERN_ZOOM) / 2;
const ensurePattern = (meta) => {
if (!meta?.image) return null;
const key = `${meta.image}|${meta.hex}`;
if (patterns.has(key)) return patterns.get(key);
const href = forExport ? (dataUrlMap.get(meta.image) || null) : meta.image;
if (!href) return null;
const id = `wallp-${patterns.size}`;
patterns.set(key, id);
defs.push(``);
return id;
};
const shadowFilters = new Map();
const ensureShadowFilter = (dx, dy, blurPx, alpha) => {
const key = `${dx}|${dy}|${blurPx}|${alpha}`;
if (!shadowFilters.has(key)) {
const id = `wall-shadow-${shadowFilters.size}`;
const stdDev = Math.max(0.01, blurPx * 0.5);
const clampedAlpha = clamp01(alpha);
const flood = ``;
const blur = ``;
const offsetNode = ``;
const composite = ``;
const merge = ``;
defs.push(`${flood}${blur}${offsetNode}${composite}${merge}`);
shadowFilters.set(key, id);
}
return shadowFilters.get(key);
};
const bigShadow = ensureShadowFilter(0, 3, 8, 0.18);
const smallShadow = ensureShadowFilter(0, 2, 4, 0.14);
const shineShadow = ensureShadowFilter(0, 0, 3, 0.08);
const positions = new Map();
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const gx = offsetX + c * colSpacing;
const gy = offsetY + r * rowStep;
positions.set(`${r}-${c}`, { x: gx, y: gy });
minX = Math.min(minX, gx - bigR);
maxX = Math.max(maxX, gx + bigR);
minY = Math.min(minY, gy - bigR);
maxY = Math.max(maxY, gy + bigR);
}
}
const width = maxX + margin;
const height = maxY + margin;
const vb = `0 0 ${width} ${height}`;
const smallNodes = [];
const bigNodes = [];
const labels = [];
for (let c = 0; c < cols; c++) {
const x = offsetX + c * colSpacing;
labels.push(`${c + 1}`);
}
for (let r = 0; r < rows; r++) {
const y = offsetY + r * rowStep;
labels.push(`${r + 1}`);
}
const customOverride = (key) => {
const raw = customColors?.[key];
const parsed = Number.isInteger(raw) ? raw : Number.parseInt(raw, 10);
if (parsed === -1) return { mode: 'empty' };
if (Number.isInteger(parsed) && parsed >= 0) {
return { mode: 'color', idx: normalizeColorIdx(parsed) };
}
return { mode: 'auto' };
};
// Helper to create a shine ellipse with coordinates relative to (0,0)
const shineNodeRelative = (rx, ry, hex, rot = -20) => {
const shine = shineStyle(hex || WALL_FALLBACK_COLOR);
const sx_relative = -rx * 0.25;
const sy_relative = -ry * 0.25;
const shineRx = rx * 0.48;
const shineRy = ry * 0.28;
const stroke = shine.stroke ? `stroke="${shine.stroke}" stroke-width="1"` : '';
const shineFilter = shineShadow ? `filter="url(#${shineShadow})"` : '';
return ``;
};
if (isGrid) {
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const pos = positions.get(`${r}-${c}`);
const keyId = `p-${r}-${c}`;
const override = customOverride(keyId);
const customIdx = override.mode === 'color' ? override.idx : null;
const isEmpty = override.mode === 'empty' || customIdx === null;
const invisible = isEmpty && !showWireframes;
const hitFill = 'rgba(0,0,0,0.001)';
const meta = wallColorMeta(customIdx);
const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (isEmpty ? (showWireframes ? 1.4 : 0) : (showOutline ? 0.6 : 0));
const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
smallNodes.push(`
${shine}
`);
}
}
// Gap 11" balloons between centers (horizontal/vertical midpoints) inside the grid (exclude only right edge for horizontals, bottom edge for verticals)
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols - 1; c++) {
const p1 = positions.get(`${r}-${c}`);
const p2 = positions.get(`${r}-${c+1}`);
const mid = { x: (p1.x + p2.x) / 2, y: p1.y };
const keyId = `h-${r}-${c}`;
const override = customOverride(keyId);
const customIdx = override.mode === 'color' ? override.idx : null;
const isEmpty = override.mode === 'empty' || customIdx === null;
const invisible = isEmpty && !showWireframes;
const hitFill = 'rgba(0,0,0,0.001)';
const meta = wallColorMeta(customIdx);
const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
console.log(`h-r-c: keyId: ${keyId}, customIdx: ${customIdx}, isEmpty: ${isEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`
${shine}
`);
}
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols; c++) {
const p1 = positions.get(`${r}-${c}`);
const p2 = positions.get(`${r+1}-${c}`);
const mid = { x: p1.x, y: (p1.y + p2.y) / 2 };
const keyId = `v-${r}-${c}`;
const override = customOverride(keyId);
const customIdx = override.mode === 'color' ? override.idx : null;
const isEmpty = override.mode === 'empty' || customIdx === null;
const invisible = isEmpty && !showWireframes;
const hitFill = 'rgba(0,0,0,0.001)';
const meta = wallColorMeta(customIdx);
const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`
${shine}
`);
}
}
// Gap 11" balloons at square centers
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
const pTL = positions.get(`${r}-${c}`);
const pBR = positions.get(`${r+1}-${c+1}`);
const center = { x: (pTL.x + pBR.x) / 2, y: (pTL.y + pBR.y) / 2 };
const gapKey = `g-${r}-${c}`;
const override = customOverride(gapKey);
const gapIdx = override.mode === 'color'
? override.idx
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
const isEmpty = gapIdx === null;
const hitFill = 'rgba(0,0,0,0.001)';
// Hide wireframes for 11" gap balloons; keep them clickable with a hit target.
const invisible = isEmpty;
const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex);
const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none');
const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0);
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
bigNodes.push(`
${shineGap}
`);
}
}
} else if (isX) {
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
const p1 = positions.get(`${r}-${c}`);
const p2 = positions.get(`${r}-${c+1}`);
const p3 = positions.get(`${r+1}-${c}`);
const p4 = positions.get(`${r+1}-${c+1}`);
const center = { x: (p1.x + p4.x) / 2, y: (p1.y + p4.y) / 2 };
const centerKey = `c-${r}-${c}`;
const centerOverride = customOverride(centerKey);
const centerCustomIdx = centerOverride.mode === 'color' ? centerOverride.idx : null;
const centerIsEmpty = centerOverride.mode === 'empty' || centerCustomIdx === null;
const invisible = centerIsEmpty && !showWireframes;
const meta = wallColorMeta(centerCustomIdx);
const patId = ensurePattern(meta);
const fill = invisible ? 'rgba(0,0,0,0.001)' : (centerIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
console.log(`c-r-c: keyId: ${centerKey}, customIdx: ${centerCustomIdx}, isEmpty: ${centerIsEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
const stroke = invisible ? 'none' : (centerIsEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (centerIsEmpty ? 1.4 : (showOutline ? 0.6 : 0));
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
const shine = centerIsEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
smallNodes.push(`
${shine}
`);
const targets = [p1, p2, p4, p3];
const linkKeys = [`l1-${r}-${c}`, `l2-${r}-${c}`, `l3-${r}-${c}`, `l4-${r}-${c}`];
for (let i = 0; i < 4; i++) {
const target = targets[i];
const mid = { x: (center.x + target.x) / 2, y: (center.y + target.y) / 2 };
const angle = Math.atan2(target.y - center.y, target.x - center.x) * 180 / Math.PI;
const linkKey = linkKeys[i];
const linkOverride = customOverride(linkKey);
const linkCustomIdx = linkOverride.mode === 'color' ? linkOverride.idx : null;
const linkIsEmpty = linkOverride.mode === 'empty' || linkCustomIdx === null;
const invisibleLink = linkIsEmpty && !showWireframes;
const meta = wallColorMeta(linkCustomIdx);
const patId = ensurePattern(meta);
const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta);
// Outline only when filled; light wireframe when empty and wireframes shown.
const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0));
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`
${shine}
`);
}
}
}
// Gap 11" balloons between centers (horizontal/vertical midpoints) across the X pattern
// Keep gaps at midpoints; skip the top row and the far-left column.
const maxCH = Math.max(0, cols - 1);
// Diamond 5" fillers at original grid intersections (skip border)
for (let r = 1; r < rows - 1; r++) {
for (let c = 1; c < cols - 1; c++) {
const pos = positions.get(`${r}-${c}`);
if (!pos) continue;
const fillerKey = `f-x-${r}-${c}`;
const fillerOverride = customOverride(fillerKey);
const fillerIdx = fillerOverride.mode === 'color' ? fillerOverride.idx : null;
const fillerEmpty = fillerOverride.mode === 'empty' || fillerIdx === null;
const fillerInvisible = fillerEmpty && !showWireframes;
const fillerMeta = wallColorMeta(fillerIdx);
const fillerPat = ensurePattern(fillerMeta);
const fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? (showWireframes ? 'none' : 'rgba(0,0,0,0.001)') : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
const fillerStroke = fillerInvisible ? 'none' : (fillerEmpty ? (showWireframes ? '#cbd5e1' : 'none') : 'none');
const fillerStrokeW = fillerInvisible ? 0 : (fillerEmpty ? (showWireframes ? 1.2 : 0) : 0);
const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`;
const fillerShine = fillerEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, fillerMeta.hex);
smallNodes.push(`
${fillerShine}
`);
}
}
for (let r = 1; r < rows - 1; r++) {
for (let c = 0; c < maxCH; c++) {
const p1 = positions.get(`${r}-${c}`);
const p2 = positions.get(`${r}-${c+1}`);
const mid = { x: (p1.x + p2.x) / 2, y: p1.y };
const key = `g-h-${r}-${c}`;
const override = customOverride(key);
const gapIdx = override.mode === 'color'
? override.idx
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
const isEmpty = gapIdx === null;
const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta);
const invisible = isEmpty && !showGaps;
const fill = invisible ? 'rgba(0,0,0,0.001)' : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
bigNodes.push(`
${shineGap}
`);
}
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 1; c < maxCH; c++) { // start at column 1 to keep far-left clear
const p1 = positions.get(`${r}-${c}`);
const p2 = positions.get(`${r+1}-${c}`);
const mid = { x: p1.x, y: (p1.y + p2.y) / 2 };
const key = `g-v-${r}-${c}`;
const override = customOverride(key);
const gapIdx = override.mode === 'color'
? override.idx
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
const isEmpty = gapIdx === null;
const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta);
const invisible = isEmpty && !showGaps;
const fill = invisible ? 'rgba(0,0,0,0.001)' : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
bigNodes.push(`
${shineGap}
`);
}
}
}
const svgString = ``;
return { svgString, width, height };
}
function wallUsedColors() {
ensureFlatColors();
if (!wallState) wallState = loadWallState();
ensureWallGridSize(wallState.rows, wallState.cols);
const map = new Map();
const addIdx = (idx) => {
if (!Number.isInteger(idx) || idx < 0 || !FLAT_COLORS[idx]) return;
const meta = FLAT_COLORS[idx];
const key = idx;
const entry = map.get(key) || { idx, hex: meta.hex, image: meta.image, name: meta.name, count: 0 };
entry.count += 1;
map.set(key, entry);
};
wallState.colors.forEach(row => row.forEach(addIdx));
Object.values(wallState.customColors || {}).forEach(addIdx);
return Array.from(map.values()).sort((a, b) => b.count - a.count);
}
function renderWallUsedPalette() {
if (!wallUsedPaletteEl) return;
wallUsedPaletteEl.innerHTML = '
Palette opens in modal.
';
populateWallReplaceSelects();
updateWallReplacePreview();
}
function populateWallReplaceSelects() {
ensureFlatColors();
// "From" = only colors currently used on the wall
if (wallReplaceFromSel) {
wallReplaceFromSel.innerHTML = '';
const used = wallUsedColors();
used.forEach(u => {
const opt = document.createElement('option');
opt.value = String(u.idx);
opt.textContent = `${u.name || u.hex} (${u.count})`;
wallReplaceFromSel.appendChild(opt);
});
if (used.length) {
const val = wallReplaceFromIdx ?? used[0].idx;
wallReplaceFromSel.value = String(val);
wallReplaceFromIdx = val;
}
}
// "To" = colors from the wall palette
if (wallReplaceToSel) {
wallReplaceToSel.innerHTML = '';
(window.PALETTE || []).forEach(group => {
(group.colors || []).forEach(c => {
const idx = FLAT_COLORS.findIndex(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family);
if (idx < 0) return;
const opt = document.createElement('option');
opt.value = String(idx);
opt.textContent = c.name || c.hex;
wallReplaceToSel.appendChild(opt);
});
});
if (wallReplaceToSel.options.length) {
const val = wallReplaceToIdx ?? parseInt(wallReplaceToSel.options[0].value, 10);
wallReplaceToSel.value = String(val);
wallReplaceToIdx = val;
}
}
}
const wallSetChip = (chip, meta) => {
if (!chip) return;
if (meta?.image) {
chip.style.backgroundImage = `url("${meta.image}")`;
chip.style.backgroundColor = meta.hex || WALL_FALLBACK_COLOR;
chip.style.backgroundSize = `${100 * 2.5}%`;
chip.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
chip.style.backgroundImage = 'none';
chip.style.backgroundColor = meta?.hex || WALL_FALLBACK_COLOR;
}
};
const wallCountMatches = (idx) => {
if (!Number.isInteger(idx) || idx < 0) return 0;
ensureWallGridSize(wallState.rows, wallState.cols);
let count = 0;
wallState.colors.forEach(row => row.forEach(v => { if (v === idx) count++; }));
Object.values(wallState.customColors || {}).forEach(v => { if (Number.isInteger(v) && v === idx) count++; });
return count;
};
const updateWallReplacePreview = () => {
let fromIdx = Number.isInteger(wallReplaceFromIdx) ? wallReplaceFromIdx : parseInt(wallReplaceFromSel?.value || '-1', 10);
let toIdx = Number.isInteger(wallReplaceToIdx) ? wallReplaceToIdx : parseInt(wallReplaceToSel?.value || '-1', 10);
if ((!Number.isInteger(fromIdx) || fromIdx < 0) && wallReplaceFromSel?.options?.length) {
fromIdx = parseInt(wallReplaceFromSel.options[0].value, 10);
wallReplaceFromSel.value = String(fromIdx);
wallReplaceFromIdx = fromIdx;
}
if ((!Number.isInteger(toIdx) || toIdx < 0) && wallReplaceToSel?.options?.length) {
toIdx = parseInt(wallReplaceToSel.options[0].value, 10);
wallReplaceToSel.value = String(toIdx);
wallReplaceToIdx = toIdx;
}
wallSetChip(wallReplaceFromChip, wallColorMeta(fromIdx));
wallSetChip(wallReplaceToChip, wallColorMeta(toIdx));
const cnt = wallCountMatches(fromIdx);
if (wallReplaceCount) wallReplaceCount.textContent = cnt ? `${cnt} match${cnt === 1 ? '' : 'es'}` : '0 matches';
return cnt;
};
const openWallReplacePicker = (mode = 'from') => {
const picker = window.openColorPicker;
if (!picker) return;
populateWallReplaceSelects();
if (mode === 'from') {
const used = wallUsedColors();
const items = used.map(u => ({
label: u.name || u.hex,
metaText: `${u.count} in wall`,
idx: u.idx
}));
if (!items.length) {
if (wallReplaceMsg) wallReplaceMsg.textContent = 'No colors on the wall yet.';
return;
}
picker({
title: 'Replace: From color',
subtitle: 'Pick a color currently used in the wall',
items,
onSelect: (item) => {
if (!wallReplaceFromSel) return;
wallReplaceFromSel.value = String(item.idx);
wallReplaceFromIdx = item.idx;
updateWallReplacePreview();
}
});
} else {
ensureFlatColors();
const items = [];
(window.PALETTE || []).forEach(group => {
(group.colors || []).forEach(c => {
const idx = FLAT_COLORS.findIndex(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family);
if (idx >= 0) {
items.push({
label: c.name || c.hex,
metaText: group.family || '',
idx
});
}
});
});
if (!items.length) {
if (wallReplaceMsg) wallReplaceMsg.textContent = 'No palette colors available.';
return;
}
picker({
title: 'Replace: To color',
subtitle: 'Choose any color from the wall palette',
items,
onSelect: (item) => {
if (!wallReplaceToSel) return;
wallReplaceToSel.value = String(item.idx);
wallReplaceToIdx = item.idx;
updateWallReplacePreview();
}
});
}
};
// Pick a visible default (first reasonably saturated entry).
function defaultActiveColorIdx() {
if (!Array.isArray(FLAT_COLORS) || !FLAT_COLORS.length) return 0;
const isTooLight = (hex = '') => {
const h = hex.replace('#', '');
if (h.length !== 6) return false;
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return (r + g + b) > 640; // avoid near-white/pastel defaults
};
const firstVisible = FLAT_COLORS.find(c => c?.hex && !isTooLight(c.hex));
if (firstVisible) {
const idx = Number.isInteger(firstVisible._idx) ? firstVisible._idx : FLAT_COLORS.indexOf(firstVisible);
if (idx >= 0) return idx;
}
return 0;
}
const normalizeColorIdx = (idx) => {
const fallback = defaultActiveColorIdx();
if (!Number.isInteger(idx)) return fallback;
if (idx < 0) return fallback;
if (Array.isArray(FLAT_COLORS) && FLAT_COLORS.length > 0 && idx >= FLAT_COLORS.length) {
return FLAT_COLORS.length - 1;
}
return idx;
};
function setActiveColor(idx) {
selectedColorIdx = normalizeColorIdx(idx);
wallState.activeColorIdx = selectedColorIdx;
updateWallActiveChip(selectedColorIdx);
if (window.organic?.setColor) {
window.organic.setColor(selectedColorIdx);
} else if (window.organic?.updateCurrentColorChip) {
window.organic.updateCurrentColorChip(selectedColorIdx);
}
saveWallState();
}
// Sync the wall's active color with the global/organic selection when available.
function syncActiveColorFromOrganic() {
const organicIdx = window.organic?.getColor?.();
if (!Number.isInteger(organicIdx)) return null;
const normalized = normalizeColorIdx(organicIdx);
if (normalized !== selectedColorIdx) {
selectedColorIdx = normalized;
if (wallState) wallState.activeColorIdx = normalized;
saveWallState();
renderWallPalette();
updateWallActiveChip(normalized);
}
return normalized;
}
// Read the current UI-selected color. Prefer the global/organic selection so the active color chip always drives wall clicks.
// Current active color: prefer organic tab, then wall selection, then stored default.
function getActiveWallColorIdx() {
const organicIdx = syncActiveColorFromOrganic();
if (Number.isInteger(organicIdx)) return organicIdx;
if (Number.isInteger(selectedColorIdx)) return normalizeColorIdx(selectedColorIdx);
if (Number.isInteger(wallState?.activeColorIdx)) return normalizeColorIdx(wallState.activeColorIdx);
return defaultActiveColorIdx();
}
// Normalize the stored color for a wall key to either a valid index or null (empty).
function getStoredColorForKey(key) {
if (!wallState?.customColors) return null;
const raw = wallState.customColors[key];
const parsed = Number.isInteger(raw) ? raw : Number.parseInt(raw, 10);
if (!Number.isInteger(parsed)) return null;
if (parsed < 0) return null;
const val = normalizeColorIdx(parsed);
if (val < 0) return null;
wallState.customColors[key] = val; // write back normalized numeric value
return val;
}
function updateWallActiveChip(idx) {
if (!wallActiveChip || !wallActiveLabel) return;
ensureFlatColors();
const meta = wallColorMeta(idx);
if (meta.image) {
wallActiveChip.style.backgroundImage = `url("${meta.image}")`;
const zoom = Math.max(1, meta.imageZoom ?? 2.5);
wallActiveChip.style.backgroundSize = `${100 * zoom}%`;
wallActiveChip.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
wallActiveChip.style.backgroundColor = '#fff';
} else {
wallActiveChip.style.backgroundImage = 'none';
wallActiveChip.style.backgroundSize = '';
wallActiveChip.style.backgroundPosition = '';
wallActiveChip.style.backgroundColor = meta.hex || WALL_FALLBACK_COLOR;
}
wallActiveLabel.textContent = meta.name || meta.hex || '';
}
function setWallToolMode(mode) {
wallToolMode = mode === 'erase' ? 'erase' : 'paint';
if (wallToolPaintBtn && wallToolEraseBtn) {
const isErase = wallToolMode === 'erase';
wallToolPaintBtn.setAttribute('aria-pressed', String(!isErase));
wallToolEraseBtn.setAttribute('aria-pressed', String(isErase));
wallToolPaintBtn.classList.toggle('tab-active', !isErase);
wallToolEraseBtn.classList.toggle('tab-active', isErase);
wallToolPaintBtn.classList.toggle('tab-idle', isErase);
wallToolEraseBtn.classList.toggle('tab-idle', !isErase);
}
}
// Paint a specific group of nodes with the active color.
function paintWallGroup(group) {
ensureWallGridSize(wallState.rows, wallState.cols);
const idx = getActiveWallColorIdx();
if (!Number.isInteger(idx)) return;
const rows = wallState.rows;
const cols = wallState.cols;
const isGrid = wallState.pattern === 'grid';
const custom = { ...wallState.customColors };
const set = (key) => { custom[key] = idx; };
if (group === 'links') {
if (isGrid) {
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols - 1; c++) set(`h-${r}-${c}`);
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols; c++) set(`v-${r}-${c}`);
}
} else {
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
['l1', 'l2', 'l3', 'l4'].forEach(l => set(`${l}-${r}-${c}`));
}
}
}
} else if (group === 'small') {
if (isGrid) {
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) set(`p-${r}-${c}`);
}
wallState.colors = wallState.colors.map(row => row.map(() => idx));
} else {
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
set(`c-${r}-${c}`);
set(`f-x-${r+1}-${c+1}`); // diamond 5" fillers between link crosses
}
}
}
} else if (group === 'gaps') {
if (isGrid) {
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) set(`g-${r}-${c}`);
}
} else {
const maxCH = Math.max(0, cols - 1);
for (let r = 1; r < rows - 1; r++) {
for (let c = 0; c < maxCH; c++) set(`g-h-${r}-${c}`);
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 1; c < maxCH; c++) set(`g-v-${r}-${c}`);
}
}
} else if (group === 'filler') {
if (!isGrid) {
for (let r = 1; r < rows - 1; r++) {
for (let c = 1; c < cols - 1; c++) set(`f-x-${r}-${c}`);
}
}
} else {
return;
}
wallState.customColors = custom;
saveActivePatternState();
saveWallState();
renderWall();
}
async function renderWall() {
if (!wallDisplay) return;
ensureWallGridSize(wallState.rows, wallState.cols);
if (wallGridLabel) wallGridLabel.textContent = `${wallState.cols} × ${wallState.rows}`;
try {
console.info('[Wall] render start');
const { svgString } = await buildWallSvgPayload(false);
wallDisplay.innerHTML = svgString;
// Force a reflow to ensure the browser repaints the new SVG.
void wallDisplay.offsetWidth;
renderWallUsedPalette();
updateWallReplacePreview();
console.info('[Wall] render done');
} catch (err) {
console.error('[Wall] render failed', err?.stack || err);
wallDisplay.innerHTML = `Could not render wall.
`;
}
}
function renderWallPalette() {
if (!wallPaletteEl) return;
wallPaletteEl.innerHTML = '';
populateWallReplaceSelects();
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn-dark w-full';
btn.textContent = 'Choose color';
btn.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Choose active wall color',
subtitle: 'Applies to wall fill tools',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
setActiveColor(item.idx);
updateWallActiveChip(getActiveWallColorIdx());
updateWallReplacePreview();
}
});
});
wallPaletteEl.appendChild(btn);
renderWallUsedPalette();
updateWallActiveChip(getActiveWallColorIdx());
updateWallReplacePreview();
}
function syncWallInputs() {
if (!wallState) wallState = wallDefaultState();
ensureWallGridSize(wallState.rows, wallState.cols);
if (wallRowsInput) wallRowsInput.value = wallState.rows;
if (wallColsInput) wallColsInput.value = wallState.cols;
if (wallSpacingLabel) wallSpacingLabel.textContent = `${wallState.spacing} px (fixed)`;
if (wallSizeLabel) wallSizeLabel.textContent = `${wallState.bigSize} px (fixed)`;
if (wallGridLabel) wallGridLabel.textContent = `${wallState.cols} × ${wallState.rows}`;
if (wallPatternSelect) wallPatternSelect.value = wallState.pattern || 'grid';
if (wallShowWireCb) wallShowWireCb.checked = wallState.showWireframes !== false;
if (wallOutlineCb) wallOutlineCb.checked = !!wallState.outline;
}
function initWallDesigner() {
if (!ensureShared()) return;
ensureFlatColors();
if (!wallDisplay) return;
wallState = loadWallState();
ensurePatternStore();
loadPatternState(patternKey());
if (Number.isInteger(wallState.activeColorIdx)) selectedColorIdx = normalizeColorIdx(wallState.activeColorIdx);
else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor());
else selectedColorIdx = defaultActiveColorIdx();
setActiveColor(selectedColorIdx);
setWallToolMode('paint');
// Allow picking active wall color by clicking the chip.
if (wallActiveChip && window.openColorPicker) {
wallActiveChip.style.cursor = 'pointer';
wallActiveChip.addEventListener('click', () => {
window.openColorPicker({
title: 'Choose wall color',
subtitle: 'Sets the active wall color',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
setActiveColor(item.idx);
renderWallPalette();
renderWallUsedPalette();
renderWall();
}
});
});
}
loadPatternState(patternKey());
ensureWallGridSize(wallState.rows, wallState.cols);
syncWallInputs();
renderWallPalette();
renderWall();
saveActivePatternState();
saveWallState();
wallRowsInput?.addEventListener('change', () => {
const rows = clamp(parseInt(wallRowsInput?.value || '0', 10) || wallState.rows, 2, 20);
ensureWallGridSize(rows, wallState.cols);
saveWallState();
syncWallInputs();
renderWall();
});
wallColsInput?.addEventListener('change',() => {
const cols = clamp(parseInt(wallColsInput?.value || '0', 10) || wallState.cols, 2, 20);
ensureWallGridSize(wallState.rows, cols);
saveWallState();
syncWallInputs();
renderWall();
});
wallPatternSelect?.addEventListener('change', () => {
saveActivePatternState();
wallState.pattern = wallPatternSelect.value === 'x' ? 'x' : 'grid';
loadPatternState(patternKey());
ensureWallGridSize(wallState.rows, wallState.cols);
saveWallState();
renderWall();
syncWallInputs();
renderWallUsedPalette();
updateWallReplacePreview();
});
wallShowWireCb?.addEventListener('change', () => {
wallState.showWireframes = !!wallShowWireCb.checked;
saveActivePatternState();
saveWallState();
renderWall();
});
wallOutlineCb?.addEventListener('change', () => {
wallState.outline = !!wallOutlineCb.checked;
saveActivePatternState();
saveWallState();
renderWall();
});
wallPaintLinksBtn?.addEventListener('click', () => paintWallGroup('links'));
wallPaintSmallBtn?.addEventListener('click', () => paintWallGroup('small'));
wallPaintGapsBtn?.addEventListener('click', () => paintWallGroup('gaps'));
wallReplaceFromSel?.addEventListener('change', updateWallReplacePreview);
wallReplaceToSel?.addEventListener('change', updateWallReplacePreview);
wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from'));
wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to'));
wallToolPaintBtn?.addEventListener('click', () => setWallToolMode('paint'));
wallToolEraseBtn?.addEventListener('click', () => setWallToolMode('erase'));
const findWallNode = (el) => {
let cur = el;
while (cur && cur !== wallDisplay) {
if (cur.dataset?.wallKey) return cur;
cur = cur.parentNode;
}
return null;
};
const setHoverCursor = (e) => {
const hit = findWallNode(e.target);
wallDisplay.style.cursor = hit ? 'crosshair' : 'auto';
};
wallDisplay?.addEventListener('pointermove', setHoverCursor);
wallDisplay?.addEventListener('pointerleave', () => { wallDisplay.style.cursor = 'auto'; });
wallDisplay.addEventListener('click', (e) => {
const hit = findWallNode(e.target);
if (!hit) return;
const key = hit.dataset.wallKey;
if (!key) return;
const activeColor = getActiveWallColorIdx();
if (!Number.isInteger(activeColor)) return;
const rawStored = wallState.customColors?.[key];
const parsedStored = Number.isInteger(rawStored) ? rawStored : Number.parseInt(rawStored, 10);
const storedColor = Number.isInteger(parsedStored) && parsedStored >= 0 ? normalizeColorIdx(parsedStored) : null;
const hasStoredColor = Number.isInteger(storedColor) && storedColor >= 0;
if (e.altKey) {
if (Number.isInteger(storedColor)) {
setActiveColor(storedColor);
renderWallPalette();
renderWallUsedPalette();
}
return;
}
// Paint/erase based on tool mode; modifiers still erase.
const isEraseClick = wallToolMode === 'erase' || e.shiftKey || e.metaKey || e.ctrlKey;
wallState.customColors[key] = isEraseClick ? -1 : activeColor;
saveActivePatternState();
saveWallState();
renderWall();
});
wallClearBtn?.addEventListener('click', () => {
ensureWallGridSize(wallState.rows, wallState.cols);
wallState.colors = wallState.colors.map(row => row.map(() => -1));
wallState.customColors = {};
// Preserve outline/wireframe toggles; just clear colors.
wallState.showWireframes = wallState.showWireframes !== false;
wallState.outline = wallState.outline === true;
if (wallShowWireCb) wallShowWireCb.checked = wallState.showWireframes;
if (wallOutlineCb) wallOutlineCb.checked = wallState.outline;
saveActivePatternState();
saveWallState();
renderWall();
});
wallFillAllBtn?.addEventListener('click', () => {
if (window.organic?.getColor) setActiveColor(window.organic.getColor());
const idx = Number.isInteger(selectedColorIdx) ? selectedColorIdx : 0;
ensureWallGridSize(wallState.rows, wallState.cols);
wallState.colors = wallState.colors.map(row => row.map(() => idx));
const custom = {};
const rows = wallState.rows;
const cols = wallState.cols;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols - 1; c++) custom[`h-${r}-${c}`] = idx;
for (let c = 0; c < cols; c++) custom[`p-${r}-${c}`] = idx;
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols; c++) custom[`v-${r}-${c}`] = idx;
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 0; c < cols - 1; c++) {
custom[`c-${r}-${c}`] = idx;
['l1', 'l2', 'l3', 'l4'].forEach(l => custom[`${l}-${r}-${c}`] = idx);
custom[`g-${r}-${c}`] = idx; // gap center in grid
}
}
// For X pattern gaps between nodes (skip top row and far-left column)
if (wallState.pattern === 'x') {
const maxCH = Math.max(0, cols - 1);
for (let r = 1; r < rows - 1; r++) {
for (let c = 0; c < maxCH; c++) custom[`g-h-${r}-${c}`] = idx;
}
for (let r = 0; r < rows - 1; r++) {
for (let c = 1; c < maxCH; c++) custom[`g-v-${r}-${c}`] = idx;
}
for (let r = 1; r < rows - 1; r++) {
for (let c = 1; c < cols - 1; c++) custom[`f-x-${r}-${c}`] = idx;
}
}
wallState.customColors = custom;
saveActivePatternState();
saveWallState();
renderWall();
});
wallRemoveUnusedBtn?.addEventListener('click', () => {
ensureWallGridSize(wallState.rows, wallState.cols);
const beforeCustom = Object.keys(wallState.customColors || {}).length;
const filtered = {};
Object.entries(wallState.customColors || {}).forEach(([k, v]) => {
if (Number.isInteger(v) && v >= 0) filtered[k] = v;
});
wallState.customColors = filtered;
const afterCustom = Object.keys(filtered).length;
saveWallState();
renderWall();
if (wallReplaceMsg) {
const changed = (beforeCustom !== afterCustom);
wallReplaceMsg.textContent = changed ? 'Removed unused.' : 'Nothing to remove.';
}
saveActivePatternState();
});
wallReplaceBtn?.addEventListener('click', () => {
if (!wallReplaceFromSel || !wallReplaceToSel) return;
const fromIdx = normalizeColorIdx(Number.isInteger(wallReplaceFromIdx) ? wallReplaceFromIdx : parseInt(wallReplaceFromSel.value, 10));
const toIdx = normalizeColorIdx(Number.isInteger(wallReplaceToIdx) ? wallReplaceToIdx : parseInt(wallReplaceToSel.value, 10));
if (!Number.isInteger(fromIdx) || !Number.isInteger(toIdx) || fromIdx === toIdx) {
if (wallReplaceMsg) wallReplaceMsg.textContent = 'Choose two different colors.';
return;
}
const matches = updateWallReplacePreview();
if (!matches) {
if (wallReplaceMsg) wallReplaceMsg.textContent = 'No matches to replace.';
return;
}
if (matches > 120) {
const ok = window.confirm(`Replace ${matches} balloons? This cannot be undone except via undo/reload.`);
if (!ok) return;
}
ensureWallGridSize(wallState.rows, wallState.cols);
let replaced = 0;
wallState.colors = wallState.colors.map(row => row.map(v => {
const val = Number.isInteger(v) ? v : parseInt(v, 10);
if (val === fromIdx) { replaced++; return toIdx; }
return v;
}));
Object.keys(wallState.customColors || {}).forEach(k => {
const raw = wallState.customColors[k];
const val = Number.isInteger(raw) ? raw : parseInt(raw, 10);
if (val === fromIdx) { wallState.customColors[k] = toIdx; replaced++; }
});
// Keep pattern store in sync for this pattern
saveActivePatternState();
saveWallState();
renderWall();
renderWallUsedPalette();
updateWallReplacePreview();
if (wallReplaceMsg) wallReplaceMsg.textContent = replaced ? `Replaced ${replaced} item${replaced === 1 ? '' : 's'}.` : 'Nothing to replace.';
});
}
window.WallDesigner = {
init: initWallDesigner,
buildWallSvgPayload: buildWallSvgPayload
};
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('tab-wall') && !window.__wallInit) {
window.__wallInit = true;
initWallDesigner();
}
});
})();