Fix wall click painting and gap rendering

This commit is contained in:
chris 2025-12-04 11:01:49 -05:00
parent 57423a1d88
commit 242a9f1ab0

142
wall.js
View File

@ -41,6 +41,8 @@
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');
@ -182,9 +184,13 @@
else if ((type === 'c' || type.startsWith('l') || type === 'f') && (rVal >= r - 1 || cVal >= c - 1)) delete wallState.customColors[k];
});
}
const wallColorMeta = (idx) => (Number.isInteger(idx) && idx >= 0 && FLAT_COLORS[idx]) ? FLAT_COLORS[idx] : { hex: WALL_FALLBACK_COLOR };
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.');
@ -369,6 +375,7 @@
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})"`;
@ -420,9 +427,9 @@
? override.idx
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
const isEmpty = gapIdx === null;
const invisible = isEmpty && !showWireframes;
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));
@ -453,6 +460,7 @@
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})"`;
@ -481,6 +489,7 @@
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);
// Always outline X-pattern link ovals; thicken when outline toggle is on.
const stroke = invisibleLink ? 'none' : (showOutline ? '#111827' : '#cbd5e1');
const strokeW = invisibleLink ? 0 : (showOutline ? 0.8 : 0.6);
@ -680,6 +689,7 @@
function setActiveColor(idx) {
selectedColorIdx = normalizeColorIdx(idx);
wallState.activeColorIdx = selectedColorIdx;
updateWallActiveChip(selectedColorIdx);
if (window.organic?.setColor) {
window.organic.setColor(selectedColorIdx);
} else if (window.organic?.updateCurrentColorChip) {
@ -698,6 +708,7 @@
if (wallState) wallState.activeColorIdx = normalized;
saveWallState();
renderWallPalette();
updateWallActiveChip(normalized);
}
return normalized;
}
@ -725,6 +736,25 @@
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 || '';
}
// Paint a specific group of nodes with the active color.
function paintWallGroup(group) {
ensureWallGridSize(wallState.rows, wallState.cols);
@ -795,62 +825,22 @@
renderWall();
}
// Apply an immediate visual change to the clicked element so users see feedback even before a full re-render.
function applyImmediateFill(el, colorIdx) {
if (!el) return;
const shape = el.querySelector('circle,ellipse');
if (!shape) return;
const isEmpty = !Number.isInteger(colorIdx);
const meta = wallColorMeta(isEmpty ? null : colorIdx);
const emptyHitFill = wallState.showWireframes ? 'none' : 'rgba(0,0,0,0.001)';
const fill = isEmpty ? emptyHitFill : (meta.hex || WALL_FALLBACK_COLOR);
const stroke = isEmpty
? (wallState.showWireframes ? '#cbd5e1' : 'none')
: (wallState.outline ? '#111827' : 'none');
const strokeW = isEmpty
? (wallState.showWireframes ? 1.2 : 0)
: (wallState.outline ? 0.6 : 0);
shape.setAttribute('fill', fill);
shape.setAttribute('stroke', stroke);
shape.setAttribute('stroke-width', strokeW);
}
// After rendering the SVG, enforce fill/stroke based on current state to avoid any template mismatch.
function paintDomFromState() {
if (!wallDisplay) return;
const nodes = wallDisplay.querySelectorAll('[data-wall-key]');
nodes.forEach(node => {
const key = node.dataset?.wallKey;
const idx = getStoredColorForKey(key);
const shape = node.querySelector('circle,ellipse');
if (!shape) return;
const isEmpty = !Number.isInteger(idx);
const meta = wallColorMeta(isEmpty ? null : idx);
const emptyHitFill = wallState.showWireframes ? 'none' : 'rgba(0,0,0,0.001)';
const fill = isEmpty ? emptyHitFill : (meta.hex || WALL_FALLBACK_COLOR);
const stroke = isEmpty
? (wallState.showWireframes ? '#cbd5e1' : 'none')
: (wallState.outline ? '#111827' : 'none');
const strokeW = isEmpty
? (wallState.showWireframes ? 1.2 : 0)
: (wallState.outline ? 0.6 : 0);
shape.setAttribute('fill', fill);
shape.setAttribute('stroke', stroke);
shape.setAttribute('stroke-width', strokeW);
});
}
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();
// paintDomFromState();
console.info('[Wall] render done');
} catch (err) {
console.error('[Wall] render failed', err);
console.error('[Wall] render failed', err?.stack || err);
wallDisplay.innerHTML = `<div class="p-4 text-sm text-red-600">Could not render wall.</div>`;
}
}
@ -901,6 +891,7 @@
wallPaletteEl.appendChild(row);
});
renderWallUsedPalette();
updateWallActiveChip(getActiveWallColorIdx());
}
function syncWallInputs() {
@ -982,27 +973,6 @@
return null;
};
wallDisplay?.addEventListener('click', (e) => {
const hit = findWallNode(e.target);
const key = hit?.dataset?.wallKey;
if (!key) return;
const activeColorIdx = getActiveWallColorIdx();
// Blindly set the color. This removes the toggle logic for diagnostics.
const newCustomColors = { ...wallState.customColors, [key]: activeColorIdx };
// Update the global state.
wallState.customColors = newCustomColors;
// Persist the new state.
saveWallState();
saveActivePatternState();
// Explicitly pass the new state to the render function.
renderWall(newCustomColors);
});
const setHoverCursor = (e) => {
const hit = findWallNode(e.target);
wallDisplay.style.cursor = hit ? 'crosshair' : 'auto';
@ -1010,6 +980,38 @@
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;
}
// Only erase when a modifier is held (shift/ctrl/cmd). A plain click always paints.
const isEraseClick = 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));