finite: conduite task
This commit is contained in:
@@ -1,26 +1,40 @@
|
||||
/**
|
||||
* Component-editor for sonic-diamond.
|
||||
*
|
||||
* Modes:
|
||||
* - Matrix (default): grid of all valid preset combinations. Click → Single.
|
||||
* - Single: one component instance with dropdown/checkbox controls and
|
||||
* a checker-pattern preview frame. Diamond has no per-variant tuning,
|
||||
* so this mode here only demonstrates the prop-switching flow.
|
||||
* Matrix: two grids — Full (5×6) and Chipped (2×6). Click cell → Single mode.
|
||||
* Single: Shape/Color dropdowns + Chipped checkbox (auto-disabled for shapes
|
||||
* that don't support chipping). Sliders for dx/dy/scale, draggable origin,
|
||||
* draggable anchors (none for diamond), guides, overlay, A4 scene.
|
||||
*
|
||||
* Mode is driven by the URL hash:
|
||||
* '' → Matrix
|
||||
* 'shape=...&color=...&chipped=...' → Single
|
||||
*
|
||||
* "Copy snapshot" works in either mode and produces JS source ready to
|
||||
* paste between `@editor:anchors-*` and `@editor:variants-*` markers.
|
||||
* Save: POST /api/save-component-snapshot writes JSON next to the component.
|
||||
* Claude reads the JSON and edits the marker-bracketed sections of .mjs.
|
||||
*/
|
||||
await customElements.whenDefined('sonic-diamond');
|
||||
|
||||
const Comp = customElements.get('sonic-diamond');
|
||||
const { presetProps, chippedShapes } = Comp;
|
||||
const ANCHOR_NAMES = Object.keys(Comp.anchorsDefault);
|
||||
const A4_W_MM = 210;
|
||||
const A4_H_MM = 297;
|
||||
const STORAGE_KEY = 'funpen.editor.sonic-diamond.draft';
|
||||
const pendingGuides = [];
|
||||
|
||||
if (loadDraft()) {
|
||||
document.getElementById('draft-banner').hidden = false;
|
||||
}
|
||||
document.getElementById('btn-discard-draft').addEventListener('click', () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
document.getElementById('meta').textContent =
|
||||
`${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y})`;
|
||||
`${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y}) · ${ANCHOR_NAMES.length} anchors`;
|
||||
|
||||
let singleInstance = null;
|
||||
let overlayInstance = null;
|
||||
const anchorHandles = {};
|
||||
const anchorRows = {};
|
||||
const guides = [];
|
||||
|
||||
initMatrix();
|
||||
initSingle();
|
||||
@@ -28,11 +42,65 @@ window.addEventListener('hashchange', route);
|
||||
route();
|
||||
|
||||
document.getElementById('btn-copy').addEventListener('click', async () => {
|
||||
const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToast('Snapshot copied');
|
||||
await navigator.clipboard.writeText(JSON.stringify(snapshotData(), null, 2));
|
||||
showToast('Snapshot JSON copied');
|
||||
});
|
||||
|
||||
document.getElementById('btn-save').addEventListener('click', async () => {
|
||||
try {
|
||||
const r = await fetch('/api/save-component-snapshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
theme: 'sonic',
|
||||
componentName: 'diamond',
|
||||
...snapshotData(),
|
||||
}),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
showToast('Saved → ' + data.path.split('/').slice(-3).join('/'));
|
||||
} catch (e) {
|
||||
showToast('Save failed: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
function snapshotData() {
|
||||
return {
|
||||
origin: { ...Comp.origin },
|
||||
anchorsDefault: structuredClone(Comp.anchorsDefault),
|
||||
variants: structuredClone(Comp.variants),
|
||||
guides: guidesSerialize(),
|
||||
};
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshotData()));
|
||||
} catch (e) { /* quota or disabled */ }
|
||||
}
|
||||
|
||||
function loadDraft() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return false;
|
||||
const data = JSON.parse(raw);
|
||||
if (data.origin) { Comp.origin.x = data.origin.x; Comp.origin.y = data.origin.y; }
|
||||
if (data.anchorsDefault) Object.assign(Comp.anchorsDefault, data.anchorsDefault);
|
||||
if (data.variants) {
|
||||
for (const [key, v] of Object.entries(data.variants)) {
|
||||
if (Comp.variants[key]) Object.assign(Comp.variants[key], v);
|
||||
}
|
||||
}
|
||||
if (data.guides) pendingGuides.push(...data.guides);
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
function guidesSerialize() {
|
||||
return guides.map(g => ({ axis: g.axis, mm: g.mm }));
|
||||
}
|
||||
|
||||
// ---- Routing ----
|
||||
|
||||
function route() {
|
||||
@@ -121,8 +189,6 @@ function makeCell(shape, color, chipped) {
|
||||
|
||||
// ---- Single mode ----
|
||||
|
||||
let singleInstance = null;
|
||||
|
||||
function initSingle() {
|
||||
fillSelect('ctrl-shape', presetProps.shape);
|
||||
fillSelect('ctrl-color', presetProps.color);
|
||||
@@ -139,12 +205,142 @@ function initSingle() {
|
||||
document.getElementById('btn-back').addEventListener('click', () => {
|
||||
location.hash = '';
|
||||
});
|
||||
|
||||
bindSlider('ctrl-dx', 'ctrl-dx-num', val => updateField('dx', val));
|
||||
bindSlider('ctrl-dy', 'ctrl-dy-num', val => updateField('dy', val));
|
||||
bindSlider('ctrl-scale', 'ctrl-scale-num', val => updateField('scale', val));
|
||||
|
||||
document.getElementById('ctrl-edit-defaults').addEventListener('change', () => {
|
||||
refreshAnchors();
|
||||
});
|
||||
|
||||
const zoom = document.getElementById('ctrl-zoom');
|
||||
zoom.addEventListener('input', () => applyZoom(parseFloat(zoom.value)));
|
||||
applyZoom(parseFloat(zoom.value));
|
||||
|
||||
document.getElementById('ctrl-grid').addEventListener('change', e =>
|
||||
setGridColumns(parseInt(e.target.value, 10))
|
||||
);
|
||||
|
||||
document.getElementById('btn-reset').addEventListener('click', resetVariant);
|
||||
|
||||
const overlaySel = document.getElementById('ctrl-overlay-variant');
|
||||
for (const key of Object.keys(Comp.variants)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = key; o.textContent = key;
|
||||
overlaySel.appendChild(o);
|
||||
}
|
||||
overlaySel.addEventListener('change', () => applyOverlay());
|
||||
const overlayOpacity = document.getElementById('ctrl-overlay-opacity');
|
||||
overlayOpacity.addEventListener('input', () => {
|
||||
document.getElementById('overlay-opacity-label').textContent =
|
||||
`${Math.round(parseFloat(overlayOpacity.value) * 100)}%`;
|
||||
applyOverlay();
|
||||
});
|
||||
|
||||
document.getElementById('btn-guide-v').addEventListener('click', () => addGuide('v'));
|
||||
document.getElementById('btn-guide-h').addEventListener('click', () => addGuide('h'));
|
||||
|
||||
for (const g of pendingGuides) addGuide(g.axis, g.mm);
|
||||
pendingGuides.length = 0;
|
||||
|
||||
buildAnchorRows();
|
||||
if (ANCHOR_NAMES.length === 0) {
|
||||
document.getElementById('anchors-label').textContent = 'Anchors — (none defined)';
|
||||
} else {
|
||||
document.getElementById('anchors-label').textContent =
|
||||
`Anchors (${ANCHOR_NAMES.length}) — drag handles on stage`;
|
||||
}
|
||||
}
|
||||
|
||||
function bindSlider(rangeId, numId, onChange) {
|
||||
const r = document.getElementById(rangeId);
|
||||
const n = document.getElementById(numId);
|
||||
r.addEventListener('input', () => { n.value = r.value; onChange(parseFloat(r.value)); });
|
||||
n.addEventListener('input', () => { r.value = n.value; onChange(parseFloat(n.value)); });
|
||||
}
|
||||
|
||||
function updateField(field, value) {
|
||||
const key = currentKey();
|
||||
const v = Comp.variants[key];
|
||||
if (!v) return;
|
||||
v[field] = value;
|
||||
if (singleInstance) singleInstance._render();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
function resetVariant() {
|
||||
const v = Comp.variants[currentKey()];
|
||||
if (!v) return;
|
||||
v.dx = 0; v.dy = 0; v.scale = 1; v.anchors = {};
|
||||
setSlider('ctrl-dx', 0);
|
||||
setSlider('ctrl-dy', 0);
|
||||
setSlider('ctrl-scale', 1);
|
||||
if (singleInstance) singleInstance._render();
|
||||
refreshAnchors();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
function applyZoom(z) {
|
||||
document.getElementById('scene').style.transform = `scale(${z})`;
|
||||
const sz = document.getElementById('scene-zoom');
|
||||
sz.style.width = `${A4_W_MM * z}mm`;
|
||||
sz.style.height = `${A4_H_MM * z}mm`;
|
||||
document.getElementById('zoom-label').textContent = `${Math.round(z * 100)}%`;
|
||||
scrollToComponent();
|
||||
}
|
||||
|
||||
function scrollToComponent() {
|
||||
const stage = document.getElementById('single-stage');
|
||||
const overlay = document.getElementById('anchor-overlay');
|
||||
if (!stage || !overlay) return;
|
||||
requestAnimationFrame(() => {
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const overlayRect = overlay.getBoundingClientRect();
|
||||
const cx = overlayRect.left + overlayRect.width / 2;
|
||||
const cy = overlayRect.top + overlayRect.height / 2;
|
||||
const targetX = stageRect.left + stageRect.width / 2;
|
||||
const targetY = stageRect.top + stageRect.height / 2;
|
||||
stage.scrollLeft += cx - targetX;
|
||||
stage.scrollTop += cy - targetY;
|
||||
});
|
||||
}
|
||||
|
||||
function setGridColumns(cols) {
|
||||
const grid = document.getElementById('scene-grid');
|
||||
grid.innerHTML = '';
|
||||
if (!cols) return;
|
||||
for (let i = 1; i < cols; i++) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'grid-line';
|
||||
line.style.left = `${(A4_W_MM / cols) * i}mm`;
|
||||
grid.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAnchorRows() {
|
||||
const list = document.getElementById('anchor-list');
|
||||
list.innerHTML = '';
|
||||
for (const name of ANCHOR_NAMES) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'anchor-row';
|
||||
row.innerHTML = `<span class="name">${name}</span>
|
||||
<span class="coord" data-axis="x">—</span>
|
||||
<span class="coord" data-axis="y">—</span>`;
|
||||
row.addEventListener('click', () => focusAnchor(name));
|
||||
list.appendChild(row);
|
||||
anchorRows[name] = row;
|
||||
}
|
||||
}
|
||||
|
||||
function applySingle(params) {
|
||||
if (!singleInstance) {
|
||||
singleInstance = document.createElement('sonic-diamond');
|
||||
document.getElementById('single-frame').appendChild(singleInstance);
|
||||
singleInstance.classList.add('editable');
|
||||
singleInstance.tabIndex = 0;
|
||||
singleInstance.addEventListener('mousedown', startComponentDrag);
|
||||
singleInstance.addEventListener('keydown', handleNudge);
|
||||
document.getElementById('scene').appendChild(singleInstance);
|
||||
}
|
||||
|
||||
const shape = params.get('shape') ?? presetProps.shape[0];
|
||||
@@ -165,10 +361,317 @@ function applySingle(params) {
|
||||
cbLabel.textContent = supportsChipped ? 'Chipped' : 'Chipped (n/a for this shape)';
|
||||
|
||||
const key = singleInstance._variantKey();
|
||||
const v = Comp.variants[key] || {};
|
||||
|
||||
setSlider('ctrl-dx', v.dx ?? 0);
|
||||
setSlider('ctrl-dy', v.dy ?? 0);
|
||||
setSlider('ctrl-scale', v.scale ?? 1);
|
||||
|
||||
document.getElementById('info-variant').textContent = key;
|
||||
document.getElementById('info-basesize').textContent = `${Comp.baseSize.w}×${Comp.baseSize.h}mm`;
|
||||
document.getElementById('info-origin').textContent = `(${Comp.origin.x}, ${Comp.origin.y})`;
|
||||
document.getElementById('info-img').textContent = Comp.variants[key]?.img ?? '—';
|
||||
document.getElementById('info-img').textContent = v.img ?? '—';
|
||||
|
||||
const overlay = document.getElementById('anchor-overlay');
|
||||
overlay.style.width = `${Comp.baseSize.w}mm`;
|
||||
overlay.style.height = `${Comp.baseSize.h}mm`;
|
||||
|
||||
refreshAnchors();
|
||||
applyOverlay();
|
||||
scrollToComponent();
|
||||
}
|
||||
|
||||
function setSlider(rangeId, value) {
|
||||
const r = document.getElementById(rangeId);
|
||||
const n = document.getElementById(rangeId + '-num');
|
||||
r.value = value;
|
||||
n.value = value;
|
||||
}
|
||||
|
||||
function measurePxPerMm() {
|
||||
const overlay = document.getElementById('anchor-overlay');
|
||||
const rect = overlay.getBoundingClientRect();
|
||||
return rect.width / Comp.baseSize.w;
|
||||
}
|
||||
|
||||
function refreshAnchors() {
|
||||
const overlay = document.getElementById('anchor-overlay');
|
||||
overlay.querySelectorAll('.anchor-handle, .origin-marker').forEach(h => h.remove());
|
||||
|
||||
const editDefaults = document.getElementById('ctrl-edit-defaults').checked;
|
||||
overlay.classList.toggle('defaults-mode', editDefaults);
|
||||
|
||||
const o = Comp.origin;
|
||||
const origin = document.createElement('div');
|
||||
origin.className = 'origin-marker';
|
||||
origin.style.left = `${o.x}mm`;
|
||||
origin.style.top = `${o.y}mm`;
|
||||
origin.innerHTML = `<span class="lbl">origin (${o.x.toFixed(1)}, ${o.y.toFixed(1)})</span>`;
|
||||
origin.addEventListener('mousedown', startOriginDrag);
|
||||
overlay.appendChild(origin);
|
||||
|
||||
const key = currentKey();
|
||||
const variant = Comp.variants[key];
|
||||
if (!variant) return;
|
||||
|
||||
for (const name of ANCHOR_NAMES) {
|
||||
const def = Comp.anchorsDefault[name];
|
||||
const override = variant.anchors?.[name];
|
||||
const shown = editDefaults ? { ...def } : { ...def, ...(override || {}) };
|
||||
const isOverridden = !!override;
|
||||
|
||||
const handle = document.createElement('div');
|
||||
handle.className = 'anchor-handle' + (isOverridden ? ' overridden' : '');
|
||||
handle.style.left = `${shown.x}mm`;
|
||||
handle.style.top = `${shown.y}mm`;
|
||||
handle.dataset.anchor = name;
|
||||
handle.innerHTML = `<span class="lbl">${name} (${shown.x.toFixed(1)}, ${shown.y.toFixed(1)})</span>`;
|
||||
handle.addEventListener('mousedown', startDrag);
|
||||
handle.addEventListener('click', e => { e.stopPropagation(); focusAnchor(name); });
|
||||
overlay.appendChild(handle);
|
||||
anchorHandles[name] = handle;
|
||||
|
||||
const resolved = { ...def, ...(override || {}) };
|
||||
const row = anchorRows[name];
|
||||
if (row) {
|
||||
row.classList.toggle('overridden', isOverridden);
|
||||
row.querySelector('[data-axis=x]').textContent = resolved.x.toFixed(1);
|
||||
row.querySelector('[data-axis=y]').textContent = resolved.y.toFixed(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function focusAnchor(name) {
|
||||
for (const [n, h] of Object.entries(anchorHandles)) h.classList.toggle('active', n === name);
|
||||
for (const [n, r] of Object.entries(anchorRows)) r.classList.toggle('active', n === name);
|
||||
}
|
||||
|
||||
function startDrag(e) {
|
||||
e.preventDefault();
|
||||
const handle = e.currentTarget;
|
||||
const name = handle.dataset.anchor;
|
||||
focusAnchor(name);
|
||||
handle.classList.add('dragging');
|
||||
|
||||
const ppm = measurePxPerMm();
|
||||
const startMouse = { x: e.clientX, y: e.clientY };
|
||||
const editDefaults = document.getElementById('ctrl-edit-defaults').checked;
|
||||
const startPos = editDefaults
|
||||
? { ...Comp.anchorsDefault[name] }
|
||||
: (() => {
|
||||
const variant = Comp.variants[currentKey()];
|
||||
const def = Comp.anchorsDefault[name];
|
||||
const ovr = variant.anchors?.[name];
|
||||
return { ...def, ...(ovr || {}) };
|
||||
})();
|
||||
|
||||
function onMove(ev) {
|
||||
const newPos = {
|
||||
x: round1(startPos.x + (ev.clientX - startMouse.x) / ppm),
|
||||
y: round1(startPos.y + (ev.clientY - startMouse.y) / ppm),
|
||||
};
|
||||
if (editDefaults) {
|
||||
Comp.anchorsDefault[name] = newPos;
|
||||
} else {
|
||||
const variant = Comp.variants[currentKey()];
|
||||
variant.anchors = variant.anchors || {};
|
||||
variant.anchors[name] = newPos;
|
||||
}
|
||||
handle.style.left = `${newPos.x}mm`;
|
||||
handle.style.top = `${newPos.y}mm`;
|
||||
handle.querySelector('.lbl').textContent =
|
||||
`${name} (${newPos.x.toFixed(1)}, ${newPos.y.toFixed(1)})`;
|
||||
const row = anchorRows[name];
|
||||
if (row) {
|
||||
row.querySelector('[data-axis=x]').textContent = newPos.x.toFixed(1);
|
||||
row.querySelector('[data-axis=y]').textContent = newPos.y.toFixed(1);
|
||||
row.classList.toggle('overridden', !editDefaults);
|
||||
}
|
||||
if (!editDefaults) handle.classList.add('overridden');
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
handle.classList.remove('dragging');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function startOriginDrag(e) {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handle = e.currentTarget;
|
||||
handle.classList.add('dragging');
|
||||
const ppm = measurePxPerMm();
|
||||
const startMouse = { x: e.clientX, y: e.clientY };
|
||||
const start = { ...Comp.origin };
|
||||
|
||||
function onMove(ev) {
|
||||
Comp.origin.x = round1(start.x + (ev.clientX - startMouse.x) / ppm);
|
||||
Comp.origin.y = round1(start.y + (ev.clientY - startMouse.y) / ppm);
|
||||
handle.style.left = `${Comp.origin.x}mm`;
|
||||
handle.style.top = `${Comp.origin.y}mm`;
|
||||
handle.querySelector('.lbl').textContent =
|
||||
`origin (${Comp.origin.x.toFixed(1)}, ${Comp.origin.y.toFixed(1)})`;
|
||||
document.getElementById('info-origin').textContent =
|
||||
`(${Comp.origin.x}, ${Comp.origin.y})`;
|
||||
}
|
||||
function onUp() {
|
||||
handle.classList.remove('dragging');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveDraft();
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function startComponentDrag(e) {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
singleInstance.focus();
|
||||
singleInstance.classList.add('dragging');
|
||||
|
||||
const ppm = measurePxPerMm();
|
||||
const startMouse = { x: e.clientX, y: e.clientY };
|
||||
const v = Comp.variants[currentKey()];
|
||||
if (!v) return;
|
||||
const startDx = v.dx;
|
||||
const startDy = v.dy;
|
||||
|
||||
function onMove(ev) {
|
||||
v.dx = round1(startDx + (ev.clientX - startMouse.x) / ppm);
|
||||
v.dy = round1(startDy + (ev.clientY - startMouse.y) / ppm);
|
||||
setSlider('ctrl-dx', v.dx);
|
||||
setSlider('ctrl-dy', v.dy);
|
||||
singleInstance._render();
|
||||
}
|
||||
function onUp() {
|
||||
singleInstance.classList.remove('dragging');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveDraft();
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function handleNudge(e) {
|
||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
|
||||
e.preventDefault();
|
||||
const step = e.shiftKey ? 1 : 0.1;
|
||||
const v = Comp.variants[currentKey()];
|
||||
if (!v) return;
|
||||
if (e.key === 'ArrowLeft') v.dx = round1(v.dx - step);
|
||||
if (e.key === 'ArrowRight') v.dx = round1(v.dx + step);
|
||||
if (e.key === 'ArrowUp') v.dy = round1(v.dy - step);
|
||||
if (e.key === 'ArrowDown') v.dy = round1(v.dy + step);
|
||||
setSlider('ctrl-dx', v.dx);
|
||||
setSlider('ctrl-dy', v.dy);
|
||||
singleInstance._render();
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
function applyOverlay() {
|
||||
const sel = document.getElementById('ctrl-overlay-variant');
|
||||
const opacity = parseFloat(document.getElementById('ctrl-overlay-opacity').value);
|
||||
const variantKey = sel.value;
|
||||
if (!variantKey) {
|
||||
if (overlayInstance) { overlayInstance.remove(); overlayInstance = null; }
|
||||
return;
|
||||
}
|
||||
if (!overlayInstance) {
|
||||
overlayInstance = document.createElement('sonic-diamond');
|
||||
overlayInstance.classList.add('overlay-instance');
|
||||
document.getElementById('scene').appendChild(overlayInstance);
|
||||
}
|
||||
// Decompose key like "classic_blue" or "classic_blue_chipped"
|
||||
const parts = variantKey.split('_');
|
||||
let shape = parts[0];
|
||||
let color = parts[1];
|
||||
let chipped = parts[2] === 'chipped';
|
||||
overlayInstance.setAttribute('shape', shape);
|
||||
overlayInstance.setAttribute('color', color);
|
||||
overlayInstance.setAttribute('chipped', String(chipped));
|
||||
overlayInstance.style.opacity = String(opacity);
|
||||
}
|
||||
|
||||
function addGuide(axis, mm) {
|
||||
if (mm === undefined) mm = axis === 'v' ? A4_W_MM / 2 : A4_H_MM / 4;
|
||||
const el = document.createElement('div');
|
||||
el.className = `guide guide-${axis}`;
|
||||
if (axis === 'v') el.style.left = `${mm}mm`;
|
||||
else el.style.top = `${mm}mm`;
|
||||
|
||||
const coord = document.createElement('span');
|
||||
coord.className = 'guide-coord';
|
||||
coord.textContent = `${mm.toFixed(1)}mm`;
|
||||
el.appendChild(coord);
|
||||
|
||||
const remove = document.createElement('button');
|
||||
remove.className = 'guide-remove';
|
||||
remove.textContent = '×';
|
||||
remove.addEventListener('click', e => { e.stopPropagation(); removeGuide(guide); });
|
||||
el.appendChild(remove);
|
||||
|
||||
const guide = { axis, mm, el, coord };
|
||||
el.addEventListener('mousedown', e => startGuideDrag(e, guide));
|
||||
|
||||
document.getElementById('scene').appendChild(el);
|
||||
guides.push(guide);
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
function removeGuide(guide) {
|
||||
guide.el.remove();
|
||||
const i = guides.indexOf(guide);
|
||||
if (i >= 0) guides.splice(i, 1);
|
||||
saveDraft();
|
||||
}
|
||||
|
||||
function startGuideDrag(e, guide) {
|
||||
if (e.target.classList.contains('guide-remove')) return;
|
||||
e.preventDefault();
|
||||
const ppm = measurePxPerMm();
|
||||
const startMouse = { x: e.clientX, y: e.clientY };
|
||||
const startMm = guide.mm;
|
||||
|
||||
function onMove(ev) {
|
||||
if (guide.axis === 'v') {
|
||||
guide.mm = round1(startMm + (ev.clientX - startMouse.x) / ppm);
|
||||
guide.el.style.left = `${guide.mm}mm`;
|
||||
} else {
|
||||
guide.mm = round1(startMm + (ev.clientY - startMouse.y) / ppm);
|
||||
guide.el.style.top = `${guide.mm}mm`;
|
||||
}
|
||||
guide.coord.textContent = `${guide.mm.toFixed(1)}mm`;
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveDraft();
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function round1(v) { return Math.round(v * 10) / 10; }
|
||||
|
||||
function currentKey() {
|
||||
if (!singleInstance) {
|
||||
const params = new URLSearchParams(location.hash.slice(1));
|
||||
const shape = params.get('shape') ?? presetProps.shape[0];
|
||||
const color = params.get('color') ?? presetProps.color[0];
|
||||
const chipped = params.get('chipped') === 'true';
|
||||
const supportsChipped = chippedShapes.has(shape);
|
||||
return supportsChipped && chipped ? `${shape}_${color}_chipped` : `${shape}_${color}`;
|
||||
}
|
||||
return singleInstance._variantKey();
|
||||
}
|
||||
|
||||
function fillSelect(id, options) {
|
||||
@@ -182,39 +685,6 @@ function fillSelect(id, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Snapshot serialization ----
|
||||
|
||||
function serializeSnapshot(anchorsDefault, variants) {
|
||||
const lines = [];
|
||||
lines.push(' // @editor:anchors-start');
|
||||
lines.push(` static anchorsDefault = ${formatAnchors(anchorsDefault, ' ')};`);
|
||||
lines.push(' // @editor:anchors-end');
|
||||
lines.push('');
|
||||
lines.push(' // @editor:variants-start');
|
||||
lines.push(' static variants = {');
|
||||
for (const [key, v] of Object.entries(variants)) {
|
||||
const a = formatAnchorsInline(v.anchors || {});
|
||||
lines.push(` ${key}: { img: ${JSON.stringify(v.img)}, dx: ${v.dx}, dy: ${v.dy}, scale: ${v.scale}, anchors: ${a} },`);
|
||||
}
|
||||
lines.push(' };');
|
||||
lines.push(' // @editor:variants-end');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatAnchors(anchors, indent) {
|
||||
const keys = Object.keys(anchors);
|
||||
if (keys.length === 0) return '{}';
|
||||
const inner = keys.map(k => `${indent} ${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`).join(',\n');
|
||||
return `{\n${inner},\n${indent}}`;
|
||||
}
|
||||
|
||||
function formatAnchorsInline(anchors) {
|
||||
const keys = Object.keys(anchors);
|
||||
if (keys.length === 0) return '{}';
|
||||
const parts = keys.map(k => `${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`);
|
||||
return `{ ${parts.join(', ')} }`;
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
|
||||
Reference in New Issue
Block a user