Refine classic manual tools and corinthian controls
645
classic.js
@ -86,6 +86,8 @@
|
|||||||
const MANUAL_MODE_KEY = 'classic:manualMode:v1';
|
const MANUAL_MODE_KEY = 'classic:manualMode:v1';
|
||||||
const MANUAL_OVERRIDES_KEY = 'classic:manualOverrides:v1';
|
const MANUAL_OVERRIDES_KEY = 'classic:manualOverrides:v1';
|
||||||
const MANUAL_EXPANDED_KEY = 'classic:manualExpanded:v1';
|
const MANUAL_EXPANDED_KEY = 'classic:manualExpanded:v1';
|
||||||
|
const MANUAL_ROW_OFFSETS_KEY = 'classic:manualRowOffsets:v1';
|
||||||
|
const MANUAL_ROW_SCALES_KEY = 'classic:manualRowScales:v1';
|
||||||
const NUMBER_IMAGE_MAP = {
|
const NUMBER_IMAGE_MAP = {
|
||||||
'0': 'output_webp/0.webp',
|
'0': 'output_webp/0.webp',
|
||||||
'1': 'output_webp/1.webp',
|
'1': 'output_webp/1.webp',
|
||||||
@ -183,13 +185,35 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
function loadManualRowOffsets() {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(MANUAL_ROW_OFFSETS_KEY));
|
||||||
|
if (saved && typeof saved === 'object') return saved;
|
||||||
|
} catch {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
function loadManualRowScales() {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(MANUAL_ROW_SCALES_KEY));
|
||||||
|
if (saved && typeof saved === 'object') return saved;
|
||||||
|
} catch {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
function saveManualOverrides(map) {
|
function saveManualOverrides(map) {
|
||||||
try { localStorage.setItem(MANUAL_OVERRIDES_KEY, JSON.stringify(map || {})); } catch {}
|
try { localStorage.setItem(MANUAL_OVERRIDES_KEY, JSON.stringify(map || {})); } catch {}
|
||||||
}
|
}
|
||||||
|
function saveManualRowOffsets(map) {
|
||||||
|
try { localStorage.setItem(MANUAL_ROW_OFFSETS_KEY, JSON.stringify(map || {})); } catch {}
|
||||||
|
}
|
||||||
|
function saveManualRowScales(map) {
|
||||||
|
try { localStorage.setItem(MANUAL_ROW_SCALES_KEY, JSON.stringify(map || {})); } catch {}
|
||||||
|
}
|
||||||
function manualKey(patternName, rowCount) {
|
function manualKey(patternName, rowCount) {
|
||||||
return `${patternName || ''}::${rowCount || 0}`;
|
return `${patternName || ''}::${rowCount || 0}`;
|
||||||
}
|
}
|
||||||
const manualOverrides = loadManualOverrides();
|
const manualOverrides = loadManualOverrides();
|
||||||
|
const manualRowOffsets = loadManualRowOffsets();
|
||||||
|
const manualRowScales = loadManualRowScales();
|
||||||
function manualOverrideCount(patternName, rowCount) {
|
function manualOverrideCount(patternName, rowCount) {
|
||||||
const key = manualKey(patternName, rowCount);
|
const key = manualKey(patternName, rowCount);
|
||||||
const entry = manualOverrides[key];
|
const entry = manualOverrides[key];
|
||||||
@ -239,6 +263,18 @@
|
|||||||
});
|
});
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
function getManualRowOffsets(patternName, rowCount) {
|
||||||
|
const key = manualKey(patternName, rowCount);
|
||||||
|
const entry = manualRowOffsets[key];
|
||||||
|
if (!entry || typeof entry !== 'object') return {};
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function getManualRowScales(patternName, rowCount) {
|
||||||
|
const key = manualKey(patternName, rowCount);
|
||||||
|
const entry = manualRowScales[key];
|
||||||
|
if (!entry || typeof entry !== 'object') return {};
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
// Manual palette (used in Manual mode project palette)
|
// Manual palette (used in Manual mode project palette)
|
||||||
let projectPaletteBox = null;
|
let projectPaletteBox = null;
|
||||||
let renderProjectPalette = () => {};
|
let renderProjectPalette = () => {};
|
||||||
@ -294,6 +330,7 @@
|
|||||||
|
|
||||||
let pxUnit = 10;
|
let pxUnit = 10;
|
||||||
let clusters = 10;
|
let clusters = 10;
|
||||||
|
let lengthFt = null;
|
||||||
let reverse = false;
|
let reverse = false;
|
||||||
let topperEnabled = false;
|
let topperEnabled = false;
|
||||||
let topperType = 'round';
|
let topperType = 'round';
|
||||||
@ -317,6 +354,10 @@
|
|||||||
initialPattern: 'Arch 4',
|
initialPattern: 'Arch 4',
|
||||||
controller: (el) => makeController(el),
|
controller: (el) => makeController(el),
|
||||||
setClusters(n) { clusters = Math.max(1, (Number(n)|0) || 10); },
|
setClusters(n) { clusters = Math.max(1, (Number(n)|0) || 10); },
|
||||||
|
setLengthFt(n) {
|
||||||
|
const next = Number(n);
|
||||||
|
lengthFt = Number.isFinite(next) ? next : null;
|
||||||
|
},
|
||||||
setReverse(on){ reverse = !!on; },
|
setReverse(on){ reverse = !!on; },
|
||||||
setTopperEnabled(on) { topperEnabled = !!on; },
|
setTopperEnabled(on) { topperEnabled = !!on; },
|
||||||
setTopperType(type) { topperType = type || 'round'; },
|
setTopperType(type) { topperType = type || 'round'; },
|
||||||
@ -451,6 +492,35 @@
|
|||||||
const rel = (pattern.baseBalloonSize && base.baseBalloonSize) ? pattern.baseBalloonSize/base.baseBalloonSize : 1;
|
const rel = (pattern.baseBalloonSize && base.baseBalloonSize) ? pattern.baseBalloonSize/base.baseBalloonSize : 1;
|
||||||
let p = { x: pattern.gridX(model.pattern.cellsPerRow > 1 ? y : x, x), y: pattern.gridY(y,x) };
|
let p = { x: pattern.gridX(model.pattern.cellsPerRow > 1 ? y : x, x), y: pattern.gridY(y,x) };
|
||||||
if (pattern.transform) p = pattern.transform(p,x,y,model);
|
if (pattern.transform) p = pattern.transform(p,x,y,model);
|
||||||
|
if (pattern.sizeProfile === 'corinthian') {
|
||||||
|
const totalRows = pattern.cellsPerRow * model.rowCount;
|
||||||
|
const sizeScale = corinthianScale(y, totalRows, pattern.baseBalloonSize, pattern.sizeProfileOpts);
|
||||||
|
const axisScale = sizeScale * corinthianAxisScale(y, totalRows, pattern.sizeProfileOpts);
|
||||||
|
const rowScale = corinthianRowScale(sizeScale, pattern.sizeProfileOpts);
|
||||||
|
const cols = pattern.cellsPerColumn || 1;
|
||||||
|
let center = 0;
|
||||||
|
if (cols > 1) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
sum += pattern.gridX(model.pattern.cellsPerRow > 1 ? y : i, i);
|
||||||
|
}
|
||||||
|
center = sum / cols;
|
||||||
|
}
|
||||||
|
p = { x: center + (p.x - center) * axisScale, y: corinthianRowY(y, pattern, model, pattern.sizeProfileOpts) * rowScale };
|
||||||
|
}
|
||||||
|
if (model.manualMode && model.manualRowScales && Number.isFinite(model.manualRowScales[y])) {
|
||||||
|
const isColumnPattern = (model.patternName || '').toLowerCase().includes('column');
|
||||||
|
const cols = pattern.cellsPerColumn || 1;
|
||||||
|
if (isColumnPattern && cols > 1) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
sum += pattern.gridX(model.pattern.cellsPerRow > 1 ? y : i, i);
|
||||||
|
}
|
||||||
|
const center = sum / cols;
|
||||||
|
const tighten = model.manualRowScales[y];
|
||||||
|
p = { x: center + (p.x - center) * tighten, y: p.y };
|
||||||
|
}
|
||||||
|
}
|
||||||
let xPx = p.x * rel * pxUnit;
|
let xPx = p.x * rel * pxUnit;
|
||||||
let yPx = p.y * rel * pxUnit;
|
let yPx = p.y * rel * pxUnit;
|
||||||
if (model.manualMode && (model.explodedGapPx || 0) > 0) {
|
if (model.manualMode && (model.explodedGapPx || 0) > 0) {
|
||||||
@ -474,6 +544,24 @@
|
|||||||
yPx += rowIndex * gap; // columns: separate along the vertical path
|
yPx += rowIndex * gap; // columns: separate along the vertical path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pattern.sizeProfile === 'corinthian') {
|
||||||
|
const perRow = model.corinthianRowOffsets;
|
||||||
|
if (perRow && Number.isFinite(perRow[y])) {
|
||||||
|
yPx += perRow[y];
|
||||||
|
} else if (pattern.sizeProfileOpts?.rowOffsetBySize) {
|
||||||
|
const baseInches = Number.isFinite(pattern.sizeProfileOpts.baseInches) ? pattern.sizeProfileOpts.baseInches : 11;
|
||||||
|
const totalRows = pattern.cellsPerRow * model.rowCount;
|
||||||
|
const scale = corinthianScale(y, totalRows, pattern.baseBalloonSize, pattern.sizeProfileOpts);
|
||||||
|
const inches = Math.round(scale * baseInches);
|
||||||
|
const offset = pattern.sizeProfileOpts.rowOffsetBySize[inches];
|
||||||
|
if (Number.isFinite(offset)) yPx += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isColumnPattern = (model.patternName || '').toLowerCase().includes('column');
|
||||||
|
const rowOffsets = model.manualRowOffsets || null;
|
||||||
|
if (isColumnPattern && rowOffsets && Number.isFinite(rowOffsets[y])) {
|
||||||
|
yPx += rowOffsets[y];
|
||||||
|
}
|
||||||
return { x: xPx, y: yPx };
|
return { x: xPx, y: yPx };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +578,192 @@ function distinctPaletteSlots(palette) {
|
|||||||
return out.length ? out : [1,2,3,4,5];
|
return out.length ? out : [1,2,3,4,5];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function corinthianScale(rowIndex, totalRows, baseSize, opts = {}) {
|
||||||
|
const base = Number.isFinite(baseSize) && baseSize > 0 ? baseSize : 25;
|
||||||
|
const baseInches = Number.isFinite(opts.baseInches) ? opts.baseInches : 11;
|
||||||
|
const sequence = Array.isArray(opts.sizeSequence) ? opts.sizeSequence : null;
|
||||||
|
if (sequence && sequence.length) {
|
||||||
|
if (totalRows <= 1) return sequence[0] / baseInches;
|
||||||
|
const t = rowIndex / (totalRows - 1);
|
||||||
|
const pos = t * (sequence.length - 1);
|
||||||
|
const mode = opts.sequenceMode || 'lerp';
|
||||||
|
let inches;
|
||||||
|
if (mode === 'profile') {
|
||||||
|
const top = Array.isArray(opts.profileTop) ? opts.profileTop : [];
|
||||||
|
const bottom = Array.isArray(opts.profileBottom) ? opts.profileBottom : [];
|
||||||
|
const mid = Number.isFinite(opts.profileMid) ? opts.profileMid : (sequence[Math.floor(sequence.length / 2)] || baseInches);
|
||||||
|
if (totalRows >= (top.length + bottom.length + 1)) {
|
||||||
|
if (rowIndex < top.length) inches = top[rowIndex];
|
||||||
|
else if (rowIndex >= totalRows - bottom.length) inches = bottom[rowIndex - (totalRows - bottom.length)];
|
||||||
|
else inches = mid;
|
||||||
|
} else {
|
||||||
|
const idx = Math.floor(pos);
|
||||||
|
inches = sequence[Math.max(0, Math.min(sequence.length - 1, idx))];
|
||||||
|
}
|
||||||
|
} else if (mode === 'step') {
|
||||||
|
const idx = Math.round(pos);
|
||||||
|
inches = sequence[Math.max(0, Math.min(sequence.length - 1, idx))];
|
||||||
|
} else {
|
||||||
|
const idx = Math.floor(pos);
|
||||||
|
const frac = pos - idx;
|
||||||
|
const a = sequence[Math.max(0, Math.min(sequence.length - 1, idx))];
|
||||||
|
const b = sequence[Math.max(0, Math.min(sequence.length - 1, idx + 1))];
|
||||||
|
inches = a + (b - a) * frac;
|
||||||
|
}
|
||||||
|
return inches / baseInches;
|
||||||
|
}
|
||||||
|
const edge = Number.isFinite(opts.edge) ? opts.edge
|
||||||
|
: (Number.isFinite(opts.edgeInches) ? (opts.edgeInches / baseInches) : 1.2);
|
||||||
|
const mid = Number.isFinite(opts.mid) ? opts.mid
|
||||||
|
: (Number.isFinite(opts.midInches) ? (opts.midInches / baseInches) : 0.85);
|
||||||
|
if (totalRows <= 1) return edge;
|
||||||
|
const t = rowIndex / (totalRows - 1);
|
||||||
|
const edgeWeight = (Math.cos(2 * Math.PI * t) + 1) / 2;
|
||||||
|
return mid + (edge - mid) * edgeWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function corinthianAxisScale(rowIndex, totalRows, opts = {}) {
|
||||||
|
const tighten = Number.isFinite(opts.axisTighten) ? opts.axisTighten : 0.3;
|
||||||
|
if (totalRows <= 1) return 1;
|
||||||
|
const t = rowIndex / (totalRows - 1);
|
||||||
|
const edgeWeight = (Math.cos(2 * Math.PI * t) + 1) / 2;
|
||||||
|
const scale = 1 - tighten * (1 - edgeWeight);
|
||||||
|
return Math.max(0.2, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
function corinthianRowScale(sizeScale, opts = {}) {
|
||||||
|
const tighten = Number.isFinite(opts.verticalTighten) ? opts.verticalTighten : 0.15;
|
||||||
|
return sizeScale * (1 - tighten * (1 - sizeScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
function corinthianRowY(rowIndex, pattern, model, opts = {}) {
|
||||||
|
if (!model._corinthianYCache) model._corinthianYCache = { y: [], scale: [] };
|
||||||
|
const cache = model._corinthianYCache;
|
||||||
|
if (Number.isFinite(cache.y[rowIndex])) return cache.y[rowIndex];
|
||||||
|
const totalRows = pattern.cellsPerRow * model.rowCount;
|
||||||
|
const spacingPower = Number.isFinite(opts.rowSpacingPower) ? opts.rowSpacingPower : 0.7;
|
||||||
|
const minRowScale = Number.isFinite(opts.minRowScale) ? opts.minRowScale : 0.55;
|
||||||
|
for (let y = cache.y.length; y <= rowIndex; y++) {
|
||||||
|
const baseY = pattern.gridY(y, 0);
|
||||||
|
const scale = corinthianScale(y, totalRows, pattern.baseBalloonSize, opts);
|
||||||
|
if (y === 0) {
|
||||||
|
cache.y[y] = baseY;
|
||||||
|
cache.scale[y] = scale;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const prevBaseY = pattern.gridY(y - 1, 0);
|
||||||
|
const delta = baseY - prevBaseY;
|
||||||
|
const prevScale = cache.scale[y - 1] ?? scale;
|
||||||
|
const avgScale = (prevScale + scale) / 2;
|
||||||
|
const easedScale = Math.max(minRowScale, Math.pow(avgScale, spacingPower));
|
||||||
|
cache.y[y] = cache.y[y - 1] + (delta * easedScale);
|
||||||
|
cache.scale[y] = scale;
|
||||||
|
}
|
||||||
|
return cache.y[rowIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resampleRowOffsets(baseMap, baseCount, rowCount) {
|
||||||
|
if (!baseMap || typeof baseMap !== 'object') return null;
|
||||||
|
if (rowCount === baseCount) return baseMap;
|
||||||
|
const maxBase = Math.max(1, baseCount - 1);
|
||||||
|
const maxRow = Math.max(1, rowCount - 1);
|
||||||
|
const baseOffsets = new Array(baseCount).fill(0);
|
||||||
|
let last = Number(baseMap[0]) || 0;
|
||||||
|
baseOffsets[0] = last;
|
||||||
|
for (let i = 1; i < baseCount; i++) {
|
||||||
|
const val = Number(baseMap[i]);
|
||||||
|
if (Number.isFinite(val)) last = val;
|
||||||
|
baseOffsets[i] = last;
|
||||||
|
}
|
||||||
|
const baseDeltas = new Array(maxBase).fill(0);
|
||||||
|
for (let i = 0; i < maxBase; i++) {
|
||||||
|
baseDeltas[i] = baseOffsets[i + 1] - baseOffsets[i];
|
||||||
|
}
|
||||||
|
const deltas = new Array(maxRow).fill(0);
|
||||||
|
for (let i = 0; i < maxRow; i++) {
|
||||||
|
const pos = (i / maxRow) * maxBase;
|
||||||
|
const lo = Math.floor(pos);
|
||||||
|
const hi = Math.min(maxBase - 1, Math.ceil(pos));
|
||||||
|
const a = baseDeltas[lo] || 0;
|
||||||
|
const b = baseDeltas[hi] || 0;
|
||||||
|
const t = hi === lo ? 0 : (pos - lo) / (hi - lo);
|
||||||
|
deltas[i] = a + (b - a) * t;
|
||||||
|
}
|
||||||
|
const out = {};
|
||||||
|
out[0] = Math.round(baseOffsets[0]);
|
||||||
|
for (let i = 1; i < rowCount; i++) {
|
||||||
|
out[i] = Math.round(out[i - 1] + deltas[i - 1]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blendRowOffsets(baseA, countA, baseB, countB, rowCount, t) {
|
||||||
|
const resampledA = resampleRowOffsets(baseA, countA, rowCount);
|
||||||
|
const resampledB = resampleRowOffsets(baseB, countB, rowCount);
|
||||||
|
if (!resampledA && !resampledB) return null;
|
||||||
|
if (!resampledB) return resampledA;
|
||||||
|
if (!resampledA) return resampledB;
|
||||||
|
const out = {};
|
||||||
|
for (let row = 0; row < rowCount; row++) {
|
||||||
|
const a = Number(resampledA[row]) || 0;
|
||||||
|
const b = Number(resampledB[row]) || 0;
|
||||||
|
out[row] = Math.round(a + (b - a) * t);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRowOffsetMaps(patternName) {
|
||||||
|
const out = {};
|
||||||
|
Object.entries(manualRowOffsets || {}).forEach(([key, map]) => {
|
||||||
|
if (!map || typeof map !== 'object') return;
|
||||||
|
const [name, rowsStr] = String(key).split('::');
|
||||||
|
const rows = parseInt(rowsStr, 10);
|
||||||
|
if (!Number.isFinite(rows)) return;
|
||||||
|
if (name === patternName) out[rows] = map;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseRowOffsets(mapSet, rowCount, lengthFt) {
|
||||||
|
if (!mapSet || typeof mapSet !== 'object') return null;
|
||||||
|
const keys = Object.keys(mapSet).map(n => parseInt(n, 10)).filter(n => Number.isFinite(n)).sort((a, b) => a - b);
|
||||||
|
if (!keys.length || !Number.isFinite(rowCount) || rowCount <= 0) return null;
|
||||||
|
if (Number.isFinite(lengthFt)) {
|
||||||
|
const lowFt = Math.floor(lengthFt);
|
||||||
|
const highFt = Math.ceil(lengthFt);
|
||||||
|
const lowCount = lowFt * 2;
|
||||||
|
const highCount = highFt * 2;
|
||||||
|
if (lowCount === highCount) {
|
||||||
|
if (mapSet[lowCount]) return resampleRowOffsets(mapSet[lowCount], lowCount, rowCount);
|
||||||
|
} else if (mapSet[lowCount] && mapSet[highCount]) {
|
||||||
|
const tRaw = (lengthFt - lowFt) / (highFt - lowFt);
|
||||||
|
const t = Math.abs(tRaw - 0.5) < 0.001 ? 0.5 : tRaw;
|
||||||
|
return blendRowOffsets(mapSet[lowCount], lowCount, mapSet[highCount], highCount, rowCount, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mapSet[rowCount]) return mapSet[rowCount];
|
||||||
|
if (rowCount <= keys[0]) return resampleRowOffsets(mapSet[keys[0]], keys[0], rowCount);
|
||||||
|
if (rowCount >= keys[keys.length - 1]) return resampleRowOffsets(mapSet[keys[keys.length - 1]], keys[keys.length - 1], rowCount);
|
||||||
|
for (let i = 1; i < keys.length; i++) {
|
||||||
|
const low = keys[i - 1];
|
||||||
|
const high = keys[i];
|
||||||
|
if (rowCount > low && rowCount < high) {
|
||||||
|
const t = (rowCount - low) / (high - low);
|
||||||
|
return blendRowOffsets(mapSet[low], low, mapSet[high], high, rowCount, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaledRowOffsetsFor(pattern, rowCount, patternName, lengthFt) {
|
||||||
|
if (!Number.isFinite(rowCount) || rowCount <= 0) return null;
|
||||||
|
const manualMaps = collectRowOffsetMaps(patternName);
|
||||||
|
const fromManual = chooseRowOffsets(manualMaps, rowCount, lengthFt);
|
||||||
|
if (fromManual) return fromManual;
|
||||||
|
const byCount = pattern.sizeProfileOpts?.rowOffsetByRowCount;
|
||||||
|
return chooseRowOffsets(byCount, rowCount, lengthFt);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function newGrid(pattern, cells, container, model){
|
function newGrid(pattern, cells, container, model){
|
||||||
@ -501,9 +775,11 @@ function distinctPaletteSlots(palette) {
|
|||||||
const rowColorPatterns = {};
|
const rowColorPatterns = {};
|
||||||
const wireframeMode = false; // per-balloon wireframe handled in cellView for unpainted balloons
|
const wireframeMode = false; // per-balloon wireframe handled in cellView for unpainted balloons
|
||||||
const stackedSlots = (() => {
|
const stackedSlots = (() => {
|
||||||
const slots = distinctPaletteSlots(model.palette);
|
const slots = Object.keys(model.palette || {})
|
||||||
const limit = Math.max(1, Math.min(slots.length, balloonsPerCluster));
|
.map(Number)
|
||||||
return slots.slice(0, limit);
|
.filter(n => Number.isFinite(n) && n > 0)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
return slots.length ? slots : [1];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const colorBlock4 = [
|
const colorBlock4 = [
|
||||||
@ -555,9 +831,9 @@ function distinctPaletteSlots(palette) {
|
|||||||
const rowIndex = cell.y;
|
const rowIndex = cell.y;
|
||||||
if (!rowColorPatterns[rowIndex]) {
|
if (!rowColorPatterns[rowIndex]) {
|
||||||
const totalRows = model.rowCount * (pattern.cellsPerRow || 1);
|
const totalRows = model.rowCount * (pattern.cellsPerRow || 1);
|
||||||
const isRightHalf = false; // mirror mode removed
|
const isCorinthianPattern = (model.patternName || '').toLowerCase().includes('corinthian');
|
||||||
const baseRow = rowIndex;
|
const rowForPattern = rowIndex;
|
||||||
const qEff = baseRow + 1;
|
const qEff = rowForPattern + 1;
|
||||||
let pat;
|
let pat;
|
||||||
|
|
||||||
if (pattern.colorMode === 'stacked') {
|
if (pattern.colorMode === 'stacked') {
|
||||||
@ -577,16 +853,17 @@ function distinctPaletteSlots(palette) {
|
|||||||
// Swap left/right emphasis every 5 clusters to break repetition (per template override)
|
// Swap left/right emphasis every 5 clusters to break repetition (per template override)
|
||||||
if (balloonsPerCluster === 5) {
|
if (balloonsPerCluster === 5) {
|
||||||
const SWAP_EVERY = 5;
|
const SWAP_EVERY = 5;
|
||||||
const blockIndex = Math.floor(rowIndex / SWAP_EVERY);
|
const blockIndex = Math.floor(rowForPattern / SWAP_EVERY);
|
||||||
if (blockIndex % 2 === 1) {
|
if (blockIndex % 2 === 1) {
|
||||||
[pat[0], pat[4]] = [pat[4], pat[0]];
|
[pat[0], pat[4]] = [pat[4], pat[0]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pat.length > 1) {
|
if (pat.length > 1 && reversed) {
|
||||||
let shouldReverse;
|
if (isCorinthianPattern) {
|
||||||
shouldReverse = reversed;
|
pat.push(pat.shift());
|
||||||
if (shouldReverse) pat.reverse();
|
}
|
||||||
|
pat.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
rowColorPatterns[rowIndex] = pat;
|
rowColorPatterns[rowIndex] = pat;
|
||||||
@ -869,11 +1146,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
pattern,
|
pattern,
|
||||||
cells: [],
|
cells: [],
|
||||||
rowCount: clusters,
|
rowCount: clusters,
|
||||||
|
corinthianRowOffsets: pattern.sizeProfile === 'corinthian'
|
||||||
|
? scaledRowOffsetsFor(pattern, clusters, name, lengthFt)
|
||||||
|
: null,
|
||||||
|
manualRowScales: getManualRowScales(name, clusters),
|
||||||
palette: buildClassicPalette(),
|
palette: buildClassicPalette(),
|
||||||
topperColor: getTopperColor(),
|
topperColor: getTopperColor(),
|
||||||
topperType,
|
topperType,
|
||||||
shineEnabled,
|
shineEnabled,
|
||||||
manualMode,
|
manualMode,
|
||||||
|
manualRowOffsets: getManualRowOffsets(name, clusters),
|
||||||
manualFocusEnabled,
|
manualFocusEnabled,
|
||||||
manualFloatingQuad,
|
manualFloatingQuad,
|
||||||
explodedScale,
|
explodedScale,
|
||||||
@ -887,6 +1169,13 @@ function distinctPaletteSlots(palette) {
|
|||||||
let balloonIndexInCluster = 0;
|
let balloonIndexInCluster = 0;
|
||||||
for (let x=0; x<cols; x++) {
|
for (let x=0; x<cols; x++) {
|
||||||
const cellData = pattern.createCell(x,y);
|
const cellData = pattern.createCell(x,y);
|
||||||
|
if (cellData && pattern.sizeProfile === 'corinthian' && cellData.shape && typeof cellData.shape.size === 'number') {
|
||||||
|
const scale = corinthianScale(y, rows, pattern.baseBalloonSize, pattern.sizeProfileOpts);
|
||||||
|
cellData.shape.size *= scale;
|
||||||
|
}
|
||||||
|
if (model.manualMode && cellData && model.manualRowScales && Number.isFinite(model.manualRowScales[y]) && cellData.shape && typeof cellData.shape.size === 'number') {
|
||||||
|
cellData.shape.size *= model.manualRowScales[y];
|
||||||
|
}
|
||||||
if (cellData) model.cells.push({ ...cellData, x, y, balloonIndexInCluster: balloonIndexInCluster++ });
|
if (cellData) model.cells.push({ ...cellData, x, y, balloonIndexInCluster: balloonIndexInCluster++ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1016,6 +1305,30 @@ function distinctPaletteSlots(palette) {
|
|||||||
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
|
return { x: -r*Math.cos(a), y: -r*Math.sin(a) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
patterns['Column 4 Corinthian'] = {
|
||||||
|
deriveFrom: 'Column 4',
|
||||||
|
sizeProfile: 'corinthian',
|
||||||
|
sizeProfileOpts: {
|
||||||
|
baseInches: 11,
|
||||||
|
sizeSequence: [11, 9, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 9, 11],
|
||||||
|
sequenceMode: 'profile',
|
||||||
|
profileTop: [11, 9, 7],
|
||||||
|
profileMid: 5,
|
||||||
|
profileBottom: [7, 9, 11],
|
||||||
|
axisTighten: 0,
|
||||||
|
verticalTighten: 0.15,
|
||||||
|
rowSpacingPower: 1.2,
|
||||||
|
minRowScale: 0.35,
|
||||||
|
rowOffsetBySize: { 11: -1, 9: 19, 7: 38, 5: 56 },
|
||||||
|
rowOffsetByRowCount: {
|
||||||
|
10: { 0: 0, 1: -13, 2: -23, 3: -29, 4: -23, 5: -19, 6: -13, 7: -2, 8: 5, 9: 8 },
|
||||||
|
12: { 0: 0, 1: 14, 2: 29, 3: 38, 4: 47, 5: 55, 6: 64, 7: 72, 8: 80, 9: 92, 10: 108, 11: 125 },
|
||||||
|
14: { 0: 0, 1: 18, 2: 27, 3: 38, 4: 47, 5: 55, 6: 60, 7: 68, 8: 76, 9: 85, 10: 93, 11: 101, 12: 117, 13: 134 },
|
||||||
|
16: { 0: 0, 1: 18, 2: 31, 3: 42, 4: 51, 5: 59, 6: 68, 7: 76, 8: 84, 9: 93, 10: 101, 11: 110, 12: 118, 13: 130, 14: 146, 15: 163 },
|
||||||
|
18: { 0: 0, 1: 18, 2: 31, 3: 38, 4: 47, 5: 55, 6: 64, 7: 68, 8: 76, 9: 85, 10: 93, 11: 102, 12: 110, 13: 118, 14: 127, 15: 139, 16: 150, 17: 168 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
// --- Column 5 (template geometry) ---
|
// --- Column 5 (template geometry) ---
|
||||||
patterns['Column 5'] = {
|
patterns['Column 5'] = {
|
||||||
baseBalloonSize: 25,
|
baseBalloonSize: 25,
|
||||||
@ -1065,6 +1378,30 @@ function distinctPaletteSlots(palette) {
|
|||||||
return { x: -r * Math.cos(a), y: -r * Math.sin(a) };
|
return { x: -r * Math.cos(a), y: -r * Math.sin(a) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
patterns['Column 5 Corinthian'] = {
|
||||||
|
deriveFrom: 'Column 5',
|
||||||
|
sizeProfile: 'corinthian',
|
||||||
|
sizeProfileOpts: {
|
||||||
|
baseInches: 11,
|
||||||
|
sizeSequence: [11, 9, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 9, 11],
|
||||||
|
sequenceMode: 'profile',
|
||||||
|
profileTop: [11, 9, 7],
|
||||||
|
profileMid: 5,
|
||||||
|
profileBottom: [7, 9, 11],
|
||||||
|
axisTighten: 0,
|
||||||
|
verticalTighten: 0.15,
|
||||||
|
rowSpacingPower: 1.2,
|
||||||
|
minRowScale: 0.35,
|
||||||
|
rowOffsetBySize: { 11: -1, 9: 19, 7: 38, 5: 56 },
|
||||||
|
rowOffsetByRowCount: {
|
||||||
|
10: { 0: 0, 1: -13, 2: -23, 3: -29, 4: -23, 5: -19, 6: -13, 7: -2, 8: 5, 9: 8 },
|
||||||
|
12: { 0: 0, 1: 14, 2: 29, 3: 38, 4: 47, 5: 55, 6: 64, 7: 72, 8: 80, 9: 92, 10: 108, 11: 125 },
|
||||||
|
14: { 0: 0, 1: 18, 2: 27, 3: 38, 4: 47, 5: 55, 6: 60, 7: 68, 8: 76, 9: 85, 10: 93, 11: 101, 12: 117, 13: 134 },
|
||||||
|
16: { 0: 0, 1: 18, 2: 31, 3: 42, 4: 51, 5: 59, 6: 68, 7: 76, 8: 84, 9: 93, 10: 101, 11: 110, 12: 118, 13: 130, 14: 146, 15: 163 },
|
||||||
|
18: { 0: 0, 1: 18, 2: 31, 3: 38, 4: 47, 5: 55, 6: 64, 7: 68, 8: 76, 9: 85, 10: 93, 11: 102, 12: 110, 13: 118, 14: 127, 15: 139, 16: 150, 17: 168 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
// --- END: MODIFIED SECTION ---
|
// --- END: MODIFIED SECTION ---
|
||||||
|
|
||||||
// --- Stacked variants (same geometry, single-color clusters alternating rows) ---
|
// --- Stacked variants (same geometry, single-color clusters alternating rows) ---
|
||||||
@ -1093,7 +1430,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initClassicColorPicker(onColorChange) {
|
function initClassicColorPicker(onColorChange) {
|
||||||
const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating');
|
const slotsContainer = document.getElementById('classic-slots'), topperSwatch = document.getElementById('classic-topper-color-swatch'), swatchGrid = document.getElementById('classic-swatch-grid'), activeLabel = document.getElementById('classic-active-label'), randomizeBtn = document.getElementById('classic-randomize-colors'), addSlotBtn = document.getElementById('classic-add-slot'), removeSlotBtn = document.getElementById('classic-remove-slot'), activeChip = document.getElementById('classic-active-chip'), floatingChip = document.getElementById('classic-active-chip-floating'), activeDot = document.getElementById('classic-active-dot'), floatingDot = document.getElementById('classic-active-dot-floating');
|
||||||
const projectBlock = document.getElementById('classic-project-block');
|
const projectBlock = document.getElementById('classic-project-block');
|
||||||
const replaceBlock = document.getElementById('classic-replace-block');
|
const replaceBlock = document.getElementById('classic-replace-block');
|
||||||
const replaceFromSel = document.getElementById('classic-replace-from');
|
const replaceFromSel = document.getElementById('classic-replace-from');
|
||||||
@ -1349,7 +1686,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
sw.title = item.label || item.hex || 'Color';
|
sw.title = item.label || item.hex || 'Color';
|
||||||
sw.addEventListener('click', () => {
|
sw.addEventListener('click', () => {
|
||||||
manualActiveColorGlobal = window.shared?.setActiveColor?.({ hex: item.hex || '#ffffff', image: item.image || null }) || { hex: item.hex || '#ffffff', image: item.image || null };
|
manualActiveColorGlobal = window.shared?.setActiveColor?.({ hex: item.hex || '#ffffff', image: item.image || null }) || { hex: item.hex || '#ffffff', image: item.image || null };
|
||||||
updateClassicDesign();
|
onColorChange?.();
|
||||||
});
|
});
|
||||||
row.appendChild(sw);
|
row.appendChild(sw);
|
||||||
});
|
});
|
||||||
@ -1396,9 +1733,15 @@ function distinctPaletteSlots(palette) {
|
|||||||
const lengthInp = document.getElementById('classic-length-ft');
|
const lengthInp = document.getElementById('classic-length-ft');
|
||||||
const clusters = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
|
const clusters = Math.max(1, Math.round((parseFloat(lengthInp?.value) || 0) * 2));
|
||||||
const maxSlots = Math.min(MAX_SLOTS, clusters);
|
const maxSlots = Math.min(MAX_SLOTS, clusters);
|
||||||
|
const baseSlots = patternSlotCount(patSelect?.value || '');
|
||||||
addSlotBtn.classList.toggle('hidden', !isStacked);
|
addSlotBtn.classList.toggle('hidden', !isStacked);
|
||||||
addSlotBtn.disabled = !isStacked || slotCount >= maxSlots;
|
addSlotBtn.disabled = !isStacked || slotCount >= maxSlots;
|
||||||
}
|
}
|
||||||
|
if (removeSlotBtn) {
|
||||||
|
const baseSlots = patternSlotCount(patSelect?.value || '');
|
||||||
|
removeSlotBtn.classList.toggle('hidden', !isStacked);
|
||||||
|
removeSlotBtn.disabled = !isStacked || slotCount <= baseSlots;
|
||||||
|
}
|
||||||
|
|
||||||
const manualModeOn = isManual();
|
const manualModeOn = isManual();
|
||||||
const sharedActive = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
const sharedActive = window.shared?.getActiveColor?.() || { hex: '#ffffff', image: null };
|
||||||
@ -1438,6 +1781,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
const row = slotsContainer.parentElement;
|
const row = slotsContainer.parentElement;
|
||||||
if (row) row.style.display = manualModeOn ? 'none' : '';
|
if (row) row.style.display = manualModeOn ? 'none' : '';
|
||||||
if (addSlotBtn) addSlotBtn.style.display = manualModeOn ? 'none' : '';
|
if (addSlotBtn) addSlotBtn.style.display = manualModeOn ? 'none' : '';
|
||||||
|
if (removeSlotBtn) removeSlotBtn.style.display = manualModeOn ? 'none' : '';
|
||||||
}
|
}
|
||||||
if (activeChip) {
|
if (activeChip) {
|
||||||
activeChip.style.display = manualModeOn ? '' : 'none';
|
activeChip.style.display = manualModeOn ? '' : 'none';
|
||||||
@ -1519,6 +1863,20 @@ function distinctPaletteSlots(palette) {
|
|||||||
updateUI(); onColorChange();
|
updateUI(); onColorChange();
|
||||||
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
});
|
});
|
||||||
|
removeSlotBtn?.addEventListener('click', () => {
|
||||||
|
const patSelect = document.getElementById('classic-pattern');
|
||||||
|
const name = patSelect?.value || '';
|
||||||
|
const isStacked = name.toLowerCase().includes('stacked');
|
||||||
|
if (!isStacked) return;
|
||||||
|
const baseSlots = patternSlotCount(name);
|
||||||
|
if (slotCount <= baseSlots) return;
|
||||||
|
slotCount = setStoredSlotCount(slotCount - 1);
|
||||||
|
if (parseInt(activeTarget, 10) > slotCount) activeTarget = String(slotCount);
|
||||||
|
classicColors = classicColors.slice(0, slotCount);
|
||||||
|
setClassicColors(classicColors);
|
||||||
|
updateUI(); onColorChange();
|
||||||
|
if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
|
||||||
|
});
|
||||||
replaceFromChip?.addEventListener('click', () => openReplacePicker('from'));
|
replaceFromChip?.addEventListener('click', () => openReplacePicker('from'));
|
||||||
replaceToChip?.addEventListener('click', () => openReplacePicker('to'));
|
replaceToChip?.addEventListener('click', () => openReplacePicker('to'));
|
||||||
replaceFromSel?.addEventListener('change', updateReplaceChips);
|
replaceFromSel?.addEventListener('change', updateReplaceChips);
|
||||||
@ -1571,7 +1929,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
try {
|
try {
|
||||||
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
if (typeof window.m === 'undefined') return fail('Mithril not loaded');
|
||||||
projectPaletteBox = null;
|
projectPaletteBox = null;
|
||||||
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display');
|
const display = document.getElementById('classic-display'), patSel = document.getElementById('classic-pattern'), lengthInp = document.getElementById('classic-length-ft'), clusterHint = document.getElementById('classic-cluster-hint'), reverseCb = document.getElementById('classic-reverse'), topperControls = document.getElementById('topper-controls'), topperToggleRow = document.getElementById('classic-topper-toggle-row'), topperEnabledCb = document.getElementById('classic-topper-enabled'), topperSizeInp = document.getElementById('classic-topper-size'), shineEnabledCb = document.getElementById('classic-shine-enabled'), borderEnabledCb = document.getElementById('classic-border-enabled'), manualModeBtn = document.getElementById('classic-manual-btn'), expandedToggleRow = document.getElementById('classic-expanded-row'), expandedToggle = document.getElementById('classic-expanded-toggle'), focusRow = document.getElementById('classic-focus-row'), focusPrev = document.getElementById('classic-focus-prev'), focusNext = document.getElementById('classic-focus-next'), focusLabel = document.getElementById('classic-focus-label'), floatingBar = document.getElementById('classic-mobile-bar'), floatingChip = document.getElementById('classic-active-chip-floating'), floatingUndo = document.getElementById('classic-undo-manual'), floatingRedo = document.getElementById('classic-redo-manual'), floatingPick = document.getElementById('classic-pick-manual'), floatingErase = document.getElementById('classic-erase-manual'), floatingClear = document.getElementById('classic-clear-manual'), floatingExport = document.getElementById('classic-export-manual'), quadReset = document.getElementById('classic-quad-reset'), focusZoomOut = document.getElementById('classic-focus-zoomout'), manualHub = document.getElementById('classic-manual-hub'), manualRange = document.getElementById('classic-manual-range'), manualRangeLabel = document.getElementById('classic-manual-range-label'), manualPrevBtn = document.getElementById('classic-manual-prev'), manualNextBtn = document.getElementById('classic-manual-next'), manualFullBtn = document.getElementById('classic-manual-full'), manualFocusBtn = document.getElementById('classic-manual-focus'), manualDetailDisplay = document.getElementById('classic-manual-detail-display'), manualSizeRow = document.getElementById('classic-manual-size-row'), manualSizeLabel = document.getElementById('classic-manual-size-label'), manualSizeModePaint = document.getElementById('classic-size-mode-paint'), manualSizeModeInflate = document.getElementById('classic-size-mode-inflate'), manualSizeModeDeflate = document.getElementById('classic-size-mode-deflate'), manualSizeModeSlide = document.getElementById('classic-size-mode-slide'), manualSizeReset = document.getElementById('classic-size-reset');
|
||||||
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
const nudgeOpenBtn = document.getElementById('classic-nudge-open');
|
||||||
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
const fullscreenBtn = document.getElementById('app-fullscreen-toggle');
|
||||||
const toolbar = document.getElementById('classic-canvas-toolbar');
|
const toolbar = document.getElementById('classic-canvas-toolbar');
|
||||||
@ -1610,12 +1968,22 @@ function distinctPaletteSlots(palette) {
|
|||||||
const manualFocusSize = 8;
|
const manualFocusSize = 8;
|
||||||
manualUndoStack = [];
|
manualUndoStack = [];
|
||||||
manualRedoStack = [];
|
manualRedoStack = [];
|
||||||
let manualTool = 'paint'; // paint | pick | erase
|
let manualTool = 'paint'; // paint | inflate | deflate | slide
|
||||||
|
let manualPaintMode = 'paint'; // paint | pick | erase
|
||||||
|
let rowDrag = null;
|
||||||
|
let rowDragRaf = null;
|
||||||
|
const ROW_DRAG_STEP = 2;
|
||||||
|
const ROW_DRAG_THRESHOLD = 6;
|
||||||
|
const MANUAL_SIZE_STEPS = (window.shared?.SIZE_PRESETS || [24, 18, 11, 9, 5])
|
||||||
|
.filter(n => n <= 18 && n >= 5)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
const MANUAL_SIZE_BASE = 11;
|
||||||
|
const MIN_MANUAL_SCALE = Math.min(...MANUAL_SIZE_STEPS) / MANUAL_SIZE_BASE;
|
||||||
|
const MAX_MANUAL_SCALE = Math.max(...MANUAL_SIZE_STEPS) / MANUAL_SIZE_BASE;
|
||||||
let manualFloatingQuad = null;
|
let manualFloatingQuad = null;
|
||||||
let quadModalRow = null;
|
let quadModalRow = null;
|
||||||
let quadModalStartRect = null;
|
let quadModalStartRect = null;
|
||||||
let manualDetailRow = 0;
|
let manualDetailRow = 0;
|
||||||
let manualDetailFrame = null;
|
|
||||||
classicZoom = 1;
|
classicZoom = 1;
|
||||||
window.ClassicDesigner = window.ClassicDesigner || {};
|
window.ClassicDesigner = window.ClassicDesigner || {};
|
||||||
window.ClassicDesigner.randomizeManualFromPalette = () => {
|
window.ClassicDesigner.randomizeManualFromPalette = () => {
|
||||||
@ -1644,6 +2012,117 @@ function distinctPaletteSlots(palette) {
|
|||||||
scheduleManualDetail();
|
scheduleManualDetail();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
const getRowFromTarget = (target) => {
|
||||||
|
const g = target?.closest?.('g[id^="balloon_"]');
|
||||||
|
const id = g?.id || '';
|
||||||
|
const match = id.match(/balloon_(\d+)_(\d+)/);
|
||||||
|
if (!match) return null;
|
||||||
|
return parseInt(match[2], 10);
|
||||||
|
};
|
||||||
|
const getActiveManualRow = () => (manualFloatingQuad !== null ? manualFloatingQuad : manualDetailRow);
|
||||||
|
const clampManualScale = (val) => Math.max(MIN_MANUAL_SCALE, Math.min(MAX_MANUAL_SCALE, val));
|
||||||
|
const getManualScale = (row) => {
|
||||||
|
const key = manualKey(currentPatternName, currentRowCount);
|
||||||
|
const entry = manualRowScales[key] || {};
|
||||||
|
return Number.isFinite(entry[row]) ? entry[row] : 1;
|
||||||
|
};
|
||||||
|
const setManualScale = (row, scale) => {
|
||||||
|
const key = manualKey(currentPatternName, currentRowCount);
|
||||||
|
if (!manualRowScales[key]) manualRowScales[key] = {};
|
||||||
|
if (Math.abs(scale - 1) < 0.001) {
|
||||||
|
delete manualRowScales[key][row];
|
||||||
|
} else {
|
||||||
|
manualRowScales[key][row] = scale;
|
||||||
|
}
|
||||||
|
saveManualRowScales(manualRowScales);
|
||||||
|
};
|
||||||
|
const nearestSizeStep = (inches) => {
|
||||||
|
const steps = MANUAL_SIZE_STEPS.slice().sort((a, b) => a - b);
|
||||||
|
let best = steps[0];
|
||||||
|
let bestDist = Math.abs(inches - best);
|
||||||
|
steps.forEach(step => {
|
||||||
|
const dist = Math.abs(inches - step);
|
||||||
|
if (dist < bestDist) { bestDist = dist; best = step; }
|
||||||
|
});
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
const stepManualSize = (row, dir = 1) => {
|
||||||
|
const steps = MANUAL_SIZE_STEPS.slice().sort((a, b) => a - b);
|
||||||
|
const currentScale = getManualScale(row);
|
||||||
|
const currentInches = currentScale * MANUAL_SIZE_BASE;
|
||||||
|
const snapped = nearestSizeStep(currentInches);
|
||||||
|
const idx = steps.indexOf(snapped);
|
||||||
|
const nextIdx = Math.max(0, Math.min(steps.length - 1, idx + (dir > 0 ? 1 : -1)));
|
||||||
|
return steps[nextIdx];
|
||||||
|
};
|
||||||
|
const applyManualInflate = (row, dir = 1) => {
|
||||||
|
const nextInches = stepManualSize(row, dir);
|
||||||
|
const next = clampManualScale(nextInches / MANUAL_SIZE_BASE);
|
||||||
|
setManualScale(row, next);
|
||||||
|
updateClassicDesign();
|
||||||
|
};
|
||||||
|
const syncManualSizeUi = () => {
|
||||||
|
if (!manualSizeRow) return;
|
||||||
|
manualSizeRow.classList.toggle('hidden', !manualModeState);
|
||||||
|
if (!manualModeState) return;
|
||||||
|
const row = getActiveManualRow();
|
||||||
|
const value = clampManualScale(getManualScale(row));
|
||||||
|
if (manualSizeLabel) {
|
||||||
|
const sizeLabel = Math.round(value * MANUAL_SIZE_BASE);
|
||||||
|
manualSizeLabel.textContent = `Quad ${row + 1} • ${sizeLabel}\"`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scheduleRowDragRedraw = () => {
|
||||||
|
if (rowDragRaf) return;
|
||||||
|
rowDragRaf = requestAnimationFrame(() => {
|
||||||
|
rowDragRaf = null;
|
||||||
|
updateClassicDesign();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleRowDragMove = (evt) => {
|
||||||
|
if (!rowDrag || evt.pointerId !== rowDrag.pointerId) return;
|
||||||
|
if (rowDrag.pending) {
|
||||||
|
const dist = Math.hypot(evt.clientX - rowDrag.startX, evt.clientY - rowDrag.startY);
|
||||||
|
if (dist < ROW_DRAG_THRESHOLD) return;
|
||||||
|
rowDrag.pending = false;
|
||||||
|
display?.setPointerCapture?.(rowDrag.pointerId);
|
||||||
|
}
|
||||||
|
const delta = evt.clientY - rowDrag.startY;
|
||||||
|
const next = rowDrag.baseOffset + delta;
|
||||||
|
manualRowOffsets[rowDrag.key][rowDrag.row] = Math.round(next / ROW_DRAG_STEP) * ROW_DRAG_STEP;
|
||||||
|
saveManualRowOffsets(manualRowOffsets);
|
||||||
|
scheduleRowDragRedraw();
|
||||||
|
};
|
||||||
|
const handleRowDragEnd = (evt) => {
|
||||||
|
if (!rowDrag || evt.pointerId !== rowDrag.pointerId) return;
|
||||||
|
if (!rowDrag.pending) {
|
||||||
|
display?.releasePointerCapture?.(rowDrag.pointerId);
|
||||||
|
}
|
||||||
|
rowDrag = null;
|
||||||
|
window.removeEventListener('pointermove', handleRowDragMove);
|
||||||
|
scheduleRowDragRedraw();
|
||||||
|
};
|
||||||
|
const handleRowDragStart = (evt) => {
|
||||||
|
if (!manualModeState && patternLayout !== 'corinthian') return;
|
||||||
|
if (!(currentPatternName || '').toLowerCase().includes('column')) return;
|
||||||
|
if (manualModeState && manualTool !== 'slide') return;
|
||||||
|
const row = getRowFromTarget(evt.target);
|
||||||
|
if (row === null) return;
|
||||||
|
const key = manualKey(currentPatternName, currentRowCount);
|
||||||
|
if (!manualRowOffsets[key]) manualRowOffsets[key] = {};
|
||||||
|
const baseOffset = Number(manualRowOffsets[key][row]) || 0;
|
||||||
|
rowDrag = {
|
||||||
|
row,
|
||||||
|
key,
|
||||||
|
startX: evt.clientX,
|
||||||
|
startY: evt.clientY,
|
||||||
|
baseOffset,
|
||||||
|
pointerId: evt.pointerId,
|
||||||
|
pending: true
|
||||||
|
};
|
||||||
|
window.addEventListener('pointermove', handleRowDragMove);
|
||||||
|
window.addEventListener('pointerup', handleRowDragEnd, { once: true });
|
||||||
|
};
|
||||||
// Force UI to reflect initial manual state
|
// Force UI to reflect initial manual state
|
||||||
if (manualModeState) patternLayout = 'manual';
|
if (manualModeState) patternLayout = 'manual';
|
||||||
const topperPresets = {
|
const topperPresets = {
|
||||||
@ -1716,16 +2195,19 @@ function distinctPaletteSlots(palette) {
|
|||||||
const computePatternName = () => {
|
const computePatternName = () => {
|
||||||
const base = patternShape === 'column' ? 'Column' : 'Arch';
|
const base = patternShape === 'column' ? 'Column' : 'Arch';
|
||||||
const count = patternCount === 5 ? '5' : '4';
|
const count = patternCount === 5 ? '5' : '4';
|
||||||
const layout = patternLayout === 'stacked' ? ' Stacked' : '';
|
const isCorinthian = patternLayout === 'corinthian' && base === 'Column';
|
||||||
|
const layout = isCorinthian ? ' Corinthian' : (patternLayout === 'stacked' ? ' Stacked' : '');
|
||||||
return `${base} ${count}${layout}`;
|
return `${base} ${count}${layout}`;
|
||||||
};
|
};
|
||||||
const syncPatternStateFromSelect = () => {
|
const syncPatternStateFromSelect = () => {
|
||||||
const val = (patSel?.value || '').toLowerCase();
|
const val = (patSel?.value || '').toLowerCase();
|
||||||
patternShape = val.includes('column') ? 'column' : 'arch';
|
patternShape = val.includes('column') ? 'column' : 'arch';
|
||||||
patternCount = val.includes('5') ? 5 : 4;
|
patternCount = val.includes('5') ? 5 : 4;
|
||||||
patternLayout = val.includes('stacked') ? 'stacked' : 'spiral';
|
if (val.includes('corinthian')) patternLayout = 'corinthian';
|
||||||
|
else patternLayout = val.includes('stacked') ? 'stacked' : 'spiral';
|
||||||
};
|
};
|
||||||
const applyPatternButtons = () => {
|
const applyPatternButtons = () => {
|
||||||
|
if (patternLayout === 'corinthian' && patternShape !== 'column') patternLayout = 'spiral';
|
||||||
const displayLayout = manualModeState ? 'manual' : patternLayout;
|
const displayLayout = manualModeState ? 'manual' : patternLayout;
|
||||||
const setActive = (btns, attr, val) => btns.forEach(b => {
|
const setActive = (btns, attr, val) => btns.forEach(b => {
|
||||||
const active = b.dataset[attr] === val;
|
const active = b.dataset[attr] === val;
|
||||||
@ -1736,7 +2218,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
setActive(patternShapeBtns, 'patternShape', patternShape);
|
setActive(patternShapeBtns, 'patternShape', patternShape);
|
||||||
setActive(patternCountBtns, 'patternCount', String(patternCount));
|
setActive(patternCountBtns, 'patternCount', String(patternCount));
|
||||||
setActive(patternLayoutBtns, 'patternLayout', displayLayout);
|
setActive(patternLayoutBtns, 'patternLayout', displayLayout);
|
||||||
patternLayoutBtns.forEach(b => b.disabled = false);
|
patternLayoutBtns.forEach(b => {
|
||||||
|
if (b.dataset.patternLayout === 'corinthian') {
|
||||||
|
const show = patternShape === 'column';
|
||||||
|
b.classList.toggle('hidden', !show);
|
||||||
|
b.disabled = manualModeState || !show;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
b.classList.toggle('hidden', false);
|
||||||
|
b.disabled = false;
|
||||||
|
});
|
||||||
if (manualModeBtn) {
|
if (manualModeBtn) {
|
||||||
const active = manualModeState;
|
const active = manualModeState;
|
||||||
manualModeBtn.disabled = false;
|
manualModeBtn.disabled = false;
|
||||||
@ -1754,13 +2245,21 @@ function distinctPaletteSlots(palette) {
|
|||||||
}
|
}
|
||||||
if (manualHub) manualHub.classList.toggle('hidden', !manualModeState);
|
if (manualHub) manualHub.classList.toggle('hidden', !manualModeState);
|
||||||
if (floatingBar) floatingBar.classList.toggle('hidden', !manualModeState);
|
if (floatingBar) floatingBar.classList.toggle('hidden', !manualModeState);
|
||||||
|
if (manualSizeRow) manualSizeRow.classList.toggle('hidden', !manualModeState);
|
||||||
// No expanded toggle in bar (handled by main control)
|
// No expanded toggle in bar (handled by main control)
|
||||||
[floatingPick, floatingErase].forEach(btn => {
|
[floatingPick, floatingErase].forEach(btn => {
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const active = manualModeState && manualTool === btn.dataset.tool;
|
const active = manualModeState && manualTool === 'paint' && manualPaintMode === btn.dataset.tool;
|
||||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||||
btn.classList.toggle('active', active);
|
btn.classList.toggle('active', active);
|
||||||
});
|
});
|
||||||
|
[manualSizeModePaint, manualSizeModeInflate, manualSizeModeDeflate, manualSizeModeSlide].forEach(btn => {
|
||||||
|
if (!btn) return;
|
||||||
|
const active = manualModeState && manualTool === btn.dataset.tool;
|
||||||
|
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||||
|
btn.classList.toggle('tab-active', active);
|
||||||
|
btn.classList.toggle('tab-idle', !active);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
syncPatternStateFromSelect();
|
syncPatternStateFromSelect();
|
||||||
|
|
||||||
@ -1825,6 +2324,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
}
|
}
|
||||||
if (manualFullBtn) manualFullBtn.disabled = !manualModeState;
|
if (manualFullBtn) manualFullBtn.disabled = !manualModeState;
|
||||||
if (manualFocusBtn) manualFocusBtn.disabled = !manualModeState;
|
if (manualFocusBtn) manualFocusBtn.disabled = !manualModeState;
|
||||||
|
syncManualSizeUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusSectionForRow = (row) => {
|
const focusSectionForRow = (row) => {
|
||||||
@ -2040,8 +2540,22 @@ function distinctPaletteSlots(palette) {
|
|||||||
patSel.value = computePatternName();
|
patSel.value = computePatternName();
|
||||||
const patternName = patSel.value || 'Arch 4';
|
const patternName = patSel.value || 'Arch 4';
|
||||||
currentPatternName = patternName;
|
currentPatternName = patternName;
|
||||||
|
const isCorinthian = patternName.toLowerCase().includes('corinthian');
|
||||||
|
if (isCorinthian) {
|
||||||
|
lengthInp.min = '5';
|
||||||
|
lengthInp.max = '9';
|
||||||
|
lengthInp.step = '1';
|
||||||
|
const next = Math.round(parseFloat(lengthInp.value) || 5);
|
||||||
|
const clamped = Math.max(5, Math.min(9, next));
|
||||||
|
if (Number.isFinite(clamped)) lengthInp.value = String(clamped);
|
||||||
|
} else {
|
||||||
|
lengthInp.min = '1';
|
||||||
|
lengthInp.max = '100';
|
||||||
|
lengthInp.step = '0.5';
|
||||||
|
}
|
||||||
const clusterCount = Math.max(1, Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
const clusterCount = Math.max(1, Math.round((parseFloat(lengthInp.value) || 0) * 2));
|
||||||
currentRowCount = clusterCount;
|
currentRowCount = clusterCount;
|
||||||
|
GC.setLengthFt(parseFloat(lengthInp.value) || 0);
|
||||||
const manualOn = manualModeState;
|
const manualOn = manualModeState;
|
||||||
if (!manualOn) {
|
if (!manualOn) {
|
||||||
manualFocusEnabled = false;
|
manualFocusEnabled = false;
|
||||||
@ -2069,7 +2583,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
topperControls.classList.toggle('hidden', !showTopper);
|
topperControls.classList.toggle('hidden', !showTopper);
|
||||||
// Number tint controls removed; always use base SVG appearance for numbers.
|
// Number tint controls removed; always use base SVG appearance for numbers.
|
||||||
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
|
if (nudgeOpenBtn) nudgeOpenBtn.classList.toggle('hidden', !showTopper);
|
||||||
const showReverse = patternLayout === 'spiral' && !manualOn;
|
const showReverse = (patternLayout === 'spiral' || patternLayout === 'corinthian') && !manualOn;
|
||||||
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
|
if (reverseLabel) reverseLabel.classList.toggle('hidden', !showReverse);
|
||||||
if (reverseHint) reverseHint.classList.toggle('hidden', !showReverse);
|
if (reverseHint) reverseHint.classList.toggle('hidden', !showReverse);
|
||||||
if (reverseCb) {
|
if (reverseCb) {
|
||||||
@ -2147,26 +2661,33 @@ function distinctPaletteSlots(palette) {
|
|||||||
const x = parseInt(match[1], 10);
|
const x = parseInt(match[1], 10);
|
||||||
const y = parseInt(match[2], 10);
|
const y = parseInt(match[2], 10);
|
||||||
|
|
||||||
if (manualFloatingQuad !== y) {
|
if (manualTool === 'inflate' || manualTool === 'deflate') {
|
||||||
setManualTargetRow(y);
|
applyManualInflate(y, manualTool === 'inflate' ? 1 : -1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('manual paint click', { x, y, manualTool, mode: manualModeState, currentPatternName, currentRowCount });
|
if (manualFloatingQuad !== y) {
|
||||||
|
setManualTargetRow(y);
|
||||||
|
if (manualTool === 'slide') return;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (manualTool === 'slide') return;
|
||||||
|
|
||||||
|
debug('manual paint click', { x, y, manualTool, manualPaintMode, mode: manualModeState, currentPatternName, currentRowCount });
|
||||||
const prev = getManualOverride(currentPatternName, currentRowCount, x, y);
|
const prev = getManualOverride(currentPatternName, currentRowCount, x, y);
|
||||||
const palette = buildClassicPalette();
|
const palette = buildClassicPalette();
|
||||||
const colorCode = parseInt(g?.getAttribute('data-color-code') || '0', 10);
|
const colorCode = parseInt(g?.getAttribute('data-color-code') || '0', 10);
|
||||||
const currentFill = prev || palette[colorCode] || { hex: '#ffffff', image: null };
|
const currentFill = prev || palette[colorCode] || { hex: '#ffffff', image: null };
|
||||||
if (manualTool === 'pick') {
|
if (manualPaintMode === 'pick') {
|
||||||
const picked = { hex: normHex(currentFill.hex || currentFill.colour || '#ffffff'), image: currentFill.image || null };
|
const picked = { hex: normHex(currentFill.hex || currentFill.colour || '#ffffff'), image: currentFill.image || null };
|
||||||
manualActiveColorGlobal = window.shared?.setActiveColor?.(picked) || picked;
|
manualActiveColorGlobal = window.shared?.setActiveColor?.(picked) || picked;
|
||||||
manualRedoStack.length = 0;
|
manualRedoStack.length = 0;
|
||||||
manualTool = 'paint';
|
manualPaintMode = 'paint';
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
manualRedoStack.length = 0;
|
manualRedoStack.length = 0;
|
||||||
if (manualTool === 'erase') {
|
if (manualPaintMode === 'erase') {
|
||||||
const ERASE_COLOR = { hex: 'transparent', colour: 'transparent', image: null };
|
const ERASE_COLOR = { hex: 'transparent', colour: 'transparent', image: null };
|
||||||
manualUndoStack.push({ pattern: currentPatternName, rows: currentRowCount, x, y, prev, next: ERASE_COLOR });
|
manualUndoStack.push({ pattern: currentPatternName, rows: currentRowCount, x, y, prev, next: ERASE_COLOR });
|
||||||
setManualOverride(currentPatternName, currentRowCount, x, y, ERASE_COLOR);
|
setManualOverride(currentPatternName, currentRowCount, x, y, ERASE_COLOR);
|
||||||
@ -2223,7 +2744,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
const setLengthForPattern = () => {
|
const setLengthForPattern = () => {
|
||||||
if (!lengthInp || !patSel) return;
|
if (!lengthInp || !patSel) return;
|
||||||
const isArch = (computePatternName()).toLowerCase().includes('arch');
|
const patternName = computePatternName().toLowerCase();
|
||||||
|
const isArch = patternName.includes('arch');
|
||||||
lengthInp.value = isArch ? 20 : 5;
|
lengthInp.value = isArch ? 20 : 5;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2235,6 +2757,7 @@ function distinctPaletteSlots(palette) {
|
|||||||
|
|
||||||
document.querySelector('#mode-tabs')?.addEventListener('click', () => setTimeout(() => { if (window.updateExportButtonVisibility) window.updateExportButtonVisibility() }, 50));
|
document.querySelector('#mode-tabs')?.addEventListener('click', () => setTimeout(() => { if (window.updateExportButtonVisibility) window.updateExportButtonVisibility() }, 50));
|
||||||
display?.addEventListener('click', handleManualPaint);
|
display?.addEventListener('click', handleManualPaint);
|
||||||
|
display?.addEventListener('pointerdown', handleRowDragStart);
|
||||||
patSel?.addEventListener('change', () => {
|
patSel?.addEventListener('change', () => {
|
||||||
lastPresetKey = null;
|
lastPresetKey = null;
|
||||||
syncPatternStateFromSelect();
|
syncPatternStateFromSelect();
|
||||||
@ -2245,10 +2768,16 @@ function distinctPaletteSlots(palette) {
|
|||||||
setLengthForPattern();
|
setLengthForPattern();
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
});
|
});
|
||||||
patternShapeBtns.forEach(btn => btn.addEventListener('click', () => { patternShape = btn.dataset.patternShape; lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign(); }));
|
patternShapeBtns.forEach(btn => btn.addEventListener('click', () => {
|
||||||
|
patternShape = btn.dataset.patternShape;
|
||||||
|
if (patternShape !== 'column' && patternLayout === 'corinthian') patternLayout = 'spiral';
|
||||||
|
lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign();
|
||||||
|
}));
|
||||||
patternCountBtns.forEach(btn => btn.addEventListener('click', () => { patternCount = Number(btn.dataset.patternCount) === 5 ? 5 : 4; lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign(); }));
|
patternCountBtns.forEach(btn => btn.addEventListener('click', () => { patternCount = Number(btn.dataset.patternCount) === 5 ? 5 : 4; lastPresetKey = null; applyPatternButtons(); setLengthForPattern(); updateClassicDesign(); }));
|
||||||
patternLayoutBtns.forEach(btn => btn.addEventListener('click', () => {
|
patternLayoutBtns.forEach(btn => btn.addEventListener('click', () => {
|
||||||
patternLayout = btn.dataset.patternLayout === 'stacked' ? 'stacked' : 'spiral';
|
const nextLayout = btn.dataset.patternLayout || 'spiral';
|
||||||
|
patternLayout = (nextLayout === 'stacked' || nextLayout === 'corinthian') ? nextLayout : 'spiral';
|
||||||
|
if (patternLayout === 'corinthian' && patternShape !== 'column') patternShape = 'column';
|
||||||
lastNonManualLayout = patternLayout;
|
lastNonManualLayout = patternLayout;
|
||||||
manualModeState = false;
|
manualModeState = false;
|
||||||
saveManualMode(false);
|
saveManualMode(false);
|
||||||
@ -2266,6 +2795,8 @@ function distinctPaletteSlots(palette) {
|
|||||||
const togglingOn = !manualModeState;
|
const togglingOn = !manualModeState;
|
||||||
if (togglingOn) lastNonManualLayout = patternLayout === 'manual' ? 'spiral' : patternLayout;
|
if (togglingOn) lastNonManualLayout = patternLayout === 'manual' ? 'spiral' : patternLayout;
|
||||||
manualModeState = togglingOn;
|
manualModeState = togglingOn;
|
||||||
|
manualTool = 'paint';
|
||||||
|
manualPaintMode = 'paint';
|
||||||
patternLayout = togglingOn ? 'manual' : lastNonManualLayout;
|
patternLayout = togglingOn ? 'manual' : lastNonManualLayout;
|
||||||
manualFocusStart = 0;
|
manualFocusStart = 0;
|
||||||
manualFocusEnabled = false; // keep full-view; quad pull-out handles focus
|
manualFocusEnabled = false; // keep full-view; quad pull-out handles focus
|
||||||
@ -2301,6 +2832,13 @@ function distinctPaletteSlots(palette) {
|
|||||||
manualRange?.addEventListener('input', () => setManualTargetRow(Number(manualRange.value) - 1));
|
manualRange?.addEventListener('input', () => setManualTargetRow(Number(manualRange.value) - 1));
|
||||||
manualPrevBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow - 1));
|
manualPrevBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow - 1));
|
||||||
manualNextBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow + 1));
|
manualNextBtn?.addEventListener('click', () => setManualTargetRow(manualDetailRow + 1));
|
||||||
|
manualSizeReset?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
const row = getActiveManualRow();
|
||||||
|
setManualScale(row, 1);
|
||||||
|
syncManualSizeUi();
|
||||||
|
updateClassicDesign();
|
||||||
|
});
|
||||||
manualFullBtn?.addEventListener('click', () => {
|
manualFullBtn?.addEventListener('click', () => {
|
||||||
manualFocusEnabled = true; // keep focus so detail/rail stay active
|
manualFocusEnabled = true; // keep focus so detail/rail stay active
|
||||||
manualFloatingQuad = null;
|
manualFloatingQuad = null;
|
||||||
@ -2340,10 +2878,49 @@ function distinctPaletteSlots(palette) {
|
|||||||
saveManualExpanded(manualExpandedState);
|
saveManualExpanded(manualExpandedState);
|
||||||
updateClassicDesign();
|
updateClassicDesign();
|
||||||
});
|
});
|
||||||
floatingUndo?.addEventListener('click', undoLastManual);
|
|
||||||
floatingRedo?.addEventListener('click', redoLastManual);
|
floatingRedo?.addEventListener('click', redoLastManual);
|
||||||
floatingPick?.addEventListener('click', () => { manualTool = 'pick'; applyPatternButtons(); });
|
floatingPick?.addEventListener('click', () => {
|
||||||
floatingErase?.addEventListener('click', () => { manualTool = 'erase'; applyPatternButtons(); });
|
if (!manualModeState) return;
|
||||||
|
manualTool = 'paint';
|
||||||
|
manualPaintMode = (manualPaintMode === 'pick') ? 'paint' : 'pick';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
floatingErase?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
manualTool = 'paint';
|
||||||
|
manualPaintMode = (manualPaintMode === 'erase') ? 'paint' : 'erase';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
manualSizeModeInflate?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
manualTool = (manualTool === 'inflate') ? 'paint' : 'inflate';
|
||||||
|
manualPaintMode = 'paint';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
manualSizeModeDeflate?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
manualTool = (manualTool === 'deflate') ? 'paint' : 'deflate';
|
||||||
|
manualPaintMode = 'paint';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
manualSizeModePaint?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
manualTool = 'paint';
|
||||||
|
manualPaintMode = 'paint';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
manualSizeModeSlide?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
manualTool = 'slide';
|
||||||
|
manualPaintMode = 'paint';
|
||||||
|
applyPatternButtons();
|
||||||
|
});
|
||||||
|
manualSizeReset?.addEventListener('click', () => {
|
||||||
|
if (!manualModeState) return;
|
||||||
|
const row = getActiveManualRow();
|
||||||
|
setManualScale(row, 1);
|
||||||
|
updateClassicDesign();
|
||||||
|
});
|
||||||
floatingClear?.addEventListener('click', () => {
|
floatingClear?.addEventListener('click', () => {
|
||||||
const key = manualKey(currentPatternName, currentRowCount);
|
const key = manualKey(currentPatternName, currentRowCount);
|
||||||
const prev = manualOverrides[key] ? { ...manualOverrides[key] } : null;
|
const prev = manualOverrides[key] ? { ...manualOverrides[key] } : null;
|
||||||
|
|||||||
22
index.html
@ -257,6 +257,7 @@
|
|||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
|
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
|
||||||
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
|
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
|
||||||
|
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="corinthian" aria-pressed="false">Corinthian</button>
|
||||||
<button type="button" class="tab-btn tab-idle" id="classic-manual-btn" aria-pressed="false">Manual paint</button>
|
<button type="button" class="tab-btn tab-idle" id="classic-manual-btn" aria-pressed="false">Manual paint</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="classic-expanded-row" class="flex items-center gap-2 hidden">
|
<div id="classic-expanded-row" class="flex items-center gap-2 hidden">
|
||||||
@ -266,6 +267,20 @@
|
|||||||
</label>
|
</label>
|
||||||
<p class="hint m-0">Separate clusters for easier taps.</p>
|
<p class="hint m-0">Separate clusters for easier taps.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="classic-manual-size-row" class="hidden flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-sm font-medium text-gray-700">Manual size mode</div>
|
||||||
|
<span id="classic-manual-size-label" class="text-xs text-gray-500">Quad 1</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<button type="button" id="classic-size-mode-paint" class="tab-btn tab-idle" data-tool="paint">Paint</button>
|
||||||
|
<button type="button" id="classic-size-mode-inflate" class="tab-btn tab-idle" data-tool="inflate">Inflate</button>
|
||||||
|
<button type="button" id="classic-size-mode-deflate" class="tab-btn tab-idle" data-tool="deflate">Deflate</button>
|
||||||
|
<button type="button" id="classic-size-mode-slide" class="tab-btn tab-idle" data-tool="slide">Slide</button>
|
||||||
|
<button type="button" id="classic-size-reset" class="btn-yellow text-xs px-2 py-1">Reset</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint m-0">Pick a mode; click a quad to target. Drag to slide in Slide.</p>
|
||||||
|
</div>
|
||||||
<div id="classic-focus-row" class="flex items-center gap-2 hidden">
|
<div id="classic-focus-row" class="flex items-center gap-2 hidden">
|
||||||
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-prev" aria-hidden="true" tabindex="-1">◀ Prev</button>
|
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-prev" aria-hidden="true" tabindex="-1">◀ Prev</button>
|
||||||
<span id="classic-focus-label" class="text-sm text-gray-700">Clusters 1–8</span>
|
<span id="classic-focus-label" class="text-sm text-gray-700">Clusters 1–8</span>
|
||||||
@ -277,8 +292,10 @@
|
|||||||
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
||||||
<option value="Arch 4">Arch 4 (4-color spiral)</option>
|
<option value="Arch 4">Arch 4 (4-color spiral)</option>
|
||||||
<option value="Column 4">Column 4 (quad wrap)</option>
|
<option value="Column 4">Column 4 (quad wrap)</option>
|
||||||
|
<option value="Column 4 Corinthian">Column 4 (corinthian)</option>
|
||||||
<option value="Arch 5">Arch 5 (5-color spiral)</option>
|
<option value="Arch 5">Arch 5 (5-color spiral)</option>
|
||||||
<option value="Column 5">Column 5 (5-balloon wrap)</option>
|
<option value="Column 5">Column 5 (5-balloon wrap)</option>
|
||||||
|
<option value="Column 5 Corinthian">Column 5 (corinthian)</option>
|
||||||
<option value="Arch 4 Stacked">Arch 4 (stacked)</option>
|
<option value="Arch 4 Stacked">Arch 4 (stacked)</option>
|
||||||
<option value="Column 4 Stacked">Column 4 (stacked)</option>
|
<option value="Column 4 Stacked">Column 4 (stacked)</option>
|
||||||
<option value="Arch 5 Stacked">Arch 5 (stacked)</option>
|
<option value="Arch 5 Stacked">Arch 5 (stacked)</option>
|
||||||
@ -352,6 +369,7 @@
|
|||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div id="classic-slots" class="flex items-center gap-2"></div>
|
<div id="classic-slots" class="flex items-center gap-2"></div>
|
||||||
|
<button id="classic-remove-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Remove color slot">-</button>
|
||||||
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
@ -426,11 +444,11 @@
|
|||||||
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
|
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
|
||||||
<span>Redo</span>
|
<span>Redo</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="mobile-action-btn" id="classic-pick-manual" aria-label="Eyedropper" aria-pressed="false">
|
<button type="button" class="mobile-action-btn" id="classic-pick-manual" aria-label="Eyedropper" aria-pressed="false" data-tool="pick">
|
||||||
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
|
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
|
||||||
<span>Pick</span>
|
<span>Pick</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="mobile-action-btn" id="classic-erase-manual" aria-label="Toggle Erase" aria-pressed="false">
|
<button type="button" class="mobile-action-btn" id="classic-erase-manual" aria-label="Toggle Erase" aria-pressed="false" data-tool="erase">
|
||||||
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
|
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
|
||||||
<span>Erase</span>
|
<span>Erase</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
58
output_webp_backup/0.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
59
output_webp_backup/1.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
58
output_webp_backup/2.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
58
output_webp_backup/3.svg
Normal file
|
After Width: | Height: | Size: 31 KiB |
57
output_webp_backup/4.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
57
output_webp_backup/5.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
58
output_webp_backup/6.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
57
output_webp_backup/7.svg
Normal file
|
After Width: | Height: | Size: 18 KiB |
57
output_webp_backup/8.svg
Normal file
|
After Width: | Height: | Size: 28 KiB |
57
output_webp_backup/9.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
romancolumn.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
130
shift_paths.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import json
|
||||||
|
|
||||||
|
def parse_viewbox(root):
|
||||||
|
vb = root.get('viewBox')
|
||||||
|
if vb:
|
||||||
|
parts = [float(x) for x in vb.split()]
|
||||||
|
if len(parts) == 4:
|
||||||
|
return parts[2], parts[3] # Width, Height
|
||||||
|
width = root.get('width')
|
||||||
|
height = root.get('height')
|
||||||
|
if width and height:
|
||||||
|
return float(width.replace('in','').replace('px','')) * 96, float(height.replace('in','').replace('px','')) * 96 # rough approx if units
|
||||||
|
return 30.0, 40.0 # Fallback
|
||||||
|
|
||||||
|
def tokenize_path(d):
|
||||||
|
# Split by commands and numbers.
|
||||||
|
# Commands: M, m, L, l, V, v, H, h, C, c, S, s, Q, q, T, t, A, a, Z, z
|
||||||
|
# Numbers: float regex
|
||||||
|
tokens = re.findall(r'[a-zA-Z]|[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?', d)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
def is_num(t):
|
||||||
|
try:
|
||||||
|
float(t)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def shift_d(d, dx, dy):
|
||||||
|
tokens = tokenize_path(d)
|
||||||
|
new_tokens = []
|
||||||
|
|
||||||
|
current_cmd = ''
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
# SVG 1.1: first command must be M or m. m treated as absolute M.
|
||||||
|
|
||||||
|
while i < len(tokens):
|
||||||
|
t = tokens[i]
|
||||||
|
if not is_num(t):
|
||||||
|
current_cmd = t
|
||||||
|
new_tokens.append(t)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Special case for 'm' at start: first pair absolute, rest relative
|
||||||
|
if current_cmd == 'm' and len(new_tokens) == 1:
|
||||||
|
# First pair
|
||||||
|
if i+1 < len(tokens):
|
||||||
|
x = float(tokens[i]) + dx
|
||||||
|
y = float(tokens[i+1]) + dy
|
||||||
|
new_tokens.extend([f"{x:.4f}", f"{y:.4f}"])
|
||||||
|
i += 2
|
||||||
|
# Subsequent pairs are relative line-tos, do NOT shift
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# It's a number, implies continuation of previous command or implicit line-to
|
||||||
|
# If we are here, we are processing parameters for current_cmd
|
||||||
|
|
||||||
|
# Determine if absolute coordinate
|
||||||
|
if current_cmd in ['M', 'L', 'C', 'S', 'Q', 'T']:
|
||||||
|
# Pairs of X, Y
|
||||||
|
x = float(tokens[i]) + dx
|
||||||
|
y = float(tokens[i+1]) + dy
|
||||||
|
new_tokens.extend([f"{x:.4f}", f"{y:.4f}"])
|
||||||
|
i += 2
|
||||||
|
elif current_cmd == 'V':
|
||||||
|
y = float(tokens[i]) + dy
|
||||||
|
new_tokens.append(f"{y:.4f}")
|
||||||
|
i += 1
|
||||||
|
elif current_cmd == 'H':
|
||||||
|
x = float(tokens[i]) + dx
|
||||||
|
new_tokens.append(f"{x:.4f}")
|
||||||
|
i += 1
|
||||||
|
elif current_cmd == 'A':
|
||||||
|
# rx ry rot large sweep x y
|
||||||
|
# skip 5
|
||||||
|
new_tokens.extend(tokens[i:i+5])
|
||||||
|
i += 5
|
||||||
|
x = float(tokens[i]) + dx
|
||||||
|
y = float(tokens[i+1]) + dy
|
||||||
|
new_tokens.extend([f"{x:.4f}", f"{y:.4f}"])
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
# Relative commands (m, l, c, s, q, t, v, h, a) - do NOT shift
|
||||||
|
# Just copy tokens until next command
|
||||||
|
# But we need to know how many args to consume?
|
||||||
|
# Easier: just iterate until next alpha token
|
||||||
|
new_tokens.append(t)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return ' '.join(new_tokens)
|
||||||
|
|
||||||
|
base_dir = 'output_webp'
|
||||||
|
final_paths = {}
|
||||||
|
|
||||||
|
for num in range(10):
|
||||||
|
filename = os.path.join(base_dir, f'{num}.svg')
|
||||||
|
if os.path.exists(filename):
|
||||||
|
try:
|
||||||
|
tree = ET.parse(filename)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
w, h = parse_viewbox(root)
|
||||||
|
cx, cy = w/2, h/2
|
||||||
|
|
||||||
|
# Find path
|
||||||
|
ns = {'svg': 'http://www.w3.org/2000/svg'}
|
||||||
|
path = root.find('.//svg:path', ns)
|
||||||
|
if not path:
|
||||||
|
path = root.find('.//{http://www.w3.org/2000/svg}path')
|
||||||
|
if not path:
|
||||||
|
for elem in root.iter():
|
||||||
|
if elem.tag.endswith('path'):
|
||||||
|
path = elem
|
||||||
|
break
|
||||||
|
|
||||||
|
if path is not None:
|
||||||
|
d = path.get('d')
|
||||||
|
if d:
|
||||||
|
# Shift so center is 0,0
|
||||||
|
shifted = shift_d(d, -cx, -cy)
|
||||||
|
final_paths[str(num)] = shifted
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error {num}: {e}")
|
||||||
|
|
||||||
|
print(json.dumps(final_paths))
|
||||||