Added files generated by Claude, prompted by sdk based on the md files
This commit is contained in:
115
static/components/ShapeEditor.js
Normal file
115
static/components/ShapeEditor.js
Normal file
@@ -0,0 +1,115 @@
|
||||
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(),
|
||||
]);
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user