vue3js-app/static/components/ShapeEditor.js

116 lines
4.5 KiB
JavaScript

import { h } from 'vue';
// Renders a shape's coord table with cascade-shift and an SVG preview.
// `shape` is mutated in place; `onChange` called after each mutation.
export const ShapeEditor = {
props: ['shape', 'onChange'],
setup(props) {
function updateCoord(i, field, value) {
const coord = props.shape.coords[i];
const num = parseFloat(value);
if (isNaN(num)) return;
const old = coord[field];
const delta = num - old;
coord[field] = num;
// cascade-shift: if x increased past next coord, shift all following
if (field === 'x' && delta > 0) {
for (let j = i + 1; j < props.shape.coords.length; j++) {
if (props.shape.coords[j].x <= num) {
props.shape.coords[j].x += delta;
} else break;
}
}
props.onChange?.();
}
function addCoord() {
const coords = props.shape.coords;
const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1;
coords.push({ x: lastX, y: 0, z: 1, isSharp: false });
props.onChange?.();
}
function removeCoord(i) {
props.shape.coords.splice(i, 1);
props.onChange?.();
}
function renderSvg() {
const coords = props.shape.coords;
if (!coords.length) return h('svg', { class: 'se-shape-svg' });
const xs = coords.map(c => c.x);
const ys = coords.map(c => c.y);
const minX = Math.min(...xs), maxX = Math.max(...xs);
const minY = Math.min(...ys), maxY = Math.max(...ys);
const W = 200, H = 60;
const rangeX = maxX - minX || 1;
const rangeY = maxY - minY || 1;
const toSvg = c => {
const sx = ((c.x - minX) / rangeX) * W;
const sy = H - ((c.y - minY) / rangeY) * H;
return [sx, sy];
};
const points = coords.map(c => toSvg(c).join(',') ).join(' ');
return h('svg', {
class: 'se-shape-svg',
viewBox: `0 0 ${W} ${H}`,
preserveAspectRatio: 'none',
}, [
h('polyline', {
points,
fill: 'none',
stroke: '#2a6aaa',
'stroke-width': '1.5',
}),
...coords.map(c => {
const [sx, sy] = toSvg(c);
return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' });
}),
]);
}
return () => {
const shape = props.shape;
if (!shape) return null;
return h('div', { class: 'se-shape-editor' }, [
h('table', { class: 'se-shape-table' }, [
h('thead', null, h('tr', null, [
h('th', null, 'x'), h('th', null, 'y'), h('th', null, 'z'),
h('th', null, '♯'), h('th', null, ''),
])),
h('tbody', null, shape.coords.map((coord, i) =>
h('tr', { key: i }, [
h('td', null, h('input', {
type: 'number', value: coord.x, step: 1,
onInput: e => updateCoord(i, 'x', e.target.value),
})),
h('td', null, h('input', {
type: 'number', value: coord.y, step: 1,
onInput: e => updateCoord(i, 'y', e.target.value),
})),
h('td', null, h('input', {
type: 'number', value: coord.z ?? 1, step: 0.1,
onInput: e => updateCoord(i, 'z', e.target.value),
})),
h('td', null, h('input', {
type: 'checkbox', checked: !!coord.isSharp,
onChange: e => { coord.isSharp = e.target.checked; props.onChange?.(); },
})),
h('td', null, h('button', { onClick: () => removeCoord(i) }, '✕')),
])
)),
]),
h('button', { class: 'se-btn', style: 'margin-top:0.3rem', onClick: addCoord }, '+ coord'),
renderSvg(),
]);
};
},
};