mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 05:18:19 +00:00
Added text as instances and contains nodes
This commit is contained in:
parent
914360551f
commit
986759597b
4 changed files with 260 additions and 9 deletions
|
@ -1,6 +1,8 @@
|
|||
use crate::vector::PointId;
|
||||
use crate::instances::Instance;
|
||||
use crate::vector::{PointId, VectorData, VectorDataTable};
|
||||
use crate::{GraphicElement, GraphicGroupTable};
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use glam::DVec2;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
|
||||
|
@ -247,3 +249,151 @@ fn split_words_including_spaces() {
|
|||
assert_eq!(split_words.next(), Some("."));
|
||||
assert_eq!(split_words.next(), None);
|
||||
}
|
||||
|
||||
// Builder specifically for generating glyph paths relative to (0,0)
|
||||
struct GlyphBuilder {
|
||||
current_subpath: Subpath<PointId>,
|
||||
other_subpaths: Vec<Subpath<PointId>>,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl GlyphBuilder {
|
||||
// Calculates point relative to glyph origin, scaled and adjusted for ascender
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
DVec2::new(x as f64, self.ascender - y as f64) * self.scale
|
||||
}
|
||||
|
||||
// Extracts the generated subpaths and resets the builder state
|
||||
fn take_subpaths(&mut self) -> Vec<Subpath<PointId>> {
|
||||
let mut subpaths = std::mem::take(&mut self.other_subpaths);
|
||||
if !self.current_subpath.is_empty() {
|
||||
subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
subpaths
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for GlyphBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if !self.current_subpath.is_empty() {
|
||||
self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.current_subpath.set_closed(true);
|
||||
self.other_subpaths.push(core::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a string into a graphic group, where each element is a single character glyph as VectorData.
|
||||
pub fn to_group(str: &str, buzz_face: Option<rustybuzz::Face>, typesetting: TypesettingConfig) -> GraphicGroupTable {
|
||||
let Some(buzz_face) = buzz_face else { return GraphicGroupTable::empty() };
|
||||
let space_glyph = buzz_face.glyph_index(' ');
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio);
|
||||
let ascender = (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale;
|
||||
|
||||
let mut result_group = GraphicGroupTable::empty(); // Changed from Instances::empty()
|
||||
let mut text_cursor = DVec2::ZERO;
|
||||
let mut point_id_gen = PointId::ZERO; // Use a single generator across all glyphs
|
||||
|
||||
for line in str.split('\n') {
|
||||
for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() {
|
||||
push_str(&mut buffer, word);
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
// Don't wrap the first word
|
||||
if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, text_cursor.x, space_glyph) {
|
||||
text_cursor = DVec2::new(0., text_cursor.y + line_height);
|
||||
}
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
let glyph_id = GlyphId(glyph_info.glyph_id as u16);
|
||||
|
||||
// Calculate the intended render position for this glyph's origin before advancing the cursor
|
||||
let current_glyph_offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * scale;
|
||||
let glyph_render_pos = text_cursor + current_glyph_offset;
|
||||
|
||||
// Check for line wrap based on advance width BEFORE rendering the current glyph
|
||||
if let Some(max_width) = typesetting.max_width {
|
||||
if space_glyph != Some(glyph_id) && text_cursor.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width {
|
||||
// Move cursor to the next line for the *next* glyph
|
||||
text_cursor = DVec2::new(0., text_cursor.y + line_height);
|
||||
// Note: The current glyph still renders based on the glyph_render_pos calculated before the wrap check.
|
||||
}
|
||||
}
|
||||
|
||||
// Clip when the height is exceeded - stop adding instances
|
||||
if typesetting.max_height.is_some_and(|max_height| text_cursor.y > max_height - line_height) {
|
||||
return result_group; // Changed from result_instances
|
||||
}
|
||||
|
||||
// Build the glyph geometry relative to its origin (0,0)
|
||||
let mut glyph_builder = GlyphBuilder {
|
||||
current_subpath: Subpath::new(Vec::new(), false),
|
||||
other_subpaths: Vec::new(),
|
||||
ascender,
|
||||
scale,
|
||||
id: point_id_gen, // Pass the current generator state
|
||||
};
|
||||
|
||||
buzz_face.outline_glyph(glyph_id, &mut glyph_builder);
|
||||
point_id_gen = glyph_builder.id; // Update the generator state for the next glyph
|
||||
|
||||
let glyph_subpaths = glyph_builder.take_subpaths();
|
||||
|
||||
// Create an instance only if the glyph has geometry (e.g., not for empty spaces if they have no outline)
|
||||
if !glyph_subpaths.is_empty() {
|
||||
// Create VectorData for this single glyph
|
||||
let glyph_vector_data = VectorData::from_subpaths(glyph_subpaths, true);
|
||||
// Wrap VectorData in VectorDataTable
|
||||
let glyph_vector_table = VectorDataTable::new(glyph_vector_data);
|
||||
// TODO: Consider inheriting style (fill/stroke) if needed in the future.
|
||||
|
||||
// Create the transform to position this glyph instance
|
||||
let glyph_transform = DAffine2::from_translation(glyph_render_pos);
|
||||
|
||||
// Add the instance to the results group
|
||||
result_group.push(Instance {
|
||||
// Changed from result_instances.push
|
||||
instance: GraphicElement::VectorData(glyph_vector_table), // Changed instance type
|
||||
transform: glyph_transform,
|
||||
alpha_blending: Default::default(), // Use default blending for now
|
||||
source_node_id: None, // Glyphs don't have a specific source node ID
|
||||
});
|
||||
}
|
||||
|
||||
// Advance the main text cursor based on the glyph's advance width for the next glyph
|
||||
text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale;
|
||||
}
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
|
||||
// Move cursor down for the next line
|
||||
text_cursor = DVec2::new(0., text_cursor.y + line_height);
|
||||
}
|
||||
|
||||
result_group // Changed from result_instances
|
||||
}
|
||||
|
|
|
@ -806,6 +806,98 @@ async fn select_subpath_by_index(_ctx: impl Ctx + ExtractAll + CloneVarArgs, vec
|
|||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Contains Shape"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn contains_shape(
|
||||
_: impl Ctx,
|
||||
/// The vector data representing the shape to check for containment.
|
||||
shape_to_check: VectorDataTable,
|
||||
/// The vector data representing the shape to check against.
|
||||
#[expose]
|
||||
filter_shape: VectorDataTable,
|
||||
/// If true, requires all points of the shape to be contained. If false, requires at least one point to be contained.
|
||||
#[default(true)]
|
||||
require_full_containment: bool,
|
||||
) -> bool {
|
||||
let shape_transform = shape_to_check.transform();
|
||||
let shape_instance = shape_to_check.one_instance_ref().instance;
|
||||
|
||||
let filter_transform = filter_shape.transform();
|
||||
let filter_instance = filter_shape.one_instance_ref().instance;
|
||||
|
||||
// Pre-transform filter subpaths into world space for efficient checking
|
||||
let filter_subpaths_world: Vec<_> = filter_instance
|
||||
.stroke_bezier_paths()
|
||||
.map(|mut subpath| {
|
||||
subpath.apply_transform(filter_transform);
|
||||
subpath
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If the filter shape has no geometry, containment is impossible (unless the shape to check is also empty)
|
||||
if filter_subpaths_world.is_empty() {
|
||||
return shape_instance.point_domain.positions().is_empty();
|
||||
}
|
||||
|
||||
let points_to_check = shape_instance.point_domain.positions();
|
||||
|
||||
// Handle empty shape_to_check
|
||||
if points_to_check.is_empty() {
|
||||
return require_full_containment; // Empty shape is fully contained, but not partially contained.
|
||||
}
|
||||
|
||||
let mut points_contained_count = 0;
|
||||
|
||||
// Iterate through each point in the input shape_to_check
|
||||
for point_pos in points_to_check {
|
||||
// Transform the point into world space
|
||||
let point_world = shape_transform.transform_point2(*point_pos);
|
||||
|
||||
// Check if the point is contained within any of the filter subpaths
|
||||
let is_contained = filter_subpaths_world.iter().any(|subpath| subpath.contains_point(point_world));
|
||||
|
||||
if require_full_containment {
|
||||
// If full containment is required, return false immediately if any point is outside
|
||||
if !is_contained {
|
||||
return false;
|
||||
}
|
||||
points_contained_count += 1; // Only needed to confirm loop ran if all points were contained
|
||||
} else {
|
||||
// If partial containment is sufficient, return true immediately if any point is inside
|
||||
if is_contained {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If full containment was required and we looped through all points without returning false, it means all points were contained.
|
||||
// If partial containment was required and we looped through all points without returning true, it means no points were contained.
|
||||
require_full_containment && points_contained_count > 0
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Group Count"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn group_count(_: impl Ctx, group: GraphicGroupTable, #[default(false)] recurse: bool) -> u64 {
|
||||
// Helper function to handle recursion
|
||||
fn count_groups_recursive(group: &GraphicGroupTable) -> u64 {
|
||||
let mut count = group.len() as u64;
|
||||
|
||||
for instance in group.instance_ref_iter() {
|
||||
if let GraphicElement::GraphicGroup(subgroup) = &instance.instance {
|
||||
count += count_groups_recursive(subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
// Non-recursive count just returns the number of items
|
||||
if !recurse {
|
||||
return group.len() as u64;
|
||||
}
|
||||
|
||||
// Use the helper function for recursive counting
|
||||
count_groups_recursive(&group)
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Select Points Within Shape"), category("Vector"), path(graphene_core::vector))]
|
||||
async fn select_points_within_shape(
|
||||
_: impl Ctx,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::vector::{VectorData, VectorDataTable};
|
||||
use graph_craft::wasm_application_io::WasmEditorApi;
|
||||
use graphene_core::Ctx;
|
||||
use graphene_core::text::TypesettingConfig;
|
||||
pub use graphene_core::text::{Font, FontCache, bounding_box, load_face, to_path};
|
||||
pub use graphene_core::text::{Font, FontCache, bounding_box, load_face, to_group, to_path};
|
||||
use graphene_core::{Ctx, GraphicGroupTable};
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
#[node_macro::node(category("Text"))] // Changed category for clarity
|
||||
fn text<'i: 'n>(
|
||||
_: impl Ctx,
|
||||
editor: &'i WasmEditorApi,
|
||||
|
@ -15,7 +15,9 @@ fn text<'i: 'n>(
|
|||
#[default(1.)] character_spacing: f64,
|
||||
#[default(None)] max_width: Option<f64>,
|
||||
#[default(None)] max_height: Option<f64>,
|
||||
) -> VectorDataTable {
|
||||
#[default(false)] output_instances: bool, // Added parameter
|
||||
) -> GraphicGroupTable {
|
||||
// Changed return type
|
||||
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
|
||||
|
||||
let typesetting = TypesettingConfig {
|
||||
|
@ -26,7 +28,12 @@ fn text<'i: 'n>(
|
|||
max_height,
|
||||
};
|
||||
|
||||
let result = VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false);
|
||||
|
||||
VectorDataTable::new(result)
|
||||
if output_instances {
|
||||
to_group(&text, buzz_face, typesetting)
|
||||
} else {
|
||||
let vector_data = VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false);
|
||||
let vector_table = VectorDataTable::new(vector_data);
|
||||
// Convert VectorDataTable into GraphicGroupTable
|
||||
vector_table.into()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue