M5: overlay positions relative to the layer that opens them

From the Figma forum request "Set overlay position relative to element,
not to page": an open overlay action takes anchor: screen (default) |
triggerInside | triggerOutside. With a trigger anchor the position preset
is measured on the layer that opens the overlay instead of the screen.
Outside puts top presets above it and bottom presets below it with the
edges lined up (a dropdown is Bottom left + Outside); Center centres it
on that layer. Manual is unchanged.

The interaction editor gets Relative to (Whole screen / Trigger layer)
and Inside / Outside. The player, the command (and so MCP and the
control channel) carry it; the command's params doc lists it. Figma has
no such setting, so .fig export writes it as a manual offset. Old files
and JSON without anchor read as screen and write unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JimKnoxx
2026-10-24 09:19:33 +02:00
committed by Albert Nimtz
co-authored by Claude Opus 5.5
parent 70227e30fc
commit 545cd630fd
36 changed files with 439 additions and 29 deletions
+15 -5
View File
@@ -448,6 +448,7 @@ struct Exporter<'d> {
tracks: BTreeMap<NodeId, (Vec<u32>, Vec<u32>)>,
aux: u32,
overlays: BTreeMap<NodeId, Overlay>,
trigger_size: [f64; 2],
/// Flow starting points: layer → (order, name).
starts: BTreeMap<NodeId, (usize, String)>,
/// Shared styles, variable collections and variables → local id of their node's GUID (session
@@ -486,6 +487,7 @@ impl<'d> Exporter<'d> {
tracks: BTreeMap::new(),
aux: 0,
overlays: BTreeMap::new(),
trigger_size: [0.0, 0.0],
starts: BTreeMap::new(),
style_ids: BTreeMap::new(),
set_ids: BTreeMap::new(),
@@ -574,8 +576,9 @@ impl<'d> Exporter<'d> {
_ => {}
}
for a in n.interactions.iter().flat_map(|it| figcraft_doc::proto::walk(&it.actions)) {
if let Action::OpenOverlay { dest, position, close_on_outside, background, .. } = a {
let o = Overlay { position: *position, close_on_outside: *close_on_outside, background: *background };
if let Action::OpenOverlay { dest, position, close_on_outside, background, anchor, .. } = a {
let position = if !anchor.is_screen() && *position != OverlayPosition::Manual { OverlayPosition::Manual } else { *position };
let o = Overlay { position, close_on_outside: *close_on_outside, background: *background };
match self.overlays.get(dest) {
Some(old) if (old.position, old.close_on_outside, old.background) != (o.position, o.close_on_outside, o.background) => {
self.warn("overlays opened with different settings keep the first one's");
@@ -2086,6 +2089,7 @@ impl<'d> Exporter<'d> {
/// Interactions, scrolling, flow starts and the overlay settings this layer is opened with.
fn prototype(&mut self, n: &Node, f: &mut Fields) {
self.trigger_size = [n.width, n.height];
let list: Vec<Kv> = n.interactions.iter().filter_map(|it| self.interaction(it)).collect();
if !list.is_empty() {
f.push(("prototypeInteractions", arr(list)));
@@ -2174,10 +2178,16 @@ impl<'d> Exporter<'d> {
}
Some(act)
}
Action::OpenOverlay { dest, position, offset, .. } => {
Action::OpenOverlay { dest, position, offset, anchor, .. } => {
let mut act = node(self, dest, "OVERLAY")?;
if let (Kv::Object(f), OverlayPosition::Manual, Some([x, y])) = (&mut act, position, offset) {
f.push(("overlayRelativePosition", o(vec![("x", fl(*x)), ("y", fl(*y))])));
let size = self.doc.node(*dest).map_or([0.0, 0.0], |d| [d.width, d.height]);
let offset = match anchor.offset(*position, self.trigger_size, size) {
Some(o) => Some(o),
None if *position == OverlayPosition::Manual => *offset,
None => None,
};
if let (Kv::Object(f), Some([x, y])) = (&mut act, offset) {
f.push(("overlayRelativePosition", o(vec![("x", fl(x)), ("y", fl(y))])));
}
Some(act)
}
+4 -1
View File
@@ -7,7 +7,9 @@ use std::collections::{BTreeMap, BTreeSet};
use figcraft_doc::color::{BlendMode, Color, ColorStop, Gradient, GradientKind, ImageScaleMode, Paint, PaintKind};
use figcraft_doc::geom::{Affine, Point};
use figcraft_doc::proto::{Action, Direction, Easing, Flow, Interaction, KeyMods, Overflow, OverlayPosition, ScrollPosition, Transition, Trigger};
use figcraft_doc::proto::{
Action, Direction, Easing, Flow, Interaction, KeyMods, Overflow, OverlayAnchor, OverlayPosition, ScrollPosition, Transition, Trigger,
};
use figcraft_doc::*;
use serde_json::Value;
@@ -511,6 +513,7 @@ impl Gen {
close_on_outside: true,
background: Some(Color::new(0.0, 0.0, 0.0, 0.5)),
offset: None,
anchor: OverlayAnchor::Screen,
},
3 => Action::SwapOverlay { dest },
4 => Action::ChangeTo { dest },
+2 -2
View File
@@ -4,7 +4,7 @@
use figcraft_doc::proto::{
Action, ConditionalBlock, DeviceRotation, Direction, Easing, Expr, ExprOp, Interaction, KeyMods, MAX_ACTION_DEPTH, MAX_EXPR_DEPTH, Overflow,
OverlayPosition, PrototypeSettings, ScrollPosition, Transition, Trigger, gamepad_button,
OverlayAnchor, OverlayPosition, PrototypeSettings, ScrollPosition, Transition, Trigger, gamepad_button,
};
use figcraft_doc::{Node, NodeId, VariableType, VariableValue};
use serde_json::{Value, json};
@@ -322,7 +322,7 @@ impl Ctx<'_> {
.and_then(|v| Some([num(v, "x")?, num(v, "y")?]))
.filter(|o| o.iter().all(|c| c.is_finite()))
.map(|o| o.map(|c| c.clamp(-MAX_OFFSET, MAX_OFFSET)));
Action::OpenOverlay { dest, position, close_on_outside, background, offset }
Action::OpenOverlay { dest, position, close_on_outside, background, offset, anchor: OverlayAnchor::Screen }
}
Some(_) => {
self.warn("prototype navigations of unknown types were skipped");