mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 03:39:50 +00:00
Changing Preferences › Hyphenation › Exceptions bumped each open document's revision, but the caches of laid-out type outside the text crate were keyed on the font generation alone. The renderer kept the glyph geometry, raster-effect pixels, live-effect and envelope art, pattern tiles and placed symbol art of unchanged type objects, so hyphenated area type kept its old line breaks on the canvas. The hit-test art of blends and envelopes, the Fit to Artwork Bounds answer, the layout caches of the Type and Touch Type tools and the out port's overflow mark were keyed the same way. vectorcraft_text::layout_inputs() returns a number that changes with the fonts and with the hyphenation exceptions. Every one of those caches now keys on it instead of FontDb::generation(), and envelope ink uses it in place of the private envelope_ink_inputs(). The hit-test key hashes it together with the plug-in revision. Tests: a renderer that drew hyphenated area type, alone, with a drop shadow, in a group with a Transform effect, in a symbol instance and in an envelope, draws it as a new renderer does after the exceptions change; the out port of selected area type shows the overflow that the new exceptions cause. The render test helpers that run a step again when the fonts change during it now also run it again when the exceptions change.
201 lines
9.3 KiB
Rust
201 lines
9.3 KiB
Rust
use super::{FontDb, NodeId, NodeKind, Overlay, Point, Rect, Shape, TextKind, TextObject, ToolContext};
|
|
|
|
/// How far (screen px) the ports of area type sit from the frame's corners, along its sides: the
|
|
/// out port above the bottom-right corner on the right side, the in port below the top-left corner
|
|
/// on the left side, clear of the bounding box's corner handles, so both can be grabbed.
|
|
pub const PORT_RISE_PX: f64 = 12.0;
|
|
|
|
/// Half a port's side (screen px).
|
|
const PORT_HALF_PX: f64 = 4.5;
|
|
|
|
/// The red of the out port of text that doesn't fit.
|
|
const OVERFLOW_RED: [u8; 3] = [230, 30, 30];
|
|
|
|
/// One of the two ports of area type or type on a path: its story flows in through the in port
|
|
/// and on through the out port.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Port {
|
|
In,
|
|
Out,
|
|
}
|
|
|
|
/// What a port shows: empty when the frame is not threaded on that side (and, for the out port,
|
|
/// all its text is seen), an arrow when it is threaded to another frame there, a red "+" in the out
|
|
/// port when its text doesn't fit.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum PortState {
|
|
Empty,
|
|
Threaded,
|
|
Overflow,
|
|
}
|
|
|
|
/// Centre of `port` of `t`, `px` document units to the screen pixel: area type's on its frame's
|
|
/// sides by [`PORT_RISE_PX`] from the corners; type on a path's along its path, as far past its end
|
|
/// bracket (out) or before its start bracket (in), clear of the brackets, which drag.
|
|
pub fn port_centre(t: &TextObject, port: Port, px: f64) -> Option<Point> {
|
|
let rise = PORT_RISE_PX * px;
|
|
match &t.kind {
|
|
TextKind::Area { frame } => {
|
|
let b = frame.bounds()?;
|
|
// The corner the port sits by, and the far end of the side it sits on.
|
|
let (corner, far) = match port {
|
|
Port::Out => (Point::new(b.x1, b.y1), Point::new(b.x1, b.y0)),
|
|
Port::In => (Point::new(b.x0, b.y0), Point::new(b.x0, b.y1)),
|
|
};
|
|
let (corner, side) = (t.xf * corner, t.xf * far - t.xf * corner);
|
|
let len = side.hypot();
|
|
Some(if len > 1e-9 && len.is_finite() { corner + side * (rise.min(len / 2.0) / len) } else { corner })
|
|
}
|
|
TextKind::OnPath { path, .. } => {
|
|
let arc = vectorcraft_geom::ArcPath::new(path);
|
|
let (start, end) = t.kind.path_span()?;
|
|
if arc.is_empty() {
|
|
return None;
|
|
}
|
|
let (at, away) = match port {
|
|
Port::Out => (end, rise),
|
|
Port::In => (start, -rise),
|
|
};
|
|
let (p, dir) = arc.at(at * arc.len());
|
|
let (at, ahead) = (t.xf * p, t.xf * (p + dir) - t.xf * p);
|
|
let len = ahead.hypot();
|
|
Some(if len > 1e-9 && len.is_finite() { at + ahead * (away / len) } else { at })
|
|
}
|
|
TextKind::Point => None,
|
|
}
|
|
}
|
|
|
|
/// What the in and out ports of frame `id` of `d` show, its text `overflow`ing or not: an arrow
|
|
/// where a frame of its thread comes before (in) or after (out) it, else a red "+" in the out port
|
|
/// of text that doesn't fit, else nothing.
|
|
pub fn port_states(d: &vectorcraft_doc::Document, id: NodeId, overflow: bool) -> (PortState, PortState) {
|
|
let thread = d.text_threads.iter().find(|t| t.contains(&id));
|
|
let at = thread.and_then(|t| t.iter().position(|x| *x == id));
|
|
let input = if at.is_some_and(|i| i > 0) { PortState::Threaded } else { PortState::Empty };
|
|
let output = match (thread, at) {
|
|
(Some(t), Some(i)) if i + 1 < t.len() => PortState::Threaded,
|
|
_ if overflow => PortState::Overflow,
|
|
_ => PortState::Empty,
|
|
};
|
|
(input, output)
|
|
}
|
|
|
|
/// A port at `c` showing `state`, `px` document units to the screen pixel: a white box outlined in
|
|
/// `color` (the frame's layer colour; red for text that doesn't fit), with a red "+" or an arrow
|
|
/// pointing the way the story flows.
|
|
pub fn push_port(o: &mut Vec<Overlay>, c: Point, state: PortState, color: [u8; 3], px: f64) {
|
|
let h = PORT_HALF_PX * px;
|
|
let r = Rect::new(c.x - h, c.y - h, c.x + h, c.y + h);
|
|
let color = if state == PortState::Overflow { OVERFLOW_RED } else { color };
|
|
let corners = [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)];
|
|
o.push(Overlay::Highlight { quad: corners, color: [255, 255, 255, 255] });
|
|
o.push(Overlay::Path { path: r.to_path(0.1), color, width: 1.0, dashed: false });
|
|
let k = 2.5 * px;
|
|
let line = |a: Point, b: Point| Overlay::Line { a, b, color, dashed: false };
|
|
match state {
|
|
PortState::Empty => {}
|
|
PortState::Overflow => {
|
|
o.push(line(Point::new(c.x - k, c.y), Point::new(c.x + k, c.y)));
|
|
o.push(line(Point::new(c.x, c.y - k), Point::new(c.x, c.y + k)));
|
|
}
|
|
PortState::Threaded => {
|
|
let tip = Point::new(c.x + k, c.y);
|
|
o.push(line(Point::new(c.x - k, c.y), tip));
|
|
o.push(line(Point::new(c.x, c.y - k), tip));
|
|
o.push(line(Point::new(c.x, c.y + k), tip));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The in and out ports of area type or type on a path `t`, frame `id`, its text `overflow`ing or
|
|
/// not ([`port_states`]).
|
|
pub(super) fn ports(cx: &ToolContext, id: NodeId, t: &TextObject, overflow: bool, o: &mut Vec<Overlay>) {
|
|
let (input, output) = port_states(cx.doc, id, overflow);
|
|
let (color, px) = (cx.doc.layer_color(id), cx.tol(1.0));
|
|
for (port, state) in [(Port::In, input), (Port::Out, output)] {
|
|
if let Some(c) = port_centre(t, port, px) {
|
|
push_port(o, c, state, color, px);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The most selected area type and type on a path whose ports are drawn and take clicks: selecting
|
|
/// all of an opened PDF can select tens of thousands of type objects, whose ports would only bury
|
|
/// the canvas (and the GPU's buffers) in boxes.
|
|
pub const PORTS_MAX: usize = 1_000;
|
|
|
|
/// The text of `id` in `d` when it has ports: area type or type on a path a thread can hold, not in
|
|
/// perspective ([`crate::select::is_threadable`]). The one test for drawing ports, clicking them and
|
|
/// counting them toward [`PORTS_MAX`].
|
|
pub fn ported(d: &vectorcraft_doc::Document, id: NodeId) -> Option<&TextObject> {
|
|
match d.node(id).filter(|n| crate::select::is_threadable(n)).map(|n| &n.kind) {
|
|
Some(NodeKind::Text(t)) => Some(t),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Are at most [`PORTS_MAX`] of `selected` [`ported`] (counting stops past it), so that their
|
|
/// ports show?
|
|
pub fn ports_shown(d: &vectorcraft_doc::Document, selected: &[NodeId]) -> bool {
|
|
selected.iter().filter(|id| ported(d, **id).is_some()).take(PORTS_MAX + 1).count() <= PORTS_MAX
|
|
}
|
|
|
|
/// The selected area type or type on a path with a port under `p`, and which port: a click on the
|
|
/// out port loads the Selection tool with its text to thread into another frame after it, a click
|
|
/// on the in port to thread into one before it. Every such frame has both ports, threaded,
|
|
/// overflowing or not, while their ports show ([`PORTS_MAX`]).
|
|
pub(crate) fn port_at(cx: &ToolContext, p: Point) -> Option<(NodeId, Port)> {
|
|
port_near(cx, p).map(|(id, port, _)| (id, port))
|
|
}
|
|
|
|
/// [`port_at`], with how far (document units) the port's centre is from `p`: the nearest port
|
|
/// within reach, of every selected frame (ports of overlapping frames can both be in reach).
|
|
pub(crate) fn port_near(cx: &ToolContext, p: Point) -> Option<(NodeId, Port, f64)> {
|
|
if !ports_shown(cx.doc, &cx.selection.objects) {
|
|
return None;
|
|
}
|
|
let h = cx.tol(PORT_HALF_PX + 1.0);
|
|
let near = |c: Point| (c.x - p.x).abs() <= h && (c.y - p.y).abs() <= h;
|
|
cx.selection
|
|
.objects
|
|
.iter()
|
|
.filter_map(|id| ported(cx.doc, *id).map(|t| (*id, t)))
|
|
.flat_map(|(id, t)| [Port::Out, Port::In].into_iter().filter_map(move |port| port_centre(t, port, cx.tol(1.0)).map(|c| (id, port, c))))
|
|
.filter(|(_, _, c)| near(*c))
|
|
.map(|(id, port, c)| (id, port, c.distance(p)))
|
|
.min_by(|a, b| a.2.total_cmp(&b.2))
|
|
}
|
|
|
|
/// Which selected text overflows, for [`thread_ports`]: each text object's layout verdict kept
|
|
/// with the object and the layout inputs it was made with (fonts and hyphenation exceptions,
|
|
/// [`vectorcraft_text::layout_inputs`]), as overlays run every frame.
|
|
#[derive(Default)]
|
|
pub struct PortMarks {
|
|
seen: std::collections::HashMap<NodeId, ((u64, TextObject), bool)>,
|
|
}
|
|
|
|
/// The in and out ports of the selected area type and type on a path, for every tool but the Type
|
|
/// tools (they draw their own): each tells whether the frame is threaded on that side and the out
|
|
/// port whether text is hidden by the size of its frame, whatever tool is active. None when more
|
|
/// than [`PORTS_MAX`] of them are selected.
|
|
pub fn thread_ports(marks: &mut PortMarks, cx: &ToolContext) -> Vec<Overlay> {
|
|
let fonts = vectorcraft_text::layout_inputs();
|
|
let mut o = Vec::new();
|
|
let mut kept = std::collections::HashMap::new();
|
|
if !ports_shown(cx.doc, &cx.selection.objects) {
|
|
marks.seen = kept;
|
|
return o;
|
|
}
|
|
for id in &cx.selection.objects {
|
|
let Some(t) = ported(cx.doc, *id) else { continue };
|
|
let over = match marks.seen.remove(id) {
|
|
Some(((g, was), over)) if g == fonts && &was == t => over,
|
|
_ => vectorcraft_text::layout(FontDb::global(), t).overflow,
|
|
};
|
|
ports(cx, *id, t, over, &mut o);
|
|
kept.insert(*id, ((fonts, t.clone()), over));
|
|
}
|
|
marks.seen = kept;
|
|
o
|
|
}
|