Merge branch 'master' into add-median-filter-blur

This commit is contained in:
Youssef Ahmed Hassan Elzedy 2025-11-26 15:01:01 +02:00 committed by GitHub
commit 1927925525
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1201 additions and 485 deletions

View file

@ -5,6 +5,10 @@ use std::process::{Command, Stdio};
pub(crate) const APP_NAME: &str = "Graphite";
pub(crate) fn workspace_path() -> PathBuf {
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
}
fn profile_name() -> &'static str {
let mut profile = env!("CARGO_PROFILE");
if profile == "debug" {
@ -14,7 +18,7 @@ fn profile_name() -> &'static str {
}
pub(crate) fn profile_path() -> PathBuf {
PathBuf::from(env!("CARGO_WORKSPACE_DIR")).join(format!("target/{}", env!("CARGO_PROFILE")))
workspace_path().join(format!("target/{}", env!("CARGO_PROFILE")))
}
pub(crate) fn cef_path() -> PathBuf {

View file

@ -10,9 +10,12 @@ const APP_ID: &str = "rs.graphite.Graphite";
const PACKAGE: &str = "graphite-desktop-platform-mac";
const HELPER_BIN: &str = "graphite-desktop-platform-mac-helper";
const ICONS_FILE_NAME: &str = "graphite.icns";
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const RESOURCES_PATH: &str = "Contents/Resources";
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin(PACKAGE, None)?;
@ -46,7 +49,13 @@ fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
}
copy_dir(&cef_path().join(FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(FRAMEWORK));
copy_dir(&cef_path().join(CEF_FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(CEF_FRAMEWORK));
let resource_dir = app_dir.join(RESOURCES_PATH);
fs::create_dir_all(&resource_dir).expect("failed to create app resource dir");
let icon_file = workspace_path().join("branding/app-icons").join(ICONS_FILE_NAME);
fs::copy(icon_file, resource_dir.join(ICONS_FILE_NAME)).expect("failed to copy icon file");
app_dir
}
@ -61,25 +70,22 @@ fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool)
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
let info = InfoPlist {
cf_bundle_development_region: "en".to_string(),
cf_bundle_name: exec_name.to_string(),
cf_bundle_identifier: id.to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_identifier: id.to_string(),
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_name: exec_name.to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
cf_bundle_version: "0.0.0".to_string(),
cf_bundle_short_version_string: "0.0".to_string(),
cf_bundle_development_region: "en".to_string(),
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
ls_file_quarantine_enabled: true,
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
ns_bluetooth_always_usage_description: exec_name.to_string(),
ns_supports_automatic_graphics_switching: true,
ns_web_browser_publickey_credential_usage_description: exec_name.to_string(),
ns_camera_usage_description: exec_name.to_string(),
ns_microphone_usage_description: exec_name.to_string(),
};
let plist_file = dir.join("Info.plist");
@ -89,18 +95,18 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) ->
#[derive(serde::Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleDisplayName")]
cf_bundle_display_name: String,
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleIconFile")]
cf_bundle_icon_file: String,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundlePackageType")]
cf_bundle_package_type: String,
#[serde(rename = "CFBundleSignature")]
@ -109,6 +115,8 @@ struct InfoPlist {
cf_bundle_version: String,
#[serde(rename = "CFBundleShortVersionString")]
cf_bundle_short_version_string: String,
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "LSEnvironment")]
ls_environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
@ -117,14 +125,6 @@ struct InfoPlist {
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
ls_ui_element: Option<String>,
#[serde(rename = "NSBluetoothAlwaysUsageDescription")]
ns_bluetooth_always_usage_description: String,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
#[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")]
ns_web_browser_publickey_credential_usage_description: String,
#[serde(rename = "NSCameraUsageDescription")]
ns_camera_usage_description: String,
#[serde(rename = "NSMicrophoneUsageDescription")]
ns_microphone_usage_description: String,
}

View file

@ -33,6 +33,16 @@ impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
if let Some(cmd) = command_line {
cmd.append_switch_with_value(Some(&CefString::from("renderer-process-limit")), Some(&CefString::from("1")));
cmd.append_switch_with_value(Some(&CefString::from("disk-cache-size")), Some(&CefString::from("0")));
cmd.append_switch(Some(&CefString::from("incognito")));
cmd.append_switch(Some(&CefString::from("no-first-run")));
cmd.append_switch(Some(&CefString::from("disable-file-system")));
cmd.append_switch(Some(&CefString::from("disable-local-storage")));
cmd.append_switch(Some(&CefString::from("disable-background-networking")));
cmd.append_switch(Some(&CefString::from("disable-audio-input")));
cmd.append_switch(Some(&CefString::from("disable-audio-output")));
#[cfg(not(feature = "accelerated_paint"))]
{
// Disable GPU acceleration when accelerated_paint feature is not enabled

View file

@ -1,6 +1,7 @@
use winit::dpi::PhysicalSize;
use winit::event_loop::ActiveEventLoop;
use winit::icon::Icon;
use winit::platform::windows::WinIcon;
use winit::platform::windows::{WinIcon, WindowAttributesWindows};
use winit::window::{Window, WindowAttributes};
use crate::event::AppEventScheduler;
@ -11,12 +12,10 @@ pub(super) struct NativeWindowImpl {
impl super::NativeWindow for NativeWindowImpl {
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
if let Ok(win_icon) = WinIcon::from_resource(1, None) {
let icon = Icon(std::sync::Arc::new(win_icon));
attributes.with_window_icon(Some(icon))
} else {
attributes
}
let icon = WinIcon::from_resource(1, Some(PhysicalSize::new(256, 256))).ok().map(|icon| Icon(std::sync::Arc::new(icon)));
let win_window = WindowAttributesWindows::default().with_taskbar_icon(icon);
let icon = WinIcon::from_resource(1, None).ok().map(|icon| Icon(std::sync::Arc::new(icon)));
attributes.with_window_icon(icon).with_platform_attributes(Box::new(win_window))
}
fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {

View file

@ -502,10 +502,17 @@ impl TableRowLayout for Raster<CPU> {
format!("Raster ({}x{})", self.width, self.height)
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
let raster = self.data();
if raster.width == 0 || raster.height == 0 {
let widgets = vec![TextLabel::new("Image has no area").widget_holder()];
return vec![LayoutGroup::Row { widgets }];
}
let base64_string = raster.base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let output = raster.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);

View file

@ -416,6 +416,539 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface."),
properties: None,
},
DocumentNodeDefinition {
identifier: "Blend Shapes",
category: "Vector",
// [IMPORTS]2 -> 0[0:Floor]
// [0:Floor]0 -> 0[1:Subtract]
// "1: f64" -> 1[1:Subtract]
// "(): ()" -> 0[2:Instance Index]
// "0: u32" -> 1[2:Instance Index]
// [2:Instance Index]0 -> 0[3:Divide]
// [1:Subtract]0 -> 1[3:Divide]
// [IMPORTS]1 -> 0[4:Position on Path]
// [3:Divide]0 -> 1[4:Position on Path]
// "false: bool" -> 2[4:Position on Path]
// "false: bool" -> 3[4:Position on Path]
// "(): ()" -> 0[5:Instance Vector]
// [5:Instance Vector]0 -> 0[6:Reset Transform]
// "true: bool" -> 1[6:Reset Transform]
// "false: bool" -> 2[6:Reset Transform]
// "false: bool" -> 3[6:Reset Transform]
// [12:Flatten Vector]0 -> 0[7:Instance Map]
// [6:Reset Transform]0 -> 1[7:Instance Map]
// [7:Instance Map]0 -> 0[8:Morph]
// [15:Multiply]0 -> 1[8:Morph]
// [8:Morph]0 -> 0[9:Transform]
// [4:Position on Path]0 -> 1[9:Transform]
// "0: f64" -> 2[9:Transform]
// "(0, 0): DVec2" -> 3[9:Transform]
// "(0, 0): DVec2" -> 4[9:Transform]
// [IMPORTS]1 -> 0[10:Count Points]
// [10:Count Points]0 -> 0[11:Equals]
// [13:Count Elements]0 -> 1[11:Equals]
// [IMPORTS]0 -> 0[12:Flatten Vector]
// [12:Flatten Vector]0 -> 0[13:Count Elements]
// [13:Count Elements]0 -> 0[14:Subtract]
// "1: f64" -> 1[14:Subtract]
// [3:Divide]0 -> 0[15:Multiply]
// [14:Subtract]0 -> 1[15:Multiply]
// [12:Flatten Vector]0 -> 0[16:Morph]
// [15:Multiply]0 -> 1[16:Morph]
// [11:Equals]0 -> 0[17:Switch]
// [9:Transform]0 -> 1[17:Switch]
// [16:Morph]0 -> 2[17:Switch]
// [17:Switch]0 -> 0[18:Instance Repeat]
// [0:Floor]0 -> 1[18:Instance Repeat]
// [IMPORTS]3 -> 2[18:Instance Repeat]
// [18:Instance Repeat]0 -> 0[EXPORTS]
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(18), 0)],
nodes: [
// 0: Floor
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::floor::IDENTIFIER),
inputs: vec![NodeInput::import(concrete!(f64), 2)],
..Default::default()
},
// 1: Subtract
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(1.), false)],
..Default::default()
},
// 2: Instance Index
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_index::IDENTIFIER),
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::value(TaggedValue::U32(0), false)],
..Default::default()
},
// 3: Divide
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::divide::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(1), 0)],
..Default::default()
},
// 4: Position on Path
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::position_on_path::IDENTIFIER),
inputs: vec![
NodeInput::import(generic!(T), 1),
NodeInput::node(NodeId(3), 0),
NodeInput::value(TaggedValue::Bool(false), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
},
// 5: Instance Vector
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_vector::IDENTIFIER),
inputs: vec![NodeInput::value(TaggedValue::None, false)],
..Default::default()
},
// 6: Reset Transform
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::reset_transform::IDENTIFIER),
inputs: vec![
NodeInput::node(NodeId(5), 0),
NodeInput::value(TaggedValue::Bool(true), false),
NodeInput::value(TaggedValue::Bool(false), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
},
// 7: Instance Map
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_map::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(12), 0), NodeInput::node(NodeId(6), 0)],
..Default::default()
},
// 8: Morph
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::morph::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(7), 0), NodeInput::node(NodeId(15), 0)],
..Default::default()
},
// 9: Transform
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform::IDENTIFIER),
inputs: vec![
NodeInput::node(NodeId(8), 0),
NodeInput::node(NodeId(4), 0),
NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
],
..Default::default()
},
// 10: Count Points
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::count_points::IDENTIFIER),
inputs: vec![NodeInput::import(generic!(T), 1)],
..Default::default()
},
// 11: Equals
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::equals::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(10), 0), NodeInput::node(NodeId(13), 0)],
..Default::default()
},
// 12: Flatten Vector
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER),
inputs: vec![NodeInput::import(generic!(T), 0)],
..Default::default()
},
// 13: Count Elements
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::count_elements::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(12), 0)],
..Default::default()
},
// 14: Subtract
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::subtract::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(13), 0), NodeInput::value(TaggedValue::F64(1.), false)],
..Default::default()
},
// 15: Multiply
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(math_nodes::multiply::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(3), 0), NodeInput::node(NodeId(14), 0)],
..Default::default()
},
// 16: Morph
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::morph::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(12), 0), NodeInput::node(NodeId(15), 0)],
..Default::default()
},
// 17: Switch
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(logic::switch::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(11), 0), NodeInput::node(NodeId(9), 0), NodeInput::node(NodeId(16), 0)],
..Default::default()
},
// 18: Instance Repeat
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_repeat::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(17), 0), NodeInput::node(NodeId(0), 0), NodeInput::import(generic!(T), 3)],
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(TaggedValue::F64(10.), false),
NodeInput::value(TaggedValue::Bool(Default::default()), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Content", "TODO").into(), ("Path", "TODO").into(), ("Count", "TODO").into(), ("Reverse", "TODO").into()],
output_names: vec!["Out".to_string()],
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
// 0: Floor
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
// 1: Subtract
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, -1)),
..Default::default()
},
..Default::default()
},
// 2: Instance Index
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, -2)),
..Default::default()
},
..Default::default()
},
// 3: Divide
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, -2)),
..Default::default()
},
..Default::default()
},
// 4: Position on Path
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, -3)),
..Default::default()
},
..Default::default()
},
// 5: Instance Vector
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 2)),
..Default::default()
},
..Default::default()
},
// 6: Reset Transform
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 2)),
..Default::default()
},
..Default::default()
},
// 7: Instance Map
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)),
..Default::default()
},
..Default::default()
},
// 8: Morph
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 1)),
..Default::default()
},
..Default::default()
},
// 9: Transform
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 1)),
..Default::default()
},
..Default::default()
},
// 10: Count Points
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 4)),
..Default::default()
},
..Default::default()
},
// 11: Equals
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 4)),
..Default::default()
},
..Default::default()
},
// 12: Flatten Vector
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 6)),
..Default::default()
},
..Default::default()
},
// 13: Count Elements
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 8)),
..Default::default()
},
..Default::default()
},
// 14: Subtract
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 8)),
..Default::default()
},
..Default::default()
},
// 15: Multiply
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 7)),
..Default::default()
},
..Default::default()
},
// 16: Morph
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 6)),
..Default::default()
},
..Default::default()
},
// 17: Switch
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 4)),
..Default::default()
},
..Default::default()
},
// 18: Instance Repeat
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(49, -1)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Origins to Polyline",
category: "Vector",
// "(): ()" -> 0[0:Instance Vector]
// [0:Instance Vector]0 -> 0[1:Extract Transform]
// [1:Extract Transform]0 -> 0[2:Decompose Translation]
// [2:Decompose Translation]0 -> 0[3:Vec2 to Point]
// [IMPORTS]0 -> 0[4:Flatten Vector]
// [4:Flatten Vector]0 -> 0[5:Instance Map]
// [3:Vec2 to Point]0 -> 1[5:Instance Map]
// [5:Instance Map]0 -> 0[6: Flatten Path]
// [6:Flatten Path]0 -> 0[7:Points to Polyline]
// "false: bool" -> 1[7:Points to Polyline]
// [7:Points to Polyline]0 -> 0[EXPORTS]
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(7), 0)],
nodes: [
// 0: Instance Vector
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_vector::IDENTIFIER),
inputs: vec![NodeInput::value(TaggedValue::None, false)],
..Default::default()
},
// 1: Extract Transform
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::extract_transform::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(0), 0)],
..Default::default()
},
// 2: Decompose Translation
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::decompose_translation::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(1), 0)],
..Default::default()
},
// 3: Vec2 to Point
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::vec_2_to_point::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(2), 0)],
..Default::default()
},
// 4: Flatten Vector
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(graphic_nodes::graphic::flatten_vector::IDENTIFIER),
inputs: vec![NodeInput::import(generic!(T), 0)],
..Default::default()
},
// 5: Instance Map
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector_nodes::instance_map::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(4), 0), NodeInput::node(NodeId(3), 0)],
..Default::default()
},
// 6: Flatten Path
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::flatten_path::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(5), 0)],
..Default::default()
},
// 7: Points to Polyline
DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(vector::points_to_polyline::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(6), 0), NodeInput::value(TaggedValue::Bool(false), false)],
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::Vector(Default::default()), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Vector", "TODO").into()],
output_names: vec!["Vector".to_string()],
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
// 0: Instance Vector
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 1)),
..Default::default()
},
..Default::default()
},
// 1: Extract Transform
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 1)),
..Default::default()
},
..Default::default()
},
// 2: Decompose Transform
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 1)),
..Default::default()
},
..Default::default()
},
// 3: Vec2 to Point
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 1)),
..Default::default()
},
..Default::default()
},
// 4: Flatten Vector
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)),
..Default::default()
},
..Default::default()
},
// 5: Instance Map
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 0)),
..Default::default()
},
..Default::default()
},
// 6: Flatten Path
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(35, 0)),
..Default::default()
},
..Default::default()
},
// 7: Points to Polyline
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(42, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Load Image",
category: "Web Request",
@ -745,13 +1278,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::import(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::import(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
inputs: vec![NodeInput::import(concrete!(DVec2), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
call_argument: generic!(T),
..Default::default()

View file

@ -154,6 +154,8 @@ pub(crate) fn property_from_type(
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
Some("Progression") => progression_widget(default_info, number_input.min(min(0.))).into(),
Some("SignedInteger") => number_widget(default_info, number_input.int()).into(),
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
@ -793,6 +795,50 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetH
(first_widgets, second_widgets)
}
// Two number fields beside one another, the first for the fractional part (decimals, range mode) and the second for the whole part (integers, increment mode)
pub fn progression_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
let Some(document_node) = document_node else { return Vec::new() };
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::F64(x)) = input.as_non_exposed_value() {
let whole_part = x.trunc();
let fractional_part = x.fract();
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
number_props
.clone()
.label("Progress")
.mode_range()
.min(0.)
.max(0.99999)
.value(Some(fractional_part))
.on_update(update_value(move |input: &NumberInput| TaggedValue::F64(whole_part + input.value.unwrap()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
TextLabel::new("+").widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
number_props
.label("Element #")
.mode_increment()
.min(0.)
.is_integer(true)
.value(Some(whole_part))
.on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap() + fractional_part), node_id, index))
.on_commit(commit_value)
.widget_holder(),
])
}
widgets
}
pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;

View file

@ -5,13 +5,13 @@ use crate::messages::portfolio::document::graph_operation::utility_types::{Modif
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformationState;
use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::vector::{HandleExt, PointId, VectorModificationType};
use std::collections::{HashMap, VecDeque};
use std::f64::consts::PI;
#[derive(Debug, PartialEq, Clone, Copy)]
struct AnchorPoint {
@ -156,22 +156,25 @@ pub struct Translation {
}
impl Translation {
pub fn to_dvec(self, transform: DAffine2, increment_mode: bool) -> DVec2 {
pub fn to_dvec(self, state: &TransformationState, document: &DocumentMessageHandler) -> DVec2 {
let document_to_viewport = document.metadata().document_to_viewport;
let displacement = if let Some(value) = self.typed_distance {
match self.constraint {
Axis::X => transform.transform_vector2(DVec2::new(value, 0.)),
Axis::Y => transform.transform_vector2(DVec2::new(0., value)),
Axis::X => DVec2::X * value,
Axis::Y => DVec2::Y * value,
Axis::Both => self.dragged_distance,
}
} else {
match self.constraint {
Axis::Both => self.dragged_distance,
Axis::X => DVec2::new(self.dragged_distance.x, 0.),
Axis::Y => DVec2::new(0., self.dragged_distance.y),
Axis::X => DVec2::X * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()),
Axis::Y => DVec2::Y * self.dragged_distance.dot(state.constraint_axis(self.constraint).unwrap_or_default()),
}
};
let displacement = transform.inverse().transform_vector2(displacement);
if increment_mode { displacement.round() } else { displacement }
let displacement_viewport = displacement * document_to_viewport.matrix2.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale.
let displacement_document = document_to_viewport.inverse().transform_vector2(displacement_viewport);
let displacement_document = if state.is_rounded_to_intervals { displacement_document.round() } else { displacement_document }; // It rounds in document space?
document_to_viewport.transform_vector2(displacement_document)
}
#[must_use]
@ -327,36 +330,19 @@ impl TransformType {
impl TransformOperation {
#[allow(clippy::too_many_arguments)]
pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
let local_axis_transform_angle = (quad.top_left() - quad.top_right()).to_angle();
pub fn apply_transform_operation(&self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
if self != &TransformOperation::None {
let transformation = match self {
TransformOperation::Grabbing(translation) => {
let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(local_transform, increment_mode)));
if local {
let resolved_angle = if local_axis_transform_angle > 0. {
local_axis_transform_angle
} else {
local_axis_transform_angle - PI
};
DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
} else {
translate
}
}
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(increment_mode)),
TransformOperation::Scaling(scale) => {
if local {
DAffine2::from_angle(local_axis_transform_angle) * DAffine2::from_scale(scale.to_dvec(increment_mode)) * DAffine2::from_angle(-local_axis_transform_angle)
} else {
DAffine2::from_scale(scale.to_dvec(increment_mode))
}
}
let mut transformation = match self {
TransformOperation::Grabbing(translation) => DAffine2::from_translation(translation.to_dvec(state, document)),
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(state.is_rounded_to_intervals)),
TransformOperation::Scaling(scale) => DAffine2::from_scale(scale.to_dvec(state.is_rounded_to_intervals)),
TransformOperation::None => unreachable!(),
};
let normalized_transform = state.local_to_viewport_transform();
transformation = normalized_transform * transformation * normalized_transform.inverse();
selected.update_transforms(transformation, Some(pivot), Some(*self));
self.hints(selected.responses, local);
selected.update_transforms(transformation, Some(state.pivot_viewport(document)), Some(*self));
self.hints(selected.responses, state.is_transforming_in_local_space);
}
}
@ -373,24 +359,27 @@ impl TransformOperation {
}
#[allow(clippy::too_many_arguments)]
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) -> bool {
(*self, local) = match self {
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) -> bool {
let resulting_local;
(*self, resulting_local) = match self {
TransformOperation::Grabbing(translation) => {
let (translation, local) = translation.with_constraint(axis, local);
(TransformOperation::Grabbing(translation), local)
let (translation, resulting_local) = translation.with_constraint(axis, state.is_transforming_in_local_space);
(TransformOperation::Grabbing(translation), resulting_local)
}
TransformOperation::Scaling(scale) => {
let (scale, local) = scale.with_constraint(axis, local);
(TransformOperation::Scaling(scale), local)
let (scale, resulting_local) = scale.with_constraint(axis, state.is_transforming_in_local_space);
(TransformOperation::Scaling(scale), resulting_local)
}
_ => (*self, false),
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
local
self.apply_transform_operation(selected, state, document);
resulting_local
}
#[allow(clippy::too_many_arguments)]
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
match self {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
@ -398,7 +387,7 @@ impl TransformOperation {
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
self.apply_transform_operation(selected, state, document);
}
pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
@ -481,7 +470,7 @@ impl TransformOperation {
}
#[allow(clippy::too_many_arguments)]
pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
pub fn negate(&mut self, selected: &mut Selected, state: &TransformationState, document: &DocumentMessageHandler) {
if *self != TransformOperation::None {
*self = match self {
TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
@ -489,7 +478,8 @@ impl TransformOperation {
TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
_ => *self,
};
self.apply_transform_operation(selected, increment_mode, local, quad, transform, pivot, local_transform);
self.apply_transform_operation(selected, state, document);
}
}
}

View file

@ -1582,6 +1582,53 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}
}
// Migrate from the old source/target "Morph" node to the new vector table based "Morph" node.
// This doesn't produce exactly equivalent results in cases involving input vector tables with multiple rows.
// The old version would zip the source and target table rows, interpoleating each pair together.
// The migrated version will instead deeply flatten both merged tables and morph sequentially between all source vectors and all target vector elements.
// This migration assumes most usages didn't involve multiple parallel vector elements, and instead morphed from a single source to a single target vector element.
if reference == "Morph" && inputs_count == 3 {
// Old signature:
// async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vector>, #[default(0.5)] time: Fraction) -> Table<Vector> { ... }
//
// New signature:
// async fn morph<I: IntoGraphicTable>(_: impl Ctx, content: #[implementations(Table<Graphic>, Table<Vector>)] content: I, progression: Progression) -> Table<Vector> { ... }
let mut node_template = resolve_document_node_type(reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
// Create a new Merge node
let Some(merge_node_type) = resolve_document_node_type("Merge") else {
log::error!("Could not get merge node from definition when upgrading morph");
return None;
};
let merge_template = merge_node_type.default_node_template();
let merge_node_id = NodeId::new();
// Decide on the placement position of the new Merge node
let Some(morph_position) = document.network_interface.position_from_downstream_node(node_id, network_path) else {
log::error!("Could not get position for morph node {node_id}");
return None;
};
let merge_position = morph_position + IVec2::new(-7, 0);
// Insert the new Merge node into the network
document.network_interface.insert_node(merge_node_id, merge_template, network_path);
document.network_interface.set_to_node_or_layer(&merge_node_id, network_path, false);
document.network_interface.shift_absolute_node_position(&merge_node_id, merge_position, network_path);
// Connect the old 'source' and 'target' inputs to the new Merge node
document.network_interface.set_input(&InputConnector::node(merge_node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(merge_node_id, 1), old_inputs[1].clone(), network_path);
// Connect the new Merge node to the 'content' input of the Morph node
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::node(merge_node_id, 0), network_path);
// Connect the old 'progression' input to the new 'progression' input of the Morph node
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
}
// Add context features to nodes that don't have them (fine-grained context caching migration)
if node.context_features == graphene_std::ContextDependencies::default()
&& let Some(reference) = document.network_interface.reference(node_id, network_path).cloned().flatten()

View file

@ -593,6 +593,19 @@ impl SelectToolData {
}
}
/// Bounding boxes are unfortunately not axis aligned. The bounding boxes are found after a transformation is applied to all of the layers.
/// This uses some rather confusing logic to determine what transform that should be.
pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffine2 {
// Update bounds
document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default()
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
type ToolOptions = ();
@ -633,14 +646,7 @@ impl Fsm for SelectToolFsmState {
}
}
// Update bounds
let mut transform = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default();
let mut transform = create_bounding_box_transform(document);
// Check if the matrix is not invertible
let mut transform_tampered = false;

View file

@ -7,6 +7,7 @@ use crate::messages::portfolio::document::utility_types::transformation::{Axis,
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType};
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::tool_messages::select_tool;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::{ToolData, ToolType};
use glam::{DAffine2, DVec2};
@ -31,43 +32,67 @@ pub struct TransformLayerMessageContext<'a> {
pub viewport: &'a ViewportMessageHandler,
}
#[derive(Debug, Clone, Default, ExtractField)]
pub struct TransformationState {
pub is_rounded_to_intervals: bool,
pub is_transforming_in_local_space: bool,
pub local_transform_axes: [DVec2; 2],
pub document_space_pivot: DocumentPosition,
}
impl TransformationState {
pub fn pivot_viewport(&self, document: &DocumentMessageHandler) -> DVec2 {
document.metadata().document_to_viewport.transform_point2(self.document_space_pivot)
}
pub fn constraint_axis(&self, axis_constraint: Axis) -> Option<DVec2> {
match axis_constraint {
Axis::X => Some(if self.is_transforming_in_local_space { self.local_transform_axes[0] } else { DVec2::X }),
Axis::Y => Some(if self.is_transforming_in_local_space { self.local_transform_axes[1] } else { DVec2::Y }),
_ => None,
}
}
pub fn project_onto_constrained(&self, vector: DVec2, axis_constraint: Axis) -> DVec2 {
self.constraint_axis(axis_constraint).map_or(vector, |direction| vector.project_onto_normalized(direction))
}
pub fn local_to_viewport_transform(&self) -> DAffine2 {
if self.is_transforming_in_local_space {
DAffine2::from_cols(self.local_transform_axes[0], self.local_transform_axes[1], DVec2::ZERO)
} else {
DAffine2::IDENTITY
}
}
}
#[derive(Debug, Clone, Default, ExtractField)]
pub struct TransformLayerMessageHandler {
pub transform_operation: TransformOperation,
state: TransformationState,
slow: bool,
increments: bool,
local: bool,
layer_bounding_box: Quad,
typing: Typing,
mouse_position: ViewportPosition,
start_mouse: ViewportPosition,
original_transforms: OriginalTransforms,
pivot_gizmo: PivotGizmo,
pivot: ViewportPosition,
path_bounds: Option<[DVec2; 2]>,
local_pivot: DocumentPosition,
local_mouse_start: DocumentPosition,
grab_target: DocumentPosition,
ptz: PTZ,
initial_transform: DAffine2,
operation_count: usize,
was_grabbing: bool,
// Pen tool (outgoing handle GRS manipulation)
handle: DVec2,
last_point: DVec2,
grs_pen_handle: bool,
// Ghost outlines for Path Tool
// Path tool (ghost outlines showing pre-transform geometry)
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
was_grabbing: bool,
}
#[message_handler_data]
@ -124,8 +149,8 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
if !using_path_tool {
self.pivot_gizmo.recalculate_transform(document);
*selected.pivot = self.pivot_gizmo.position(document);
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = self.local_pivot;
self.state.document_space_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = self.state.document_space_pivot;
}
// TODO: Here vector data from all layers is not considered which can be a problem in pivot calculation
else if let Some(vector) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
@ -156,7 +181,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
*selected.pivot = new_pivot;
self.path_bounds = bounds;
self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
self.state.document_space_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
self.grab_target = document_to_viewport.inverse().transform_point2(grab_target);
} else {
log::warn!("Failed to calculate pivot.");
@ -206,52 +231,52 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
match self.transform_operation {
TransformOperation::None => (),
TransformOperation::Grabbing(translation) => {
let translation = translation.to_dvec(self.initial_transform, self.increments);
let viewport_translate = document_to_viewport.transform_vector2(translation);
let translation_viewport = self.state.local_to_viewport_transform().matrix2 * translation.to_dvec(&self.state, document);
let pivot = document_to_viewport.transform_point2(self.grab_target);
let quad = Quad::from_box([pivot, pivot + viewport_translate]);
let quad = Quad::from_box([pivot, pivot + translation_viewport]);
responses.add(SelectToolMessage::PivotShift {
offset: Some(viewport_translate),
offset: Some(translation_viewport),
flush: false,
});
let typed_string = (!self.typing.digits.is_empty() && self.transform_operation.can_begin_typing()).then(|| self.typing.string.clone());
overlay_context.translation_box(translation, quad, typed_string);
overlay_context.translation_box(translation_viewport / document_to_viewport.matrix2.y_axis.length(), quad, typed_string);
}
TransformOperation::Scaling(scale) => {
let scale = scale.to_f64(self.increments);
let scale = scale.to_f64(self.state.is_rounded_to_intervals);
let text = format!("{}x", format_rounded(scale, 3));
let pivot = document_to_viewport.transform_point2(self.local_pivot);
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
let local_edge = start_mouse - pivot;
let local_edge = project_edge_to_quad(local_edge, &self.layer_bounding_box, self.local, axis_constraint);
let boundary_point = pivot + local_edge * scale.min(1.);
let end_point = pivot + local_edge * scale.max(1.);
let local_edge = start_mouse - self.state.pivot_viewport(document);
let local_edge = self.state.project_onto_constrained(local_edge, axis_constraint);
let boundary_point = self.state.pivot_viewport(document) + local_edge * scale.min(1.);
let end_point = self.state.pivot_viewport(document) + local_edge * scale.max(1.);
if scale > 0. {
overlay_context.dashed_line(pivot, boundary_point, None, None, Some(2.), Some(2.), Some(0.5));
overlay_context.dashed_line(self.state.pivot_viewport(document), boundary_point, None, None, Some(2.), Some(2.), Some(0.5));
}
overlay_context.line(boundary_point, end_point, None, None);
let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
let transform = DAffine2::from_translation(
boundary_point.midpoint(self.state.pivot_viewport(document)) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.,
);
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
}
TransformOperation::Rotating(rotation) => {
let angle = rotation.to_f64(self.increments);
let pivot = document_to_viewport.transform_point2(self.local_pivot);
let angle = rotation.to_f64(self.state.is_rounded_to_intervals);
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
let offset_angle = if self.grs_pen_handle {
self.handle - self.last_point
} else if using_path_tool {
start_mouse - pivot
start_mouse - self.state.pivot_viewport(document)
} else {
// TODO: This is always zero breaking the `.to_angle()` below?
self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
};
let tilt_offset = document.document_ptz.unmodified_tilt();
let offset_angle = offset_angle.to_angle() + tilt_offset;
let width = viewport_box.max_element();
let radius = start_mouse.distance(pivot);
let radius = start_mouse.distance(self.state.pivot_viewport(document));
let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width;
let radius = radius.clamp(ARC_MEASURE_RADIUS_FACTOR_RANGE.0 * width, ARC_MEASURE_RADIUS_FACTOR_RANGE.1 * width);
let angle_in_degrees = angle.to_degrees();
@ -270,8 +295,8 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
(arc_radius + 4. + text_texture_width) * text_angle_on_unit_circle.x,
(arc_radius + text_texture_height) * text_angle_on_unit_circle.y,
);
let transform = DAffine2::from_translation(text_texture_position + pivot);
overlay_context.draw_angle(pivot, radius, arc_radius, offset_angle, angle);
let transform = DAffine2::from_translation(text_texture_position + self.state.pivot_viewport(document));
overlay_context.draw_angle(self.state.pivot_viewport(document), radius, arc_radius, offset_angle, angle);
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
}
}
@ -320,6 +345,8 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
TransformType::Scale => TransformOperation::Scaling(Default::default()),
};
self.layer_bounding_box = selected.bounding_box();
let bounding_box = select_tool::create_bounding_box_transform(document);
self.state.local_transform_axes = [bounding_box.x_axis, bounding_box.y_axis].map(|axis| axis.normalize_or_zero());
}
TransformLayerMessage::BeginGrabPen { last_point, handle } | TransformLayerMessage::BeginRotatePen { last_point, handle } | TransformLayerMessage::BeginScalePen { last_point, handle } => {
self.typing.clear();
@ -332,11 +359,13 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
let top_left = DVec2::new(last_point.x, handle.y);
let bottom_right = DVec2::new(handle.x, last_point.y);
self.local = false;
self.state.is_transforming_in_local_space = false;
self.layer_bounding_box = Quad::from_box([top_left, bottom_right]);
let normalized_along = (handle - last_point).normalize_or_zero();
self.state.local_transform_axes = [normalized_along, normalized_along.perp()];
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(handle);
self.pivot = last_point;
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(self.pivot);
let pivot = last_point;
self.state.document_space_pivot = document.metadata().document_to_viewport.inverse().transform_point2(pivot);
self.local_mouse_start = document.metadata().document_to_viewport.inverse().transform_point2(self.start_mouse);
self.handle = handle;
@ -428,7 +457,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
}
}
self.local = false;
self.state.is_transforming_in_local_space = false;
self.operation_count += 1;
let chain_operation = self.transform_operation != TransformOperation::None;
@ -479,50 +508,12 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
});
}
TransformLayerMessage::ConstrainX => {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
self.local = self.transform_operation.constrain_axis(
Axis::X,
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.transform_operation.grs_typed(
self.typing.evaluate(),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.state.is_transforming_in_local_space = self.transform_operation.constrain_axis(Axis::X, &mut selected, &self.state, document);
self.transform_operation.grs_typed(self.typing.evaluate(), &mut selected, &self.state, document);
}
TransformLayerMessage::ConstrainY => {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
self.local = self.transform_operation.constrain_axis(
Axis::Y,
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.transform_operation.grs_typed(
self.typing.evaluate(),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.state.is_transforming_in_local_space = self.transform_operation.constrain_axis(Axis::Y, &mut selected, &self.state, document);
self.transform_operation.grs_typed(self.typing.evaluate(), &mut selected, &self.state, document);
}
TransformLayerMessage::PointerMove { slow_key, increments_key } => {
self.slow = input.keyboard.get(slow_key as usize);
@ -533,13 +524,10 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
return;
}
let pivot = document_to_viewport.transform_point2(self.local_pivot);
let new_increments = input.keyboard.get(increments_key as usize);
if new_increments != self.increments {
self.increments = new_increments;
self.transform_operation
.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform);
if new_increments != self.state.is_rounded_to_intervals {
self.state.is_rounded_to_intervals = new_increments;
self.transform_operation.apply_transform_operation(&mut selected, &self.state, document);
}
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
@ -548,45 +536,30 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
TransformOperation::Grabbing(translation) => {
let delta_pos = input.mouse.position - self.mouse_position;
let delta_pos = (self.initial_transform * document_to_viewport.inverse()).transform_vector2(delta_pos);
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
self.transform_operation.apply_transform_operation(
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
let delta_viewport = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
let delta_scaled = delta_viewport / document_to_viewport.y_axis.length(); // Values are local to the viewport but scaled so values are relative to the current scale.
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(delta_scaled));
self.transform_operation.apply_transform_operation(&mut selected, &self.state, document);
}
TransformOperation::Rotating(rotation) => {
let start_offset = pivot - self.mouse_position;
let end_offset = pivot - input.mouse.position;
let start_offset = self.state.pivot_viewport(document) - self.mouse_position;
let end_offset = self.state.pivot_viewport(document) - input.mouse.position;
let angle = start_offset.angle_to(end_offset);
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
self.transform_operation.apply_transform_operation(
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.transform_operation.apply_transform_operation(&mut selected, &self.state, document);
}
TransformOperation::Scaling(mut scale) => {
let axis_constraint = scale.constraint;
let to_mouse_final = self.mouse_position - pivot;
let to_mouse_final_old = input.mouse.position - pivot;
let to_mouse_start = self.start_mouse - pivot;
let to_mouse_final = self.mouse_position - self.state.pivot_viewport(document);
let to_mouse_final_old = input.mouse.position - self.state.pivot_viewport(document);
let to_mouse_start = self.start_mouse - self.state.pivot_viewport(document);
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.layer_bounding_box, self.local, axis_constraint);
let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.layer_bounding_box, self.local, axis_constraint);
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.layer_bounding_box, self.local, axis_constraint);
let to_mouse_final = self.state.project_onto_constrained(to_mouse_final, axis_constraint);
let to_mouse_final_old = self.state.project_onto_constrained(to_mouse_final_old, axis_constraint);
let to_mouse_start = self.state.project_onto_constrained(to_mouse_start, axis_constraint);
let change = {
let previous_frame_dist = to_mouse_final.dot(to_mouse_start);
@ -599,15 +572,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
scale = scale.increment_amount(change);
self.transform_operation = TransformOperation::Scaling(scale);
self.transform_operation.apply_transform_operation(
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.transform_operation.apply_transform_operation(&mut selected, &self.state, document);
}
};
}
@ -619,69 +584,27 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
shape_editor.set_selected_layers(target_layers);
}
TransformLayerMessage::TypeBackspace => {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
if self.typing.digits.is_empty() && self.typing.negative {
self.transform_operation
.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform);
self.transform_operation.negate(&mut selected, &self.state, document);
self.typing.type_negate();
}
self.transform_operation.grs_typed(
self.typing.type_backspace(),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
);
self.transform_operation.grs_typed(self.typing.type_backspace(), &mut selected, &self.state, document);
}
TransformLayerMessage::TypeDecimalPoint => {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
if self.transform_operation.can_begin_typing() {
self.transform_operation.grs_typed(
self.typing.type_decimal_point(),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
)
self.transform_operation.grs_typed(self.typing.type_decimal_point(), &mut selected, &self.state, document)
}
}
TransformLayerMessage::TypeDigit { digit } => {
if self.transform_operation.can_begin_typing() {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
self.transform_operation.grs_typed(
self.typing.type_number(digit),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
)
self.transform_operation.grs_typed(self.typing.type_number(digit), &mut selected, &self.state, document)
}
}
TransformLayerMessage::TypeNegate => {
let pivot = document_to_viewport.transform_point2(self.local_pivot);
if self.typing.digits.is_empty() {
self.transform_operation
.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport, pivot, self.initial_transform);
self.transform_operation.negate(&mut selected, &self.state, document);
}
self.transform_operation.grs_typed(
self.typing.type_negate(),
&mut selected,
self.increments,
self.local,
self.layer_bounding_box,
document_to_viewport,
pivot,
self.initial_transform,
)
self.transform_operation.grs_typed(self.typing.type_negate(), &mut selected, &self.state, document)
}
TransformLayerMessage::SetPivotGizmo { pivot_gizmo } => {
self.pivot_gizmo = pivot_gizmo;
@ -721,7 +644,7 @@ impl TransformLayerMessageHandler {
}
pub fn hints(&self, responses: &mut VecDeque<Message>) {
self.transform_operation.hints(responses, self.local);
self.transform_operation.hints(responses, self.state.is_transforming_in_local_space);
}
fn set_ghost_outline(ghost_outline: &mut Vec<(Vec<ClickTargetType>, DAffine2)>, shape_editor: &ShapeState, document: &DocumentMessageHandler) {
@ -797,26 +720,6 @@ fn calculate_pivot(
}
}
fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint: Axis) -> DVec2 {
match axis_constraint {
Axis::X => {
if local {
edge.project_onto(quad.top_right() - quad.top_left())
} else {
edge.with_y(0.)
}
}
Axis::Y => {
if local {
edge.project_onto(quad.bottom_left() - quad.top_left())
} else {
edge.with_x(0.)
}
}
_ => edge,
}
}
fn update_colinear_handles(selected_layers: &[LayerNodeIdentifier], document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for &layer in selected_layers {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };

View file

@ -505,7 +505,7 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")}
data-node={node.id}
>
<div class="thumbnail">
@ -650,7 +650,7 @@
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
title={`${node.displayName}\n\n${description || ""}`.trim() + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}, Position: (${node.position.x}, ${node.position.y})` : "")}
data-node={node.id}
>
<!-- Primary row -->

View file

@ -848,7 +848,7 @@ impl NodeNetwork {
// If the input to self is a node, connect the corresponding output of the inner network to it
NodeInput::Node { node_id, output_index } => {
nested_node.populate_first_network_input(node_id, output_index, nested_input_index, node.original_location.inputs(*import_index), 1);
let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("unable find input node {node_id:?}"));
let input_node = self.nodes.get_mut(&node_id).unwrap_or_else(|| panic!("Unable to find input node {node_id:?}"));
input_node.original_location.dependants[output_index].push(nested_node_id);
}
NodeInput::Import { import_index, .. } => {

View file

@ -305,7 +305,7 @@ impl ExtractAnimationTime for OwnedContextImpl {
}
impl ExtractIndex for OwnedContextImpl {
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
self.index.clone().map(|x| x.into_iter())
self.index.clone().map(|x| x.into_iter().rev())
}
}
impl ExtractVarArgs for OwnedContextImpl {

View file

@ -79,6 +79,8 @@ impl Convert<DVec2, ()> for DVec2 {
}
}
// TODO: Add a DVec2 to Table<Vector> anchor point conversion implementation to replace the 'Vec2 to Point' node
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty, $to:ty) => {

View file

@ -119,6 +119,55 @@ impl From<Table<GradientStops>> for Graphic {
// Local trait to convert types to Table<Graphic> (avoids orphan rule issues)
pub trait IntoGraphicTable {
fn into_graphic_table(self) -> Table<Graphic>;
/// Deeply flattens any vector content within a graphic table, discarding non-vector content, and returning a table of only vector elements.
fn into_flattened_vector_table(self) -> Table<Vector>
where
Self: std::marker::Sized,
{
let content = self.into_graphic_table();
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_vector_table: &mut Table<Vector>, current_graphic_table: Table<Graphic>) {
for current_graphic_row in current_graphic_table.iter() {
let current_graphic = current_graphic_row.element.clone();
let source_node_id = *current_graphic_row.source_node_id;
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
}
flatten_table(output_vector_table, current_graphic_table);
}
// Push any leaf Vector elements we encounter
Graphic::Vector(vector_table) => {
for current_vector_row in vector_table.iter() {
output_vector_table.push(TableRow {
element: current_vector_row.element.clone(),
transform: *current_graphic_row.transform * *current_vector_row.transform,
alpha_blending: AlphaBlending {
blend_mode: current_vector_row.alpha_blending.blend_mode,
opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity,
fill: current_vector_row.alpha_blending.fill,
clip: current_vector_row.alpha_blending.clip,
},
source_node_id,
});
}
}
_ => {}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content);
output
}
}
impl IntoGraphicTable for Table<Graphic> {
@ -284,6 +333,7 @@ impl RenderComplexity for Graphic {
pub trait AtIndex {
type Output;
fn at_index(&self, index: usize) -> Option<Self::Output>;
fn at_index_from_end(&self, index: usize) -> Option<Self::Output>;
}
impl<T: Clone> AtIndex for Vec<T> {
type Output = T;
@ -291,6 +341,10 @@ impl<T: Clone> AtIndex for Vec<T> {
fn at_index(&self, index: usize) -> Option<Self::Output> {
self.get(index).cloned()
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
if index == 0 || index > self.len() { None } else { self.get(self.len() - index).cloned() }
}
}
impl<T: Clone> AtIndex for Table<T> {
type Output = Table<T>;
@ -304,6 +358,18 @@ impl<T: Clone> AtIndex for Table<T> {
None
}
}
fn at_index_from_end(&self, index: usize) -> Option<Self::Output> {
let mut result_table = Self::default();
if index == 0 || index > self.len() {
None
} else if let Some(row) = self.iter().nth(self.len() - index) {
result_table.push(row.into_cloned());
Some(result_table)
} else {
None
}
}
}
// TODO: Eventually remove this migration document upgrade code

View file

@ -19,6 +19,10 @@ pub mod types {
pub type Length = f64;
/// 0 to 1
pub type Fraction = f64;
/// Non-negative number broken into whole and fractional parts
pub type Progression = f64;
/// Signed integer that's actually a float because we don't handle type conversions very well yet
pub type SignedInteger = f64;
/// Unsigned integer
pub type IntegerCount = u32;
/// Unsigned integer to be used for random seeds

View file

@ -1041,18 +1041,24 @@ impl<Upstream> Iterator for StrokePathIter<'_, Upstream> {
type Item = (Vec<ManipulatorGroup<PointId>>, bool);
fn next(&mut self) -> Option<Self::Item> {
let current_start = if let Some((index, _)) = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1) {
index
} else {
if !self.done_one {
self.done_one = true;
self.skip = 0;
}
self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() > 0)?.0
};
self.skip = current_start + 1;
let mut current_start = None;
// First iterate over the single connected points
if !self.done_one {
current_start = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1);
self.done_one = current_start.is_none();
self.skip = current_start.map_or(0, |(index, _)| index + 1);
}
// There will always be one (seeing as we checked above)
// If we've already done the single connected, then go through looking at multi connected
if current_start.is_none() {
current_start = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() > 0);
self.skip = current_start.map_or(self.points.len(), |(index, _)| index);
}
// If there is no starting point, exit
let current_start = current_start?.0;
// There will always be at least one segment connected to this one
let mut point_index = current_start;
let mut manipulators_list = Vec::new();
let mut in_handle = None;

View file

@ -1,15 +1,11 @@
use core_types::Color;
use core_types::{
Ctx,
blending::AlphaBlending,
table::{Table, TableRow},
uuid::NodeId,
};
use core_types::Ctx;
use core_types::registry::types::SignedInteger;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use glam::{DAffine2, DVec2};
use graphic_types::{
Artboard, Vector,
graphic::{Graphic, IntoGraphicTable},
};
use graphic_types::graphic::{Graphic, IntoGraphicTable};
use graphic_types::{Artboard, Vector};
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
@ -164,48 +160,8 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten
/// Converts a graphic table into a vector table by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))]
pub async fn flatten_vector(_: impl Ctx, content: Table<Graphic>) -> Table<Vector> {
// TODO: Avoid mutable reference, instead return a new Table<Graphic>?
fn flatten_table(output_vector_table: &mut Table<Vector>, current_graphic_table: Table<Graphic>) {
for current_graphic_row in current_graphic_table.iter() {
let current_graphic = current_graphic_row.element.clone();
let source_node_id = *current_graphic_row.source_node_id;
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
}
flatten_table(output_vector_table, current_graphic_table);
}
// Push any leaf Vector elements we encounter
Graphic::Vector(vector_table) => {
for current_vector_row in vector_table.iter() {
output_vector_table.push(TableRow {
element: current_vector_row.element.clone(),
transform: *current_graphic_row.transform * *current_vector_row.transform,
alpha_blending: AlphaBlending {
blend_mode: current_vector_row.alpha_blending.blend_mode,
opacity: current_graphic_row.alpha_blending.opacity * current_vector_row.alpha_blending.opacity,
fill: current_vector_row.alpha_blending.fill,
clip: current_vector_row.alpha_blending.clip,
},
source_node_id,
});
}
}
_ => {}
}
}
}
let mut output = Table::new();
flatten_table(&mut output, content);
output
pub async fn flatten_vector<I: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: I) -> Table<Vector> {
content.into_flattened_vector_table()
}
/// Returns the value at the specified index in the collection.
@ -229,11 +185,18 @@ pub fn index_elements<T: graphic_types::graphic::AtIndex + Clone + Default>(
Table<GradientStops>,
)]
collection: T,
/// The index of the item to retrieve, starting from 0 for the first item.
index: u32,
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item.
index: SignedInteger,
) -> T::Output
where
T::Output: Clone + Default,
{
collection.at_index(index as usize).unwrap_or_default()
let index = index as i32;
if index < 0 {
collection.at_index_from_end(-index as usize)
} else {
collection.at_index(index as usize)
}
.unwrap_or_default()
}

View file

@ -3,7 +3,7 @@ use core_types::color::Color;
use core_types::table::Table;
use core_types::transform::{ApplyTransform, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
@ -16,14 +16,14 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
#[implementations(
Context -> DAffine2,
Context -> DVec2,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Vector>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
)]
value: impl Node<Context<'static>, Output = T>,
content: impl Node<Context<'static>, Output = T>,
translation: DVec2,
rotation: f64,
scale: DVec2,
@ -41,24 +41,75 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = value.eval(ctx.into_context()).await;
let mut transform_target = content.eval(ctx.into_context()).await;
transform_target.left_apply_transform(&matrix);
transform_target
}
/// Overwrites the transform of each element in the input table with the specified transform.
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx + InjectFootprint,
#[implementations(Table<Vector>, Table<Raster<CPU>>, Table<Graphic>, Table<Color>, Table<GradientStops>)] mut data: Table<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Table<Data> {
for data_transform in data.iter_mut() {
*data_transform.transform = transform.transform();
/// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform.
/// Shear is represented jointly by rotation and scale, so resetting both will also remove any shear.
#[node_macro::node(category("Math: Transform"))]
fn reset_transform<T>(
_: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut content: Table<T>,
#[default(true)] reset_translation: bool,
reset_rotation: bool,
reset_scale: bool,
) -> Table<T> {
for row in content.iter_mut() {
// Translation
if reset_translation {
row.transform.translation = DVec2::ZERO;
}
// (Rotation, Scale)
match (reset_rotation, reset_scale) {
(true, true) => {
row.transform.matrix2 = DMat2::IDENTITY;
}
(true, false) => {
let scale = row.transform.decompose_scale();
row.transform.matrix2 = DMat2::from_diagonal(scale);
}
(false, true) => {
let rotation = row.transform.decompose_rotation();
let rotation_matrix = DMat2::from_angle(rotation);
row.transform.matrix2 = rotation_matrix;
}
(false, false) => {}
}
}
data
content
}
/// Overwrites the transform of each element in the input table with the specified transform.
#[node_macro::node(category("Math: Transform"))]
fn replace_transform<T>(
_: impl Ctx + InjectFootprint,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
)]
mut content: Table<T>,
transform: DAffine2,
) -> Table<T> {
for row in content.iter_mut() {
*row.transform = transform.transform();
}
content
}
// TODO: Figure out how this node should behave once #2982 is implemented.
@ -74,9 +125,9 @@ async fn extract_transform<T>(
Table<Color>,
Table<GradientStops>,
)]
vector: Table<T>,
content: Table<T>,
) -> DAffine2 {
vector.iter().next().map(|row| *row.transform).unwrap_or_default()
content.iter().next().map(|row| *row.transform).unwrap_or_default()
}
/// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform.

View file

@ -1,14 +1,14 @@
use core::f64::consts::PI;
use core::hash::{Hash, Hasher};
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use core_types::registry::types::{Angle, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, Progression, SeedValue};
use core_types::table::{Table, TableRow, TableRowMut};
use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Graphic, IntoGraphicTable};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
@ -234,7 +234,7 @@ async fn repeat<I: 'n + Send + Clone>(
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
direction: PixelSize,
angle: Angle,
#[default(4)] count: IntegerCount,
#[default(5)] count: IntegerCount,
) -> Table<I> {
let angle = angle.to_radians();
let count = count.max(1);
@ -825,6 +825,7 @@ async fn dimensions(_: impl Ctx, content: Table<Vector>) -> DVec2 {
.unwrap_or_default()
}
// TODO: Replace this node with an automatic type conversion implementation of the `Convert` trait
/// Converts a vec2 value into a vector path composed of a single anchor point.
///
/// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points.
@ -848,12 +849,12 @@ async fn points_to_polyline(_: impl Ctx, mut points: Table<Vector>, #[default(tr
let points_count = row.element.point_domain.ids().len();
if points_count > 2 {
if points_count >= 2 {
(0..points_count - 1).for_each(|i| {
segment_domain.push(next_id.next_id(), i, i + 1, BezierHandles::Linear, StrokeId::generate());
});
if closed {
if closed && points_count != 2 {
segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate());
row.element
@ -1146,7 +1147,17 @@ async fn sample_polyline(
///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn cut_path(_: impl Ctx, mut content: Table<Vector>, progression: Fraction, parameterized_distance: bool, reverse: bool) -> Table<Vector> {
async fn cut_path(
_: impl Ctx,
/// The path to insert a cut into.
mut content: Table<Vector>,
/// The factor from the start to the end of the path, 01 for one subpath, 12 for a second subpath, and so on.
progression: Progression,
/// Swap the direction of the path.
reverse: bool,
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
parameterized_distance: bool,
) -> Table<Vector> {
let euclidian = !parameterized_distance;
let bezpaths = content
@ -1251,7 +1262,7 @@ async fn position_on_path(
/// The path to traverse.
content: Table<Vector>,
/// The factor from the start to the end of the path, 01 for one subpath, 12 for a second subpath, and so on.
progression: Fraction,
progression: Progression,
/// Swap the direction of the path.
reverse: bool,
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
@ -1290,7 +1301,7 @@ async fn tangent_on_path(
/// The path to traverse.
content: Table<Vector>,
/// The factor from the start to the end of the path, 01 for one subpath, 12 for a second subpath, and so on.
progression: Fraction,
progression: Progression,
/// Swap the direction of the path.
reverse: bool,
/// Traverse the path using each segment's Bézier curve parameterization instead of the Euclidean distance. Faster to compute but doesn't respect actual distances.
@ -1512,8 +1523,18 @@ async fn jitter_points(
.collect()
}
/// Interpolates the geometry and styles between multiple vector layers, producing a single morphed vector shape.
///
/// Based on the progression value, adjacent vector elements are blended together. From 0 until 1, the first element (bottom layer) morphs into the second element (next layer up). From 1 until 2, it then morphs into the third element, and so on until progression is capped at the last element (top layer).
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vector>, #[default(0.5)] time: Fraction) -> Table<Vector> {
async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
_: impl Ctx,
/// The vector elements to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
#[implementations(Table<Graphic>, Table<Vector>)]
content: I,
/// The factor from one vector element to the next in sequence. The whole number part selects the source element, and the decimal part determines the interpolation amount towards the next element.
progression: Progression,
) -> Table<Vector> {
/// Subdivides the last segment of the bezpath to until it appends 'count' number of segments.
fn make_new_segments(bezpath: &mut BezPath, count: usize) {
let bezpath_segment_count = bezpath.segments().count();
@ -1557,127 +1578,147 @@ async fn morph(_: impl Ctx, source: Table<Vector>, #[expose] target: Table<Vecto
}
}
let time = time.clamp(0., 1.);
// Preserve original graphic table as upstream data so this group layer's nested layers can be edited by the tools.
let graphic_table_content = content.clone().into_graphic_table();
source
.into_iter()
.zip(target.into_iter())
.map(|(source_row, target_row)| {
let mut vector = Vector::default();
// If the input isn't a Table<Vector>, we convert it into one by flattening any Table<Graphic> content.
let content = content.into_flattened_vector_table();
// Lerp styles
let vector_alpha_blending = source_row.alpha_blending.lerp(&target_row.alpha_blending, time as f32);
vector.style = source_row.element.style.lerp(&target_row.element.style, time);
// Determine source and target indices and interpolation time fraction
let progression = progression.max(0.);
let source_index = progression.floor() as usize;
let time = progression.fract();
// Before and after transforms
let source_transform = source_row.transform;
let target_transform = target_row.transform;
// Not enough elements to interpolate between, so we return the input as-is
if content.len() <= 1 {
return content;
}
// Progression is at or past the last element, so we return the last element without interpolation
if source_index >= content.len() - 1 {
return content.into_iter().last().into_iter().collect();
}
// Before and after paths
let source_bezpaths = source_row.element.stroke_bezpath_iter();
let target_bezpaths = target_row.element.stroke_bezpath_iter();
// Interpolation between two elements
let mut content_iter = content.into_iter();
let source_row = content_iter.nth(source_index).unwrap();
let target_row = content_iter.next().unwrap();
for (mut source_bezpath, mut target_bezpath) in source_bezpaths.zip(target_bezpaths) {
if source_bezpath.elements().is_empty() || target_bezpath.elements().is_empty() {
continue;
let mut vector = Vector {
upstream_data: Some(graphic_table_content),
..Default::default()
};
// Lerp styles
let vector_alpha_blending = source_row.alpha_blending.lerp(&target_row.alpha_blending, time as f32);
vector.style = source_row.element.style.lerp(&target_row.element.style, time);
// Before and after transforms
let source_transform = source_row.transform;
let target_transform = target_row.transform;
// Before and after paths
let source_bezpaths = source_row.element.stroke_bezpath_iter();
let target_bezpaths = target_row.element.stroke_bezpath_iter();
for (mut source_bezpath, mut target_bezpath) in source_bezpaths.zip(target_bezpaths) {
if source_bezpath.elements().is_empty() || target_bezpath.elements().is_empty() {
continue;
}
source_bezpath.apply_affine(Affine::new(source_transform.to_cols_array()));
target_bezpath.apply_affine(Affine::new(target_transform.to_cols_array()));
let target_segment_len = target_bezpath.segments().count();
let source_segment_len = source_bezpath.segments().count();
// Insert new segments to align the number of segments in sorce_bezpath and target_bezpath.
make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len);
make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len);
let source_segments = source_bezpath.segments().collect::<Vec<PathSeg>>();
let target_segments = target_bezpath.segments().collect::<Vec<PathSeg>>();
// Interpolate anchors and handles
for (i, (source_element, target_element)) in source_bezpath.elements_mut().iter_mut().zip(target_bezpath.elements_mut().iter_mut()).enumerate() {
match source_element {
PathEl::MoveTo(point) => *point = point.lerp(target_element.end_point().unwrap(), time),
PathEl::ClosePath => {}
elm => {
let mut source_segment = source_segments.get(i - 1).unwrap().to_cubic();
let target_segment = target_segments.get(i - 1).unwrap().to_cubic();
source_segment.p0 = source_segment.p0.lerp(target_segment.p0, time);
source_segment.p1 = source_segment.p1.lerp(target_segment.p1, time);
source_segment.p2 = source_segment.p2.lerp(target_segment.p2, time);
source_segment.p3 = source_segment.p3.lerp(target_segment.p3, time);
*elm = PathSeg::Cubic(source_segment).as_path_el();
}
}
}
source_bezpath.apply_affine(Affine::new(source_transform.to_cols_array()));
target_bezpath.apply_affine(Affine::new(target_transform.to_cols_array()));
vector.append_bezpath(source_bezpath.clone());
}
let target_segment_len = target_bezpath.segments().count();
let source_segment_len = source_bezpath.segments().count();
// Deal with unmatched extra paths by collapsing them
let source_paths_count = source_row.element.stroke_bezpath_iter().count();
let target_paths_count = target_row.element.stroke_bezpath_iter().count();
let source_paths = source_row.element.stroke_bezpath_iter().skip(target_paths_count);
let target_paths = target_row.element.stroke_bezpath_iter().skip(source_paths_count);
// Insert new segments to align the number of segments in sorce_bezpath and target_bezpath.
make_new_segments(&mut source_bezpath, target_segment_len.max(source_segment_len) - source_segment_len);
make_new_segments(&mut target_bezpath, source_segment_len.max(target_segment_len) - target_segment_len);
for mut source_path in source_paths {
source_path.apply_affine(Affine::new(source_transform.to_cols_array()));
let source_segments = source_bezpath.segments().collect::<Vec<PathSeg>>();
let target_segments = target_bezpath.segments().collect::<Vec<PathSeg>>();
// Skip if the path has no segments else get the point at the end of the path.
let Some(end) = source_path.segments().last().map(|element| element.end()) else { continue };
// Interpolate anchors and handles
for (i, (source_element, target_element)) in source_bezpath.elements_mut().iter_mut().zip(target_bezpath.elements_mut().iter_mut()).enumerate() {
match source_element {
PathEl::MoveTo(point) => *point = point.lerp(target_element.end_point().unwrap(), time),
PathEl::ClosePath => {}
elm => {
let mut source_segment = source_segments.get(i - 1).unwrap().to_cubic();
let target_segment = target_segments.get(i - 1).unwrap().to_cubic();
source_segment.p0 = source_segment.p0.lerp(target_segment.p0, time);
source_segment.p1 = source_segment.p1.lerp(target_segment.p1, time);
source_segment.p2 = source_segment.p2.lerp(target_segment.p2, time);
source_segment.p3 = source_segment.p3.lerp(target_segment.p3, time);
*elm = PathSeg::Cubic(source_segment).as_path_el();
}
}
for element in source_path.elements_mut() {
match element {
PathEl::MoveTo(point) => *point = point.lerp(end, time),
PathEl::LineTo(point) => *point = point.lerp(end, time),
PathEl::QuadTo(point, point1) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
}
vector.append_bezpath(source_bezpath.clone());
}
// Deal with unmatched extra paths by collapsing them
let source_paths_count = source_row.element.stroke_bezpath_iter().count();
let target_paths_count = target_row.element.stroke_bezpath_iter().count();
let source_paths = source_row.element.stroke_bezpath_iter().skip(target_paths_count);
let target_paths = target_row.element.stroke_bezpath_iter().skip(source_paths_count);
for mut source_path in source_paths {
source_path.apply_affine(Affine::new(source_transform.to_cols_array()));
// Skip if the path has no segments else get the point at the end of the path.
let Some(end) = source_path.segments().last().map(|element| element.end()) else { continue };
for element in source_path.elements_mut() {
match element {
PathEl::MoveTo(point) => *point = point.lerp(end, time),
PathEl::LineTo(point) => *point = point.lerp(end, time),
PathEl::QuadTo(point, point1) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
}
PathEl::CurveTo(point, point1, point2) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
*point2 = point2.lerp(end, time);
}
PathEl::ClosePath => {}
}
PathEl::CurveTo(point, point1, point2) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
*point2 = point2.lerp(end, time);
}
vector.append_bezpath(source_path);
PathEl::ClosePath => {}
}
}
vector.append_bezpath(source_path);
}
for mut target_path in target_paths {
target_path.apply_affine(Affine::new(source_transform.to_cols_array()));
for mut target_path in target_paths {
target_path.apply_affine(Affine::new(source_transform.to_cols_array()));
// Skip if the path has no segments else get the point at the start of the path.
let Some(start) = target_path.segments().next().map(|element| element.start()) else { continue };
// Skip if the path has no segments else get the point at the start of the path.
let Some(start) = target_path.segments().next().map(|element| element.start()) else { continue };
for element in target_path.elements_mut() {
match element {
PathEl::MoveTo(point) => *point = start.lerp(*point, time),
PathEl::LineTo(point) => *point = start.lerp(*point, time),
PathEl::QuadTo(point, point1) => {
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
}
PathEl::CurveTo(point, point1, point2) => {
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
*point2 = start.lerp(*point2, time);
}
PathEl::ClosePath => {}
}
for element in target_path.elements_mut() {
match element {
PathEl::MoveTo(point) => *point = start.lerp(*point, time),
PathEl::LineTo(point) => *point = start.lerp(*point, time),
PathEl::QuadTo(point, point1) => {
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
}
vector.append_bezpath(target_path);
PathEl::CurveTo(point, point1, point2) => {
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
*point2 = start.lerp(*point2, time);
}
PathEl::ClosePath => {}
}
}
vector.append_bezpath(target_path);
}
TableRow {
element: vector,
alpha_blending: vector_alpha_blending,
..Default::default()
}
})
.collect()
Table::new_from_row(TableRow {
element: vector,
alpha_blending: vector_alpha_blending,
..Default::default()
})
}
fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector {
@ -2007,9 +2048,47 @@ async fn count_elements<I: Count>(
Vec<f64>,
Vec<DVec2>,
)]
source: I,
content: I,
) -> f64 {
source.count() as f64
content.count() as f64
}
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn count_points(_: impl Ctx, content: Table<Vector>) -> f64 {
content.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum()
}
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in table of vector elements.
/// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn index_points(
_: impl Ctx,
/// The vector element or elements containing the anchor points to be retrieved.
content: Table<Vector>,
/// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item.
index: f64,
) -> DVec2 {
let points_count = content.iter().map(|row| row.element.point_domain.positions().len()).sum::<usize>();
// Clamp and allow negative indexing from the end
let index = index as isize;
let index = if index < 0 {
(points_count as isize + index).max(0) as usize
} else {
(index as usize).min(points_count - 1)
};
// Find the point at the given index across all vector elements
let mut accumulated = 0;
for row in content.iter() {
let row_point_count = row.element.point_domain.positions().len();
if index - accumulated < row_point_count {
return row.element.point_domain.positions()[index - accumulated];
}
accumulated += row_point_count;
}
DVec2::ZERO
}
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
@ -2304,12 +2383,12 @@ mod test {
}
#[tokio::test]
async fn morph() {
let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY);
let target = Rect::new(-100., -100., 0., 0.).to_path(DEFAULT_ACCURACY);
let morphed = super::morph(Footprint::default(), vector_node_from_bezpath(source), vector_node_from_bezpath(target), 0.5).await;
let morphed = morphed.iter().next().unwrap().element;
let rectangle = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
let rectangles = super::repeat(Footprint::default(), rectangle, DVec2::new(-100., -100.), 0., 2).await;
let morphed = super::morph(Footprint::default(), rectangles, 0.5).await;
let element = morphed.iter().next().unwrap().element;
assert_eq!(
&morphed.point_domain.positions()[..4],
&element.point_domain.positions()[..4],
vec![DVec2::new(-50., -50.), DVec2::new(50., -50.), DVec2::new(50., 50.), DVec2::new(-50., 50.)]
);
}