mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 19:38:12 +08:00
Properly track the Text node's text frame via the attribute system to fix misalignment (#4097)
* Compensate for upstream row-0 transform absorption in viewport-space 'TransformSet' * Add 'editor:text_frame' row attribute so the Text tool's drag cage tracks multi-row text * "Separate Glyph Elements" -> "Separate Glyphs" * Improve artboard migration robustness from older documents * Code review * Make the tools visualize the text frame based on attribute not upstream node
This commit is contained in:
@@ -218,6 +218,9 @@ pub enum DocumentMessage {
|
|||||||
UpdateOutlines {
|
UpdateOutlines {
|
||||||
outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||||
},
|
},
|
||||||
|
UpdateTextFrames {
|
||||||
|
text_frames: HashMap<NodeId, DAffine2>,
|
||||||
|
},
|
||||||
UpdateClipTargets {
|
UpdateClipTargets {
|
||||||
clip_targets: HashSet<NodeId>,
|
clip_targets: HashSet<NodeId>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1283,6 +1283,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
.collect();
|
.collect();
|
||||||
self.network_interface.update_outlines(layer_outlines);
|
self.network_interface.update_outlines(layer_outlines);
|
||||||
}
|
}
|
||||||
|
DocumentMessage::UpdateTextFrames { text_frames } => {
|
||||||
|
let layer_text_frames = text_frames
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||||
|
.filter_map(|(node_id, frame)| {
|
||||||
|
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||||
|
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||||
|
(layer, frame)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.network_interface.update_text_frames(layer_text_frames);
|
||||||
|
}
|
||||||
DocumentMessage::UpdateClipTargets { clip_targets } => {
|
DocumentMessage::UpdateClipTargets { clip_targets } => {
|
||||||
self.network_interface.update_clip_targets(clip_targets);
|
self.network_interface.update_clip_targets(clip_targets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -652,15 +652,30 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
// Get the existing upstream Transform node, if present
|
// Get the existing upstream Transform node, if present
|
||||||
let transform_node_id = self.existing_network_node_id("Transform", false);
|
let transform_node_id = self.existing_network_node_id("Transform", false);
|
||||||
|
|
||||||
// Get a transform appropriate for the requested space
|
// Compute the Transform node value so `transform_to_viewport` matches the target after re-render
|
||||||
let to_transform = match transform_in {
|
let final_transform = match transform_in {
|
||||||
TransformIn::Local => DAffine2::IDENTITY,
|
TransformIn::Local => transform,
|
||||||
TransformIn::Scope { scope } => scope,
|
TransformIn::Scope { scope } => scope * transform,
|
||||||
TransformIn::Viewport => self.network_interface.document_metadata().downstream_transform_to_viewport(self.layer_node.unwrap()).inverse(),
|
TransformIn::Viewport => {
|
||||||
|
let Some(layer) = self.layer_node else { return };
|
||||||
|
let metadata = self.network_interface.document_metadata();
|
||||||
|
let parent_inverse = metadata.downstream_transform_to_viewport(layer).inverse();
|
||||||
|
|
||||||
|
// Compensate for item 0's baseline offset (multi-item Text only) so the layer doesn't jump by it.
|
||||||
|
// Gated on `text_frames` because metadata can be stale mid-handler.
|
||||||
|
if metadata.text_frames.contains_key(&layer) {
|
||||||
|
let local_transform = metadata.local_transforms.get(&layer.to_node()).copied().unwrap_or(DAffine2::IDENTITY);
|
||||||
|
let current_transform_node_value = transform_node_id
|
||||||
|
.and_then(|id| self.network_interface.document_network().nodes.get(&id))
|
||||||
|
.map(|node| transform_utils::get_current_transform(&node.inputs))
|
||||||
|
.unwrap_or(DAffine2::IDENTITY);
|
||||||
|
parent_inverse * transform * local_transform.inverse() * current_transform_node_value
|
||||||
|
} else {
|
||||||
|
parent_inverse * transform
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the transform value to the Transform node
|
|
||||||
let final_transform = to_transform * transform;
|
|
||||||
self.transform_set_direct(final_transform, skip_rerender, transform_node_id);
|
self.transform_set_direct(final_transform, skip_rerender, transform_node_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub struct DocumentMetadata {
|
|||||||
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
|
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
|
||||||
/// nodes with an `editor:click_target` override still outline the precise geometry.
|
/// nodes with an `editor:click_target` override still outline the precise geometry.
|
||||||
pub outlines: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>,
|
pub outlines: HashMap<LayerNodeIdentifier, Vec<Arc<ClickTarget>>>,
|
||||||
|
/// Per-layer text frame from item 0's `editor:text_frame` attribute (`DAffine2` mapping the (0, 0)-(1, 1) unit square onto the frame).
|
||||||
|
/// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage.
|
||||||
|
pub text_frames: HashMap<LayerNodeIdentifier, DAffine2>,
|
||||||
pub clip_targets: HashSet<NodeId>,
|
pub clip_targets: HashSet<NodeId>,
|
||||||
pub vector_modify: HashMap<NodeId, Vector>,
|
pub vector_modify: HashMap<NodeId, Vector>,
|
||||||
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
|
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
|
||||||
@@ -163,6 +166,12 @@ impl DocumentMetadata {
|
|||||||
self.outlines.get(&layer).or_else(|| self.click_targets.get(&layer)).map(|v| v.as_slice())
|
self.outlines.get(&layer).or_else(|| self.click_targets.get(&layer)).map(|v| v.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to treat this layer as a text layer for tool UI. Checks the surfaced `editor:text_frame`
|
||||||
|
/// metadata rather than upstream topology, so layers that strip the attribute downstream correctly drop out.
|
||||||
|
pub fn is_text_layer(&self, layer: LayerNodeIdentifier) -> bool {
|
||||||
|
self.text_frames.contains_key(&layer)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the bounding box of the click target of the specified layer in the specified transform space
|
/// Get the bounding box of the click target of the specified layer in the specified transform space
|
||||||
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||||
self.visual_targets(layer)?
|
self.visual_targets(layer)?
|
||||||
|
|||||||
@@ -3277,6 +3277,7 @@ impl NodeNetworkInterface {
|
|||||||
self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node));
|
self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node));
|
||||||
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||||
self.document_metadata.outlines.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
self.document_metadata.outlines.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||||
|
self.document_metadata.text_frames.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the cached transforms of the layers
|
/// Update the cached transforms of the layers
|
||||||
@@ -3300,6 +3301,12 @@ impl NodeNetworkInterface {
|
|||||||
self.document_metadata.outlines = new_outlines;
|
self.document_metadata.outlines = new_outlines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the cached per-layer 'Text' node text frames in row-local space (as `DAffine2`
|
||||||
|
/// mapping the unit square onto the frame).
|
||||||
|
pub fn update_text_frames(&mut self, new_text_frames: HashMap<LayerNodeIdentifier, DAffine2>) {
|
||||||
|
self.document_metadata.text_frames = new_text_frames;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the cached clip targets of the layers
|
/// Update the cached clip targets of the layers
|
||||||
pub fn update_clip_targets(&mut self, new_clip_targets: HashSet<NodeId>) {
|
pub fn update_clip_targets(&mut self, new_clip_targets: HashSet<NodeId>) {
|
||||||
self.document_metadata.clip_targets = new_clip_targets;
|
self.document_metadata.clip_targets = new_clip_targets;
|
||||||
|
|||||||
@@ -1086,6 +1086,18 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally
|
||||||
|
if let DocumentNodeImplementation::Network(inner) = &node.implementation
|
||||||
|
&& inner
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.any(|n| matches!(&n.implementation, DocumentNodeImplementation::ProtoNode(id) if id.as_str().contains("LegacyLayerExtend") || id.as_str().contains("legacy_layer_extend")))
|
||||||
|
&& let Some(reference) = document.network_interface.reference(node_id, network_path)
|
||||||
|
&& let Some(node_definition) = resolve_document_node_type(&reference)
|
||||||
|
{
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template());
|
||||||
|
}
|
||||||
|
|
||||||
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` as their call argument
|
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` as their call argument
|
||||||
if node.call_argument == graph_craft::concrete!(()) || node.call_argument == graph_craft::concrete!(graphene_std::transform::Footprint) {
|
if node.call_argument == graph_craft::concrete!(()) || node.call_argument == graph_craft::concrete!(graphene_std::transform::Footprint) {
|
||||||
document.network_interface.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context));
|
document.network_interface.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context));
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
|||||||
let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else {
|
let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Some(&TaggedValue::Bool(per_glyph_items)) = inputs[graphene_std::text::text::SeparateGlyphElementsInput::INDEX].as_value() else {
|
let Some(&TaggedValue::Bool(per_glyph_items)) = inputs[graphene_std::text::text::SeparateGlyphsInput::INDEX].as_value() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -64,21 +64,17 @@ where
|
|||||||
|
|
||||||
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
|
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
|
||||||
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
|
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
|
||||||
let Some((text, font, typesetting, per_glyph_items)) = get_text(layer, &document.network_interface) else {
|
// Use the `editor:text_frame` attribute if available (handles multi-item glyphs and the 'Index Elements' node)
|
||||||
|
if let Some(&frame) = document.metadata().text_frames.get(&layer) {
|
||||||
|
return frame * Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: recompute from text content (e.g. layer hasn't rendered yet)
|
||||||
|
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface) else {
|
||||||
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
||||||
};
|
};
|
||||||
|
|
||||||
let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false);
|
let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false);
|
||||||
|
Quad::from_box([DVec2::ZERO, far])
|
||||||
// TODO: Once the `Table` refactor is complete and per_glyph_items can be removed (since it'll be the default),
|
|
||||||
// TODO: remove this because the top of the dashed bounding overlay should no longer be based on the first line's baseline.
|
|
||||||
let vertical_offset = if per_glyph_items {
|
|
||||||
DVec2::NEG_Y * typesetting.font_size * (1. + (typesetting.line_height_ratio - 1.) / 2.)
|
|
||||||
} else {
|
|
||||||
DVec2::ZERO
|
|
||||||
};
|
|
||||||
|
|
||||||
Quad::from_box([DVec2::ZERO + vertical_offset, far + vertical_offset])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector: &Vector, prefer_handle_direction: bool) -> Option<f64> {
|
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector: &Vector, prefer_handle_direction: bool) -> Option<f64> {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use super::tool_prelude::*;
|
|||||||
use crate::consts::*;
|
use crate::consts::*;
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
|
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
|
||||||
@@ -14,7 +13,6 @@ use crate::messages::preferences::SelectionMode;
|
|||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
|
||||||
use crate::messages::tool::common_functionality::measure;
|
use crate::messages::tool::common_functionality::measure;
|
||||||
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
||||||
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
||||||
@@ -631,7 +629,7 @@ impl Fsm for SelectToolFsmState {
|
|||||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
||||||
|
|
||||||
if is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) {
|
if document.metadata().is_text_layer(layer) {
|
||||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache);
|
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache);
|
||||||
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
||||||
}
|
}
|
||||||
@@ -1574,7 +1572,7 @@ impl Fsm for SelectToolFsmState {
|
|||||||
|
|
||||||
if let Some(layer) = selected_layers.next() {
|
if let Some(layer) = selected_layers.next() {
|
||||||
// Check that only one layer is selected
|
// Check that only one layer is selected
|
||||||
if selected_layers.next().is_none() && is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) {
|
if selected_layers.next().is_none() && document.metadata().is_text_layer(layer) {
|
||||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
||||||
responses.add(TextToolMessage::EditSelected);
|
responses.add(TextToolMessage::EditSelected);
|
||||||
}
|
}
|
||||||
@@ -1934,7 +1932,7 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer:
|
|||||||
/// Called when a double click on a layer in deep select mode.
|
/// Called when a double click on a layer in deep select mode.
|
||||||
/// If the layer is text, the text tool is selected.
|
/// If the layer is text, the text tool is selected.
|
||||||
fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, responses: &mut VecDeque<Message>) {
|
fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface, responses: &mut VecDeque<Message>) {
|
||||||
if is_layer_fed_by_node_of_name(layer, network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) {
|
if network_interface.document_metadata().is_text_layer(layer) {
|
||||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
||||||
responses.add(TextToolMessage::EditSelected);
|
responses.add(TextToolMessage::EditSelected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
use super::tool_prelude::*;
|
use super::tool_prelude::*;
|
||||||
use crate::consts::{COLOR_OVERLAY_BLUE_05, COLOR_OVERLAY_RED, DRAG_THRESHOLD};
|
use crate::consts::{COLOR_OVERLAY_BLUE_05, COLOR_OVERLAY_RED, DRAG_THRESHOLD};
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::resize::Resize;
|
use crate::messages::tool::common_functionality::resize::Resize;
|
||||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
|
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
|
||||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||||
@@ -518,16 +517,12 @@ impl TextToolData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
|
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
|
||||||
document
|
document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).find(|&layer| {
|
||||||
.metadata()
|
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache);
|
||||||
.all_layers()
|
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
||||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)))
|
|
||||||
.find(|&layer| {
|
|
||||||
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache);
|
|
||||||
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
|
||||||
|
|
||||||
transformed_quad.contains(mouse)
|
transformed_quad.contains(mouse)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
||||||
@@ -550,7 +545,7 @@ fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdent
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) {
|
if !document.metadata().is_text_layer(layer) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +607,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
// TODO: implement bounding box for multiple layers
|
// TODO: implement bounding box for multiple layers
|
||||||
let selected = document.network_interface.selected_nodes();
|
let selected = document.network_interface.selected_nodes();
|
||||||
let mut all_layers = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
let mut all_layers = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
||||||
let layer = all_layers.find(|layer| is_layer_fed_by_node_of_name(*layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)));
|
let layer = all_layers.find(|&layer| document.metadata().is_text_layer(layer));
|
||||||
let bounds = layer.map(|layer| text_bounding_box(layer, document, font_cache));
|
let bounds = layer.map(|layer| text_bounding_box(layer, document, font_cache));
|
||||||
let layer_transform = layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or(DAffine2::IDENTITY);
|
let layer_transform = layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or(DAffine2::IDENTITY);
|
||||||
|
|
||||||
@@ -673,7 +668,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
|
|
||||||
let selected = document.network_interface.selected_nodes();
|
let selected = document.network_interface.selected_nodes();
|
||||||
let mut all_selected = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
let mut all_selected = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
||||||
let selected = all_selected.find(|layer| is_layer_fed_by_node_of_name(*layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)));
|
let selected = all_selected.find(|&layer| document.metadata().is_text_layer(layer));
|
||||||
|
|
||||||
if dragging_bounds.is_some() {
|
if dragging_bounds.is_some() {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
@@ -712,7 +707,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
// This ensures the cursor only changes if a layer is selected
|
// This ensures the cursor only changes if a layer is selected
|
||||||
let selected = document.network_interface.selected_nodes();
|
let selected = document.network_interface.selected_nodes();
|
||||||
let mut all_selected = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
let mut all_selected = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
||||||
let layer = all_selected.find(|&layer| is_layer_fed_by_node_of_name(layer, &document.network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)));
|
let layer = all_selected.find(|&layer| document.metadata().is_text_layer(layer));
|
||||||
|
|
||||||
let mut cursor = tool_data
|
let mut cursor = tool_data
|
||||||
.bounding_box_manager
|
.bounding_box_manager
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ impl NodeGraphExecutor {
|
|||||||
first_element_source_id,
|
first_element_source_id,
|
||||||
click_targets,
|
click_targets,
|
||||||
outlines,
|
outlines,
|
||||||
|
text_frames,
|
||||||
clip_targets,
|
clip_targets,
|
||||||
vector_data,
|
vector_data,
|
||||||
backgrounds: _,
|
backgrounds: _,
|
||||||
@@ -431,6 +432,7 @@ impl NodeGraphExecutor {
|
|||||||
});
|
});
|
||||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||||
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
||||||
|
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
||||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||||
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
||||||
responses.add(DocumentMessage::RenderScrollbars);
|
responses.add(DocumentMessage::RenderScrollbars);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ use std::any::TypeId;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
pub use table::{
|
pub use table::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
|
||||||
ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
|
ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
pub use tsify;
|
pub use tsify;
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers";
|
|||||||
/// by clicking anywhere within their bounds, not just the filled letterform.
|
/// by clicking anywhere within their bounds, not just the filled letterform.
|
||||||
pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target";
|
pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target";
|
||||||
|
|
||||||
|
/// `DAffine2` mapping the unit square `[(0, 0), (1, 1)]` (top-left convention) onto the 'Text'
|
||||||
|
/// node's text frame in this row's local space. Each row carries the frame relative to its own
|
||||||
|
/// glyph origin so it survives `Index Elements` filtering. The Text tool reads this to position
|
||||||
|
/// its drag cage. Stored as an affine to allow non-axis-aligned frames in the future.
|
||||||
|
pub const ATTR_EDITOR_TEXT_FRAME: &str = "editor:text_frame";
|
||||||
|
|
||||||
/// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes).
|
/// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes).
|
||||||
pub const ATTR_START: &str = "start";
|
pub const ATTR_START: &str = "start";
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use core_types::table::{Table, TableRow};
|
|||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::{NodeId, generate_uuid};
|
use core_types::uuid::{NodeId, generate_uuid};
|
||||||
use core_types::{
|
use core_types::{
|
||||||
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_LOCATION,
|
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME,
|
||||||
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
|
ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
|
||||||
};
|
};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -327,6 +327,9 @@ pub struct RenderMetadata {
|
|||||||
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
|
/// Source-geometry outlines for hover/selection overlays, separate from `click_targets` so
|
||||||
/// nodes with an `editor:click_target` override still outline the precise geometry.
|
/// nodes with an `editor:click_target` override still outline the precise geometry.
|
||||||
pub outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
pub outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||||
|
/// Per-layer text frame from row 0's `editor:text_frame` attribute.
|
||||||
|
/// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage.
|
||||||
|
pub text_frames: HashMap<NodeId, DAffine2>,
|
||||||
pub clip_targets: HashSet<NodeId>,
|
pub clip_targets: HashSet<NodeId>,
|
||||||
pub vector_data: HashMap<NodeId, Arc<Vector>>,
|
pub vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||||
pub backgrounds: Vec<Background>,
|
pub backgrounds: Vec<Background>,
|
||||||
@@ -349,6 +352,7 @@ impl RenderMetadata {
|
|||||||
first_element_source_id,
|
first_element_source_id,
|
||||||
click_targets,
|
click_targets,
|
||||||
outlines,
|
outlines,
|
||||||
|
text_frames,
|
||||||
clip_targets,
|
clip_targets,
|
||||||
vector_data,
|
vector_data,
|
||||||
backgrounds,
|
backgrounds,
|
||||||
@@ -358,6 +362,7 @@ impl RenderMetadata {
|
|||||||
first_element_source_id.extend(other.first_element_source_id.iter());
|
first_element_source_id.extend(other.first_element_source_id.iter());
|
||||||
click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone())));
|
click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone())));
|
||||||
outlines.extend(other.outlines.iter().map(|(k, v)| (*k, v.clone())));
|
outlines.extend(other.outlines.iter().map(|(k, v)| (*k, v.clone())));
|
||||||
|
text_frames.extend(other.text_frames.iter());
|
||||||
clip_targets.extend(other.clip_targets.iter());
|
clip_targets.extend(other.clip_targets.iter());
|
||||||
vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone())));
|
vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone())));
|
||||||
|
|
||||||
@@ -1366,7 +1371,7 @@ impl Render for Table<Vector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
|
||||||
// Aggregate all items' targets per element_id so multi-item tables (e.g. 'Text' node with "Separate Glyph Elements" active) produce hit areas for every glyph.
|
// Aggregate all items' targets per element_id so multi-item tables (e.g. 'Text' node with "Separate Glyphs" active) produce hit areas for every glyph.
|
||||||
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
|
// Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`.
|
||||||
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
||||||
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
||||||
@@ -1414,6 +1419,11 @@ impl Render for Table<Vector> {
|
|||||||
// Source geometry (not the click-target override) so editing tools work on letterforms.
|
// Source geometry (not the click-target override) so editing tools work on letterforms.
|
||||||
// Only item 0 is recorded since editing tools can only target a single item currently.
|
// Only item 0 is recorded since editing tools can only target a single item currently.
|
||||||
metadata.vector_data.entry(element_id).or_insert_with(|| Arc::new(source.clone()));
|
metadata.vector_data.entry(element_id).or_insert_with(|| Arc::new(source.clone()));
|
||||||
|
|
||||||
|
// Surface `editor:text_frame` for the Text tool's drag cage
|
||||||
|
if let Some(&frame) = self.attribute::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index) {
|
||||||
|
metadata.text_frames.entry(element_id).or_insert(frame);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
|
// If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation,
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ fn text<'i: 'n>(
|
|||||||
/// To have an effect on a single line of text, *Max Width* must be set.
|
/// To have an effect on a single line of text, *Max Width* must be set.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_align")]
|
#[widget(ParsedWidgetOverride::Custom = "text_align")]
|
||||||
align: TextAlign,
|
align: TextAlign,
|
||||||
/// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced.
|
/// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced.
|
||||||
separate_glyph_elements: bool,
|
separate_glyphs: bool,
|
||||||
) -> Table<Vector> {
|
) -> Table<Vector> {
|
||||||
let typesetting = TypesettingConfig {
|
let typesetting = TypesettingConfig {
|
||||||
font_size: size,
|
font_size: size,
|
||||||
@@ -72,5 +72,5 @@ fn text<'i: 'n>(
|
|||||||
align,
|
align,
|
||||||
};
|
};
|
||||||
|
|
||||||
to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyph_elements)
|
to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyphs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use core_types::table::{Table, TableRow};
|
use core_types::table::{Table, TableRow};
|
||||||
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_TRANSFORM};
|
use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_TEXT_FRAME, ATTR_TRANSFORM};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use parley::GlyphRun;
|
use parley::GlyphRun;
|
||||||
use skrifa::GlyphId;
|
use skrifa::GlyphId;
|
||||||
@@ -15,19 +15,26 @@ pub struct PathBuilder {
|
|||||||
origin: DVec2,
|
origin: DVec2,
|
||||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||||
pub vector_table: Table<Vector>,
|
pub vector_table: Table<Vector>,
|
||||||
/// Per-glyph bbox rectangles collected in single-row mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
|
/// Per-glyph bbox rectangles collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
|
||||||
merged_click_target_subpaths: Vec<Subpath<PointId>>,
|
merged_click_target_subpaths: Vec<Subpath<PointId>>,
|
||||||
|
/// Text frame size, stamped per item as `ATTR_EDITOR_TEXT_FRAME` relative to each item's origin.
|
||||||
|
text_frame_size: DVec2,
|
||||||
|
/// First glyph's baseline offset (pre-height-filter). Used for the empty placeholder item so
|
||||||
|
/// `local_transforms` stays stable when all glyphs are clipped during a resize drag.
|
||||||
|
first_glyph_offset: DVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
id: PointId,
|
id: PointId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathBuilder {
|
impl PathBuilder {
|
||||||
pub fn new(per_glyph_items: bool, scale: f64) -> Self {
|
pub fn new(per_glyph_items: bool, scale: f64, text_frame_size: DVec2, first_glyph_offset: DVec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_subpath: Subpath::new(Vec::new(), false),
|
current_subpath: Subpath::new(Vec::new(), false),
|
||||||
glyph_subpaths: Vec::new(),
|
glyph_subpaths: Vec::new(),
|
||||||
vector_table: if per_glyph_items { Table::new() } else { Table::new_from_element(Vector::default()) },
|
vector_table: if per_glyph_items { Table::new() } else { Table::new_from_element(Vector::default()) },
|
||||||
merged_click_target_subpaths: Vec::new(),
|
merged_click_target_subpaths: Vec::new(),
|
||||||
|
text_frame_size,
|
||||||
|
first_glyph_offset,
|
||||||
scale,
|
scale,
|
||||||
id: PointId::ZERO,
|
id: PointId::ZERO,
|
||||||
origin: DVec2::default(),
|
origin: DVec2::default(),
|
||||||
@@ -58,12 +65,18 @@ impl PathBuilder {
|
|||||||
let glyph_bbox_rectangle = subpaths_bounding_box(&self.glyph_subpaths).map(|[min, max]| Subpath::new_rectangle(min, max));
|
let glyph_bbox_rectangle = subpaths_bounding_box(&self.glyph_subpaths).map(|[min, max]| Subpath::new_rectangle(min, max));
|
||||||
|
|
||||||
if per_glyph_items {
|
if per_glyph_items {
|
||||||
let row = TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)).with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset));
|
// Frame in item-local space: top-left at `-glyph_offset` so the item transform cancels it
|
||||||
let row = match glyph_bbox_rectangle {
|
// back to the layer-local frame origin, regardless of which glyph survived
|
||||||
Some(rect) => row.with_attribute(ATTR_EDITOR_CLICK_TARGET, Vector::from_subpaths([rect], false)),
|
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset);
|
||||||
None => row,
|
|
||||||
|
let item = TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false))
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset))
|
||||||
|
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
|
||||||
|
let item = match glyph_bbox_rectangle {
|
||||||
|
Some(rect) => item.with_attribute(ATTR_EDITOR_CLICK_TARGET, Vector::from_subpaths([rect], false)),
|
||||||
|
None => item,
|
||||||
};
|
};
|
||||||
self.vector_table.push(row);
|
self.vector_table.push(item);
|
||||||
} else {
|
} else {
|
||||||
for subpath in self.glyph_subpaths.drain(..) {
|
for subpath in self.glyph_subpaths.drain(..) {
|
||||||
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Table<Vector>` item
|
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Table<Vector>` item
|
||||||
@@ -130,16 +143,31 @@ impl PathBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalize(mut self) -> Table<Vector> {
|
pub fn finalize(mut self) -> Table<Vector> {
|
||||||
|
// Empty table = all glyphs clipped by height. Create a placeholder with the same item-0
|
||||||
|
// transform a populated table would have so `local_transforms` stays stable mid-drag.
|
||||||
|
// TODO: Remove this hack and move the attribute up to the parent return value when <https://github.com/GraphiteEditor/Graphite/issues/3779> is done.
|
||||||
if self.vector_table.is_empty() {
|
if self.vector_table.is_empty() {
|
||||||
self.vector_table = Table::new_from_element(Vector::default());
|
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -self.first_glyph_offset);
|
||||||
|
let item = TableRow::new_from_element(Vector::default())
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(self.first_glyph_offset))
|
||||||
|
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
|
||||||
|
self.vector_table.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// With "Separate Glyph Elements" inactive, combine the accumulated per-glyph AABBs as one override `Vector`
|
// With "Separate Glyphs" inactive, combine the accumulated per-glyph AABBs as one override `Vector`
|
||||||
if !self.merged_click_target_subpaths.is_empty() {
|
if !self.merged_click_target_subpaths.is_empty() {
|
||||||
self.vector_table
|
self.vector_table
|
||||||
.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(self.merged_click_target_subpaths, false));
|
.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(self.merged_click_target_subpaths, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity)
|
||||||
|
let frame = DAffine2::from_scale(self.text_frame_size);
|
||||||
|
for index in 0..self.vector_table.len() {
|
||||||
|
if self.vector_table.attribute::<DAffine2>(ATTR_EDITOR_TEXT_FRAME, index).is_none() {
|
||||||
|
self.vector_table.set_attribute(ATTR_EDITOR_TEXT_FRAME, index, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.vector_table
|
self.vector_table
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,23 @@ impl TextContext {
|
|||||||
return Table::new_from_element(Vector::default());
|
return Table::new_from_element(Vector::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut path_builder = PathBuilder::new(per_glyph_items, layout.scale() as f64);
|
let text_frame_size = DVec2::new(
|
||||||
|
typesetting.max_width.unwrap_or_else(|| layout.full_width() as f64),
|
||||||
|
typesetting.max_height.unwrap_or_else(|| layout.height() as f64),
|
||||||
|
);
|
||||||
|
|
||||||
|
// First glyph offset (pre-height-filter) so the empty placeholder item in `per_glyph_items`
|
||||||
|
// mode keeps the same item 0's transform, preventing `local_transforms` from jumping mid-drag
|
||||||
|
let first_glyph_offset = layout
|
||||||
|
.lines()
|
||||||
|
.flat_map(|line| line.items())
|
||||||
|
.find_map(|item| match item {
|
||||||
|
PositionedLayoutItem::GlyphRun(run) => run.glyphs().next().map(|glyph| DVec2::new((run.offset() + glyph.x) as f64, (run.baseline() - glyph.y) as f64)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut path_builder = PathBuilder::new(per_glyph_items, layout.scale() as f64, text_frame_size, first_glyph_offset);
|
||||||
|
|
||||||
for line in layout.lines() {
|
for line in layout.lines() {
|
||||||
for item in line.items() {
|
for item in line.items() {
|
||||||
|
|||||||
Reference in New Issue
Block a user