feat: selectable marker shapes per edge endpoint

Replace the on/off arrowhead toggle with a per-end shape picker. Each end
(start/end) independently selects: none, arrow, arrow-open, circle, diamond,
or square. Markers still recolor live from the resolved stroke color.

Frontend:
- MarkerShape type + edgeMarkers util (normalizeMarker, MARKER_GEOMETRY);
  legacy boolean coerces to 'arrow' on read.
- Per-shape <marker> inner geometry; symmetric shapes use fixed orient.
- MarkerShapePicker reused in EdgeModal and CustomStyleModal.
- Serializer normalizes to shape strings.

Backend:
- Edge marker columns Boolean -> String (default 'none'); TEXT migration.
- normalize_marker() + validators coerce legacy bool / unknown values.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-07-05 11:30:49 +02:00
parent 1cf525844b
commit c95d104245
20 changed files with 385 additions and 157 deletions
@@ -243,17 +243,24 @@ describe('serializeEdge', () => {
expect(result.animated).toBe(true)
})
it('serializes arrow markers', () => {
const edge = makeRfEdge({ data: { type: 'ethernet', marker_start: true, marker_end: true } })
it('serializes endpoint marker shapes', () => {
const edge = makeRfEdge({ data: { type: 'ethernet', marker_start: 'diamond', marker_end: 'arrow' } })
const result = serializeEdge(edge)
expect(result.marker_start).toBe(true)
expect(result.marker_end).toBe(true)
expect(result.marker_start).toBe('diamond')
expect(result.marker_end).toBe('arrow')
})
it('defaults arrow markers to false when absent', () => {
it('coerces legacy boolean markers to shape strings', () => {
const edge = makeRfEdge({ data: { type: 'ethernet', marker_start: true, marker_end: false } })
const result = serializeEdge(edge)
expect(result.marker_start).toBe('arrow')
expect(result.marker_end).toBe('none')
})
it('defaults endpoint markers to "none" when absent', () => {
const result = serializeEdge(makeRfEdge())
expect(result.marker_start).toBe(false)
expect(result.marker_end).toBe(false)
expect(result.marker_start).toBe('none')
expect(result.marker_end).toBe('none')
})
it('nulls optional fields when absent', () => {