Wall: unify paint toggle and consistent outlines; arch spacing tweaks

This commit is contained in:
chris 2025-12-18 11:53:02 -05:00
parent 346f6ff917
commit 7ba62b3d2b
2 changed files with 123 additions and 75 deletions

View File

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

160
wall.js
View File

@ -18,7 +18,6 @@
let wallState = null; let wallState = null;
let selectedColorIdx = 0; // This should be synced with organic's selectedColorIdx let selectedColorIdx = 0; // This should be synced with organic's selectedColorIdx
let wallToolMode = 'paint';
// DOM elements // DOM elements
const wallDisplay = document.getElementById('wall-display'); const wallDisplay = document.getElementById('wall-display');
@ -326,6 +325,18 @@
return { mode: 'auto' }; 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) // Helper to create a shine ellipse with coordinates relative to (0,0)
const shineNodeRelative = (rx, ry, hex, rot = -20) => { const shineNodeRelative = (rx, ry, hex, rot = -20) => {
const shine = shineStyle(hex || WALL_FALLBACK_COLOR); const shine = shineStyle(hex || WALL_FALLBACK_COLOR);
@ -351,13 +362,14 @@
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 ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none')); const stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : (isEmpty ? (showWireframes ? 1.4 : 0) : (showOutline ? 0.6 : 0)); const strokeW = invisible ? 0 : strokeWidthFor(isEmpty);
const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`; const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex); 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" /> <circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine} ${shine}
</g>`); </g>`);
@ -380,14 +392,14 @@
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 ? 'none' : (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' : strokeFor(isEmpty);
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none')); const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
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})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex); 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" /> <ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine} ${shine}
</g>`); </g>`);
@ -409,13 +421,14 @@
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 ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none')); const stroke = invisible ? 'none' : strokeFor(isEmpty, { outline: '#111827', wire: '#cbd5e1' });
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0)); const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex); 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" /> <ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine} ${shine}
</g>`); </g>`);
@ -438,13 +451,14 @@
const invisible = isEmpty; const invisible = isEmpty;
const meta = wallColorMeta(gapIdx); const meta = wallColorMeta(gapIdx);
const patId = ensurePattern(meta); const patId = ensurePattern(meta);
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex); const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none'); const stroke = invisible ? 'none' : strokeFor(isEmpty);
const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0); const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex); 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" /> <circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap} ${shineGap}
</g>`); </g>`);
@ -468,13 +482,13 @@
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' : strokeFor(centerIsEmpty);
const stroke = invisible ? 'none' : (centerIsEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none')); const strokeW = invisible ? 0 : strokeWidthFor(centerIsEmpty, { outline: 0.6, wire: 1.4 });
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})"`;
const shine = centerIsEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex); 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" /> <circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine} ${shine}
</g>`); </g>`);
@ -492,19 +506,21 @@
const linkCustomIdx = linkOverride.mode === 'color' ? linkOverride.idx : null; const linkCustomIdx = linkOverride.mode === 'color' ? linkOverride.idx : null;
const linkIsEmpty = linkOverride.mode === 'empty' || linkCustomIdx === null; const linkIsEmpty = linkOverride.mode === 'empty' || linkCustomIdx === null;
const invisibleLink = linkIsEmpty && !showWireframes;
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 = linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex);
console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta); // Wireframe shows hit area when empty; outline shows only when filled and outline enabled.
// Outline only when filled; light wireframe when empty and wireframes shown. const stroke = linkIsEmpty
const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none')); ? (showWireframes ? '#cbd5e1' : 'none')
const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0)); : (showOutline ? '#111827' : 'none');
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`; 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); 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" /> <ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shine} ${shine}
</g>`); </g>`);
@ -527,9 +543,9 @@
const fillerInvisible = fillerEmpty && !showWireframes; const fillerInvisible = fillerEmpty && !showWireframes;
const fillerMeta = wallColorMeta(fillerIdx); const fillerMeta = wallColorMeta(fillerIdx);
const fillerPat = ensurePattern(fillerMeta); 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 fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? 'none' : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
const fillerStroke = fillerInvisible ? 'none' : (fillerEmpty ? (showWireframes ? '#cbd5e1' : 'none') : 'none'); const fillerStroke = fillerInvisible ? 'none' : strokeFor(fillerEmpty);
const fillerStrokeW = fillerInvisible ? 0 : (fillerEmpty ? (showWireframes ? 1.2 : 0) : 0); const fillerStrokeW = fillerInvisible ? 0 : strokeWidthFor(fillerEmpty, { outline: 0.6, wire: 1.2 });
const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`; const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`;
const fillerShine = fillerEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, fillerMeta.hex); 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})"> 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 patId = ensurePattern(meta);
const invisible = isEmpty && !showGaps; 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' : strokeFor(isEmpty);
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0)); const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex); 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" /> <circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap} ${shineGap}
</g>`); </g>`);
@ -584,7 +601,8 @@
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`; const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
const rGap = bigR * 0.82; const rGap = bigR * 0.82;
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex); 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" /> <circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
${shineGap} ${shineGap}
</g>`); </g>`);
@ -847,6 +865,17 @@
return val; 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) { function updateWallActiveChip(idx) {
if (!wallActiveChip || !wallActiveLabel) return; if (!wallActiveChip || !wallActiveLabel) return;
ensureFlatColors(); ensureFlatColors();
@ -866,19 +895,6 @@
wallActiveLabel.textContent = meta.name || meta.hex || ''; 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. // 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);
@ -1022,7 +1038,29 @@
else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor()); else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor());
else selectedColorIdx = defaultActiveColorIdx(); else selectedColorIdx = defaultActiveColorIdx();
setActiveColor(selectedColorIdx); 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. // Allow picking active wall color by clicking the chip.
if (wallActiveChip && window.openColorPicker) { if (wallActiveChip && window.openColorPicker) {
wallActiveChip.style.cursor = 'pointer'; wallActiveChip.style.cursor = 'pointer';
@ -1093,8 +1131,7 @@
wallReplaceToSel?.addEventListener('change', updateWallReplacePreview); wallReplaceToSel?.addEventListener('change', updateWallReplacePreview);
wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from')); wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from'));
wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to')); wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to'));
wallToolPaintBtn?.addEventListener('click', () => setWallToolMode('paint')); // Remove explicit paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
wallToolEraseBtn?.addEventListener('click', () => setWallToolMode('erase'));
const findWallNode = (el) => { const findWallNode = (el) => {
let cur = el; let cur = el;
@ -1119,12 +1156,11 @@
const key = hit.dataset.wallKey; const key = hit.dataset.wallKey;
if (!key) return; if (!key) return;
const activeColor = getActiveWallColorIdx(); const activeColor = normalizeColorIdx(getActiveWallColorIdx());
if (!Number.isInteger(activeColor)) return; if (!Number.isInteger(activeColor)) return;
const rawStored = wallState.customColors?.[key]; const datasetColor = Number.parseInt(hit.dataset.wallColor ?? '', 10);
const parsedStored = Number.isInteger(rawStored) ? rawStored : Number.parseInt(rawStored, 10); const currentColor = Number.isInteger(datasetColor) ? datasetColor : getCurrentColorIdxForKey(key);
const storedColor = Number.isInteger(parsedStored) && parsedStored >= 0 ? normalizeColorIdx(parsedStored) : null; const hasCurrent = Number.isInteger(currentColor) && currentColor >= 0;
const hasStoredColor = Number.isInteger(storedColor) && storedColor >= 0;
if (e.altKey) { if (e.altKey) {
if (Number.isInteger(storedColor)) { if (Number.isInteger(storedColor)) {
@ -1135,9 +1171,9 @@
return; return;
} }
// Paint/erase based on tool mode; modifiers still erase. // Simple toggle: click paints with active; clicking again with the same active clears it.
const isEraseClick = wallToolMode === 'erase' || e.shiftKey || e.metaKey || e.ctrlKey; const sameAsActive = hasCurrent && currentColor === activeColor;
wallState.customColors[key] = isEraseClick ? -1 : activeColor; wallState.customColors[key] = sameAsActive ? -1 : activeColor;
saveActivePatternState(); saveActivePatternState();
saveWallState(); saveWallState();