116 lines
4.5 KiB
JavaScript
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(),
|
|
]);
|
|
};
|
|
},
|
|
};
|