From f0a471a150f1f445280d2bd8da7b56c24517b4cd Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:14:53 -1000 Subject: [PATCH] feat: add RotationSignal for reactive marker rotation Add RotationSignal field to Marker so rotation updates reactively via signals, matching LngSignal/LatSignal. The markerEffectExpr now calls setRotation() when RotationSignal is present. Fix ship orientation in the example by subtracting 90 degrees from the north-based heading to account for the east-facing SVG bow. --- internal/examples/maplibre/main.go | 15 +++++++++------ maplibre/js.go | 27 ++++++++++++++++++--------- maplibre/maplibre.go | 7 ++++++- maplibre/types.go | 4 ++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/internal/examples/maplibre/main.go b/internal/examples/maplibre/main.go index 52d3d50..293e56d 100644 --- a/internal/examples/maplibre/main.go +++ b/internal/examples/maplibre/main.go @@ -172,19 +172,21 @@ func main() { // Animated container ships following waypoint routes shipNames := [3]string{"MSC Adriatica", "Evergreen Harmony", "Maersk Aurora"} - type shipSignals struct{ lng, lat *via.Signal } + type shipSignals struct{ lng, lat, rot *via.Signal } var ships [3]shipSignals fleet.mu.RLock() for i, s := range fleet.ships { ships[i].lng = c.Signal(s.lng) ships[i].lat = c.Signal(s.lat) + // SVG bow points right (east), so subtract 90° from the north-based heading. + ships[i].rot = c.Signal(s.heading() - 90) m.AddMarker(fmt.Sprintf("ship-%d", i), maplibre.Marker{ - LngSignal: ships[i].lng, - LatSignal: ships[i].lat, - Element: shipSVG, - Anchor: "center", - Rotation: s.heading(), + LngSignal: ships[i].lng, + LatSignal: ships[i].lat, + RotationSignal: ships[i].rot, + Element: shipSVG, + Anchor: "center", Popup: &maplibre.Popup{ Content: fmt.Sprintf("%s", shipNames[i]), }, @@ -216,6 +218,7 @@ func main() { for i, s := range fleet.ships { ships[i].lng.SetValue(s.lng) ships[i].lat.SetValue(s.lat) + ships[i].rot.SetValue(s.heading() - 90) } fleet.mu.RUnlock() diff --git a/maplibre/js.go b/maplibre/js.go index 7137daa..7ef2978 100644 --- a/maplibre/js.go +++ b/maplibre/js.go @@ -195,7 +195,9 @@ func markerBodyJS(mapID, markerID string, mk Marker) string { if mk.Anchor != "" { opts += fmt.Sprintf(`anchor:%s,`, jsonStr(mk.Anchor)) } - if mk.Rotation != 0 { + if mk.RotationSignal != nil { + opts += fmt.Sprintf(`rotation:%s,`, mk.RotationSignal.String()) + } else if mk.Rotation != 0 { opts += fmt.Sprintf(`rotation:%s,`, formatFloat(mk.Rotation)) } if mk.Draggable { @@ -250,14 +252,21 @@ func dragendHandlerJS(mapID, markerID string, mk Marker) string { func markerEffectExpr(mapID, markerID string, mk Marker) string { // Read signals before the guard so Datastar tracks them as dependencies // even when the map/marker hasn't loaded yet on first evaluation. - return fmt.Sprintf( - `var lng=$%s,lat=$%s;`+ - `var m=window.__via_maps&&window.__via_maps[%s];`+ - `if(m&&m._via_markers[%s]){`+ - `m._via_markers[%s].setLngLat([lng,lat])}`, - mk.LngSignal.ID(), mk.LatSignal.ID(), - jsonStr(mapID), jsonStr(markerID), jsonStr(markerID), - ) + var b strings.Builder + b.WriteString(fmt.Sprintf(`var lng=$%s,lat=$%s;`, mk.LngSignal.ID(), mk.LatSignal.ID())) + if mk.RotationSignal != nil { + b.WriteString(fmt.Sprintf(`var rot=$%s;`, mk.RotationSignal.ID())) + } + b.WriteString(fmt.Sprintf( + `var m=window.__via_maps&&window.__via_maps[%s];`+ + `if(m&&m._via_markers[%[2]s]){`+ + `m._via_markers[%[2]s].setLngLat([lng,lat])`, + jsonStr(mapID), jsonStr(markerID))) + if mk.RotationSignal != nil { + b.WriteString(fmt.Sprintf(`;m._via_markers[%s].setRotation(rot)`, jsonStr(markerID))) + } + b.WriteString(`}`) + return b.String() } // addMarkerJS generates a self-contained IIFE to add a marker post-render. diff --git a/maplibre/maplibre.go b/maplibre/maplibre.go index 852a345..0cc9556 100644 --- a/maplibre/maplibre.go +++ b/maplibre/maplibre.go @@ -122,13 +122,18 @@ func (m *Map) Element(extra ...h.H) h.H { } } - // Hidden inputs for signal-backed marker position writeback (drag → signal) + // Hidden inputs for signal-backed marker position/rotation writeback for _, me := range m.markers { if me.marker.LngSignal != nil && me.marker.LatSignal != nil { children = append(children, h.Input(h.Type("hidden"), me.marker.LngSignal.Bind()), h.Input(h.Type("hidden"), me.marker.LatSignal.Bind()), ) + if me.marker.RotationSignal != nil { + children = append(children, + h.Input(h.Type("hidden"), me.marker.RotationSignal.Bind()), + ) + } } } diff --git a/maplibre/types.go b/maplibre/types.go index 5772772..f45d190 100644 --- a/maplibre/types.go +++ b/maplibre/types.go @@ -313,6 +313,7 @@ type Marker struct { Anchor string // Rotation is clockwise degrees. Useful for directional icons (ships, vehicles). + // Ignored when RotationSignal is set. Rotation float64 // Signal-backed position. When set, signals drive marker position reactively. @@ -320,6 +321,9 @@ type Marker struct { // If Draggable is true, drag updates write back to these signals. LngSignal *via.Signal LatSignal *via.Signal + + // RotationSignal drives marker rotation reactively. When set, Rotation is ignored. + RotationSignal *via.Signal } // Popup describes a map popup.