exploded-classic #1

Merged
chris merged 15 commits from exploded-classic into main 2025-12-19 09:18:59 -05:00
2 changed files with 123 additions and 75 deletions
Showing only changes of commit 7ba62b3d2b - Show all commits

View File

@ -375,7 +375,8 @@
const base = shape.base || {};
const scale = cellScale(cell);
const expandedOn = model.manualMode && (model.explodedGapPx || 0) > 0;
const manualScale = expandedOn ? 1.35 : 1;
// Keep arch geometry consistent when expanded; only scale the alpha (wireframe) ring slightly to improve hit targets.
const manualScale = expandedOn && model.patternName?.toLowerCase().includes('arch') ? 1 : (expandedOn ? 1.15 : 1);
const transform = [(base.transform||''), `scale(${scale * manualScale})`].join(' ');
const isUnpainted = !colorInfo || explicitFill === 'none';
const wireframe = !!opts.wireframe || (model.manualMode && isUnpainted);
@ -457,13 +458,18 @@
const gap = model.explodedGapPx || 0;
const isArch = (model.patternName || '').toLowerCase().includes('arch');
if (isArch) {
// Move along the arch tangent to increase spacing without distorting the curve.
// Move outward along the radial vector and add a tangential nudge for even spread; push ends a bit more.
const dist = Math.hypot(xPx, yPx) || 1;
const tx = -yPx / dist;
const ty = xPx / dist;
const push = rowIndex * gap;
xPx += tx * push;
yPx += ty * push;
const maxRow = Math.max(1, (pattern.cellsPerRow * model.rowCount) - 1);
const t = Math.max(0, Math.min(1, y / maxRow)); // 0 first row, 1 last row
const radialPush = gap * (1.6 + Math.abs(t - 0.5) * 1.6); // ends > crown
const tangentialPush = (t - 0.5) * (gap * 0.8); // small along-arc spread
const nx = xPx / dist;
const ny = yPx / dist;
const tx = -ny;
const ty = nx;
xPx += nx * radialPush + tx * tangentialPush;
yPx += ny * radialPush + ty * tangentialPush;
} else {
yPx += rowIndex * gap; // columns: separate along the vertical path
}
@ -626,14 +632,14 @@ function distinctPaletteSlots(palette) {
if (isArch) {
// Radial slide outward; preserve layout.
const dist = Math.hypot(c.x, c.y) || 1;
const offset = 80;
const offset = (model.manualMode && (model.explodedGapPx || 0) > 0) ? 120 : 80;
const nx = c.x / dist, ny = c.y / dist;
slideX = nx * offset;
slideY = ny * offset;
// Slight tangent spread (~5px) to separate balloons without reshaping the quad.
const txDirX = -ny;
const txDirY = nx;
const fan = spread * 10;
const fan = spread * ((model.manualMode && (model.explodedGapPx || 0) > 0) ? 16 : 10);
slideX += txDirX * fan;
slideY += txDirY * fan;
}
@ -773,13 +779,14 @@ function distinctPaletteSlots(palette) {
const svgDefs = svg('defs', {}, patternsDefs);
const mainGroup = svg('g', null, kids);
const zoomPercent = classicZoom * 100;
m.render(container, svg('svg', {
xmlns: 'http://www.w3.org/2000/svg',
width:'100%',
height:'100%',
viewBox: vb,
preserveAspectRatio:'xMidYMid meet',
style: `isolation:isolate; transform:scale(${classicZoom}); transform-origin:center center;`
style: `isolation:isolate; width:${zoomPercent}%; height:${zoomPercent}%; min-width:${zoomPercent}%; min-height:${zoomPercent}%; transform-origin:center center;`
}, [svgDefs, mainGroup]));
}
@ -1496,6 +1503,8 @@ function distinctPaletteSlots(palette) {
const toolbarZoomOut = document.getElementById('classic-toolbar-zoomout');
const toolbarReset = document.getElementById('classic-toolbar-reset');
const focusLabelCanvas = document.getElementById('classic-focus-label-canvas');
const reverseLabel = reverseCb?.closest('label');
const reverseHint = reverseLabel?.parentElement?.querySelector('.hint');
const quadModal = document.getElementById('classic-quad-modal');
const quadModalClose = document.getElementById('classic-quad-modal-close');
const quadModalDisplay = document.getElementById('classic-quad-modal-display');
@ -1981,9 +1990,12 @@ function distinctPaletteSlots(palette) {
topperControls.classList.toggle('hidden', !showTopper);
if (numberTintRow) numberTintRow.classList.toggle('hidden', !(showTopper && isNumberTopper));
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
const showReverse = patternLayout === 'spiral' && !manualOn;
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
if (reverseHint) reverseHint.classList.toggle('hidden', !showReverse);
if (reverseCb) {
reverseCb.disabled = manualOn;
if (manualOn) reverseCb.checked = false;
reverseCb.disabled = manualOn || !showReverse;
if (!showReverse) reverseCb.checked = false;
}
GC.setTopperEnabled(showTopper);
@ -2002,7 +2014,7 @@ function distinctPaletteSlots(palette) {
const expandedOn = manualOn && manualExpandedState;
GC.setExplodedSettings({
scale: expandedOn ? 1.18 : 1,
gapPx: expandedOn ? 26 : 0,
gapPx: expandedOn ? 90 : 0,
staggerPx: expandedOn ? 6 : 0
});
if (display) {

160
wall.js
View File

@ -18,7 +18,6 @@
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');
@ -326,6 +325,18 @@
return { mode: 'auto' };
};
// Shared stroke helpers:
// - Outline only when filled AND outline is enabled.
// - Wireframe only when empty AND wireframes are enabled.
const strokeFor = (isEmpty, { outline = '#111827', wire = '#cbd5e1' } = {}) => {
if (isEmpty) return showWireframes ? wire : 'none';
return showOutline ? outline : 'none';
};
const strokeWidthFor = (isEmpty, { outline = 0.6, wire = 1.4 } = {}) => {
if (isEmpty) return showWireframes ? wire : 0;
return showOutline ? outline : 0;
};
// 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);
@ -351,13 +362,14 @@
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 fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty);
const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine}
</g>`);
@ -380,14 +392,14 @@
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 fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine}
</g>`);
@ -409,13 +421,14 @@
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 fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : strokeFor(isEmpty, { outline: '#111827', wire: '#cbd5e1' });
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine}
</g>`);
@ -438,13 +451,14 @@
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 fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
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(`<g data-wall-gap="1" data-wall-key="${gapKey}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${gapKey}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap}
</g>`);
@ -468,13 +482,13 @@
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 stroke = invisible ? 'none' : strokeFor(centerIsEmpty);
const strokeW = invisible ? 0 : strokeWidthFor(centerIsEmpty, { outline: 0.6, wire: 1.4 });
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
const shine = centerIsEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
const displayIdxCenter = centerCustomIdx ?? -1;
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" data-wall-color="${displayIdxCenter}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine}
</g>`);
@ -492,19 +506,21 @@
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 fill = linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex);
// Wireframe shows hit area when empty; outline shows only when filled and outline enabled.
const stroke = linkIsEmpty
? (showWireframes ? '#cbd5e1' : 'none')
: (showOutline ? '#111827' : 'none');
const strokeW = linkIsEmpty
? (showWireframes ? 1.0 : 0)
: (showOutline ? 0.9 : 0);
const filter = linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
const displayIdxLink = linkIsEmpty ? -1 : (linkCustomIdx ?? -1);
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" data-wall-color="${displayIdxLink}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine}
</g>`);
@ -527,9 +543,9 @@
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 fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? 'none' : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
const fillerStroke = fillerInvisible ? 'none' : strokeFor(fillerEmpty);
const fillerStrokeW = fillerInvisible ? 0 : strokeWidthFor(fillerEmpty, { outline: 0.6, wire: 1.2 });
const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`;
const fillerShine = fillerEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, fillerMeta.hex);
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${fillerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
@ -553,12 +569,13 @@
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 stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap}
</g>`);
@ -584,7 +601,8 @@
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap}
</g>`);
@ -847,6 +865,17 @@
return val;
}
// Resolve current color (custom override only).
function getCurrentColorIdxForKey(key) {
if (!wallState) wallState = wallDefaultState();
ensureWallGridSize(wallState.rows, wallState.cols);
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;
return normalizeColorIdx(parsed);
}
function updateWallActiveChip(idx) {
if (!wallActiveChip || !wallActiveLabel) return;
ensureFlatColors();
@ -866,19 +895,6 @@
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);
@ -1022,7 +1038,29 @@
else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor());
else selectedColorIdx = defaultActiveColorIdx();
setActiveColor(selectedColorIdx);
setWallToolMode('paint');
// Hide legacy paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
if (wallToolPaintBtn) {
wallToolPaintBtn.classList.add('hidden');
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
wallToolPaintBtn.tabIndex = -1;
}
if (wallToolEraseBtn) {
wallToolEraseBtn.classList.add('hidden');
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
wallToolEraseBtn.tabIndex = -1;
}
// Hide legacy paint/erase toggles; always use click-to-paint/click-again-to-clear.
if (wallToolPaintBtn) {
wallToolPaintBtn.classList.add('hidden');
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
wallToolPaintBtn.tabIndex = -1;
}
if (wallToolEraseBtn) {
wallToolEraseBtn.classList.add('hidden');
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
wallToolEraseBtn.tabIndex = -1;
}
// Allow picking active wall color by clicking the chip.
if (wallActiveChip && window.openColorPicker) {
wallActiveChip.style.cursor = 'pointer';
@ -1093,8 +1131,7 @@
wallReplaceToSel?.addEventListener('change', updateWallReplacePreview);
wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from'));
wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to'));
wallToolPaintBtn?.addEventListener('click', () => setWallToolMode('paint'));
wallToolEraseBtn?.addEventListener('click', () => setWallToolMode('erase'));
// Remove explicit paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
const findWallNode = (el) => {
let cur = el;
@ -1119,12 +1156,11 @@
const key = hit.dataset.wallKey;
if (!key) return;
const activeColor = getActiveWallColorIdx();
const activeColor = normalizeColorIdx(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;
const datasetColor = Number.parseInt(hit.dataset.wallColor ?? '', 10);
const currentColor = Number.isInteger(datasetColor) ? datasetColor : getCurrentColorIdxForKey(key);
const hasCurrent = Number.isInteger(currentColor) && currentColor >= 0;
if (e.altKey) {
if (Number.isInteger(storedColor)) {
@ -1135,9 +1171,9 @@
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;
// Simple toggle: click paints with active; clicking again with the same active clears it.
const sameAsActive = hasCurrent && currentColor === activeColor;
wallState.customColors[key] = sameAsActive ? -1 : activeColor;
saveActivePatternState();
saveWallState();