From 7ba62b3d2b7706fb0b387ac16b687b681413228c Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 18 Dec 2025 11:53:02 -0500 Subject: [PATCH] Wall: unify paint toggle and consistent outlines; arch spacing tweaks --- classic.js | 38 ++++++++----- wall.js | 160 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 123 insertions(+), 75 deletions(-) diff --git a/classic.js b/classic.js index 11cafb5..9c32471 100644 --- a/classic.js +++ b/classic.js @@ -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) { diff --git a/wall.js b/wall.js index 74596e2..0da4a93 100644 --- a/wall.js +++ b/wall.js @@ -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(` + const displayIdx = isEmpty ? -1 : (customIdx ?? -1); + smallNodes.push(` ${shine} `); @@ -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(` + const displayIdx = isEmpty ? -1 : (customIdx ?? -1); + bigNodes.push(` ${shine} `); @@ -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(` + const displayIdx = isEmpty ? -1 : (customIdx ?? -1); + bigNodes.push(` ${shine} `); @@ -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(` + const displayIdx = isEmpty ? -1 : (gapIdx ?? -1); + bigNodes.push(` ${shineGap} `); @@ -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(` + const displayIdxCenter = centerCustomIdx ?? -1; + smallNodes.push(` ${shine} `); @@ -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(` + const displayIdxLink = linkIsEmpty ? -1 : (linkCustomIdx ?? -1); + bigNodes.push(` ${shine} `); @@ -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(` @@ -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(` + const displayIdx = isEmpty ? -1 : (gapIdx ?? -1); + bigNodes.push(` ${shineGap} `); @@ -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(` + const displayIdx = isEmpty ? -1 : (gapIdx ?? -1); + bigNodes.push(` ${shineGap} `); @@ -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();