Fix wall click painting and gap rendering
This commit is contained in:
parent
57423a1d88
commit
242a9f1ab0
144
wall.js
144
wall.js
@ -41,6 +41,8 @@
|
|||||||
const wallPaintLinksBtn = document.getElementById('wall-paint-links');
|
const wallPaintLinksBtn = document.getElementById('wall-paint-links');
|
||||||
const wallPaintSmallBtn = document.getElementById('wall-paint-small');
|
const wallPaintSmallBtn = document.getElementById('wall-paint-small');
|
||||||
const wallPaintGapsBtn = document.getElementById('wall-paint-gaps');
|
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 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];
|
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) {
|
async function buildWallSvgPayload(forExport = false, customColorsOverride = null) {
|
||||||
|
ensureFlatColors();
|
||||||
const customColors = customColorsOverride || wallState.customColors;
|
const customColors = customColorsOverride || wallState.customColors;
|
||||||
|
|
||||||
if (!ensureShared()) throw new Error('Wall designer shared helpers missing.');
|
if (!ensureShared()) throw new Error('Wall designer shared helpers missing.');
|
||||||
@ -323,7 +329,7 @@
|
|||||||
const shineRy = ry * 0.28;
|
const shineRy = ry * 0.28;
|
||||||
const stroke = shine.stroke ? `stroke="${shine.stroke}" stroke-width="1"` : '';
|
const stroke = shine.stroke ? `stroke="${shine.stroke}" stroke-width="1"` : '';
|
||||||
const shineFilter = shineShadow ? `filter="url(#${shineShadow})"` : '';
|
const shineFilter = shineShadow ? `filter="url(#${shineShadow})"` : '';
|
||||||
return `<ellipse cx="${sx_relative}" cy="${sy_relative}" rx="${shineRx}" ry="${shineRy}" fill="${shine.fill}" opacity="${shine.opacity ?? 1}" transform="rotate(${rot} ${sx_relative} ${sy_relative})"${stroke}${shineFilter} />`;
|
return `<ellipse cx="${sx_relative}" cy="${sy_relative}" rx="${shineRx}" ry="${shineRy}" fill="${shine.fill}" opacity="${shine.opacity ?? 1}" transform="rotate(${rot} ${sx_relative} ${sy_relative})" ${stroke}${shineFilter} />`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
@ -369,6 +375,7 @@
|
|||||||
const meta = wallColorMeta(customIdx);
|
const meta = wallColorMeta(customIdx);
|
||||||
const patId = ensurePattern(meta);
|
const patId = ensurePattern(meta);
|
||||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
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 stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||||
@ -420,9 +427,9 @@
|
|||||||
? override.idx
|
? override.idx
|
||||||
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
|
: (override.mode === 'empty' ? null : (showGaps ? autoGapColorIdx() : null));
|
||||||
const isEmpty = gapIdx === null;
|
const isEmpty = gapIdx === null;
|
||||||
|
const invisible = isEmpty && !showWireframes;
|
||||||
const meta = wallColorMeta(gapIdx);
|
const meta = wallColorMeta(gapIdx);
|
||||||
const patId = ensurePattern(meta);
|
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 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 stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||||
@ -453,6 +460,7 @@
|
|||||||
const meta = wallColorMeta(centerCustomIdx);
|
const meta = wallColorMeta(centerCustomIdx);
|
||||||
const patId = ensurePattern(meta);
|
const patId = ensurePattern(meta);
|
||||||
const fill = invisible ? 'rgba(0,0,0,0.001)' : (centerIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
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 stroke = invisible ? 'none' : (centerIsEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||||
const strokeW = invisible ? 0 : (centerIsEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
const strokeW = invisible ? 0 : (centerIsEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||||
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
|
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
|
||||||
@ -481,6 +489,7 @@
|
|||||||
const meta = wallColorMeta(linkCustomIdx);
|
const meta = wallColorMeta(linkCustomIdx);
|
||||||
const patId = ensurePattern(meta);
|
const patId = ensurePattern(meta);
|
||||||
const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
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.
|
// Always outline X-pattern link ovals; thicken when outline toggle is on.
|
||||||
const stroke = invisibleLink ? 'none' : (showOutline ? '#111827' : '#cbd5e1');
|
const stroke = invisibleLink ? 'none' : (showOutline ? '#111827' : '#cbd5e1');
|
||||||
const strokeW = invisibleLink ? 0 : (showOutline ? 0.8 : 0.6);
|
const strokeW = invisibleLink ? 0 : (showOutline ? 0.8 : 0.6);
|
||||||
@ -680,6 +689,7 @@
|
|||||||
function setActiveColor(idx) {
|
function setActiveColor(idx) {
|
||||||
selectedColorIdx = normalizeColorIdx(idx);
|
selectedColorIdx = normalizeColorIdx(idx);
|
||||||
wallState.activeColorIdx = selectedColorIdx;
|
wallState.activeColorIdx = selectedColorIdx;
|
||||||
|
updateWallActiveChip(selectedColorIdx);
|
||||||
if (window.organic?.setColor) {
|
if (window.organic?.setColor) {
|
||||||
window.organic.setColor(selectedColorIdx);
|
window.organic.setColor(selectedColorIdx);
|
||||||
} else if (window.organic?.updateCurrentColorChip) {
|
} else if (window.organic?.updateCurrentColorChip) {
|
||||||
@ -698,6 +708,7 @@
|
|||||||
if (wallState) wallState.activeColorIdx = normalized;
|
if (wallState) wallState.activeColorIdx = normalized;
|
||||||
saveWallState();
|
saveWallState();
|
||||||
renderWallPalette();
|
renderWallPalette();
|
||||||
|
updateWallActiveChip(normalized);
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@ -725,6 +736,25 @@
|
|||||||
return val;
|
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.
|
// Paint a specific group of nodes with the active color.
|
||||||
function paintWallGroup(group) {
|
function paintWallGroup(group) {
|
||||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||||
@ -795,62 +825,22 @@
|
|||||||
renderWall();
|
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() {
|
async function renderWall() {
|
||||||
if (!wallDisplay) return;
|
if (!wallDisplay) return;
|
||||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||||
if (wallGridLabel) wallGridLabel.textContent = `${wallState.cols} × ${wallState.rows}`;
|
if (wallGridLabel) wallGridLabel.textContent = `${wallState.cols} × ${wallState.rows}`;
|
||||||
try {
|
try {
|
||||||
|
console.info('[Wall] render start');
|
||||||
const { svgString } = await buildWallSvgPayload(false);
|
const { svgString } = await buildWallSvgPayload(false);
|
||||||
|
|
||||||
wallDisplay.innerHTML = svgString;
|
wallDisplay.innerHTML = svgString;
|
||||||
|
|
||||||
|
// Force a reflow to ensure the browser repaints the new SVG.
|
||||||
|
void wallDisplay.offsetWidth;
|
||||||
renderWallUsedPalette();
|
renderWallUsedPalette();
|
||||||
// paintDomFromState();
|
console.info('[Wall] render done');
|
||||||
} catch (err) {
|
} 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>`;
|
wallDisplay.innerHTML = `<div class="p-4 text-sm text-red-600">Could not render wall.</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -901,6 +891,7 @@
|
|||||||
wallPaletteEl.appendChild(row);
|
wallPaletteEl.appendChild(row);
|
||||||
});
|
});
|
||||||
renderWallUsedPalette();
|
renderWallUsedPalette();
|
||||||
|
updateWallActiveChip(getActiveWallColorIdx());
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncWallInputs() {
|
function syncWallInputs() {
|
||||||
@ -982,27 +973,6 @@
|
|||||||
return null;
|
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 setHoverCursor = (e) => {
|
||||||
const hit = findWallNode(e.target);
|
const hit = findWallNode(e.target);
|
||||||
wallDisplay.style.cursor = hit ? 'crosshair' : 'auto';
|
wallDisplay.style.cursor = hit ? 'crosshair' : 'auto';
|
||||||
@ -1010,6 +980,38 @@
|
|||||||
wallDisplay?.addEventListener('pointermove', setHoverCursor);
|
wallDisplay?.addEventListener('pointermove', setHoverCursor);
|
||||||
wallDisplay?.addEventListener('pointerleave', () => { wallDisplay.style.cursor = 'auto'; });
|
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', () => {
|
wallClearBtn?.addEventListener('click', () => {
|
||||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||||
wallState.colors = wallState.colors.map(row => row.map(() => -1));
|
wallState.colors = wallState.colors.map(row => row.map(() => -1));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user