Improve tooltip docs with Markdown styling and refined math node explanations (#3488)

This commit is contained in:
Keavon Chambers 2025-12-20 01:05:15 -08:00 committed by GitHub
parent 2c21e1a90b
commit f1e8ebefc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 276 additions and 185 deletions

View File

@ -70,6 +70,9 @@ pub enum FrontendMessage {
SendShortcutAltClick { SendShortcutAltClick {
shortcut: Option<ActionShortcut>, shortcut: Option<ActionShortcut>,
}, },
SendShortcutShiftClick {
shortcut: Option<ActionShortcut>,
},
// Trigger prefix: cause a frontend specific API to do something // Trigger prefix: cause a frontend specific API to do something
TriggerAboutGraphiteLocalizedCommitDate { TriggerAboutGraphiteLocalizedCommitDate {

View File

@ -274,7 +274,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}, },
}, },
description: Cow::Borrowed("Merges new content as an entry into the graphic table that represents a layer compositing stack."), description: Cow::Borrowed("Merges the provided content as a new element in the graphic table that represents a layer compositing stack."),
properties: None, properties: None,
}, },
DocumentNodeDefinition { DocumentNodeDefinition {

View File

@ -124,6 +124,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::SendShortcutAltClick { responses.add(FrontendMessage::SendShortcutAltClick {
shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft), shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft),
}); });
responses.add(FrontendMessage::SendShortcutShiftClick {
shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft),
});
// Before loading any documents, initially prepare the welcome screen buttons layout // Before loading any documents, initially prepare the welcome screen buttons layout
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);

View File

@ -169,6 +169,7 @@ pub struct PivotGizmoState {
impl PivotGizmoState { impl PivotGizmoState {
pub fn is_pivot_type(&self) -> bool { pub fn is_pivot_type(&self) -> bool {
// A disabled pivot is considered a pivot-type gizmo that is always centered
self.gizmo_type == PivotGizmoType::Pivot || self.disabled self.gizmo_type == PivotGizmoType::Pivot || self.disabled
} }

View File

@ -27,7 +27,7 @@
// State provider systems // State provider systems
let dialog = createDialogState(editor); let dialog = createDialogState(editor);
setContext("dialog", dialog); setContext("dialog", dialog);
let tooltip = createTooltipState(); let tooltip = createTooltipState(editor);
setContext("tooltip", tooltip); setContext("tooltip", tooltip);
let document = createDocumentState(editor); let document = createDocumentState(editor);
setContext("document", document); setContext("document", document);

View File

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, createEventDispatcher } from "svelte"; import { getContext, onDestroy, createEventDispatcher } from "svelte";
import type { HSV, RGB, FillChoice } from "@graphite/messages"; import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
import type { MenuDirection } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages"; import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import { clamp } from "@graphite/utility-functions/math"; import { clamp } from "@graphite/utility-functions/math";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte"; import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@ -40,6 +40,7 @@
]; ];
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>(); const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
const tooltip = getContext<TooltipState>("tooltip");
export let colorOrGradient: FillChoice; export let colorOrGradient: FillChoice;
export let allowNone = false; export let allowNone = false;
@ -424,12 +425,16 @@
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(), "--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
}} }}
> >
{@const hueDescription = "The shade along the spectrum of the rainbow."}
{@const saturationDescription = "The vividness from grayscale to full color."}
{@const valueDescription = "The brightness from black to full color."}
<LayoutCol class="pickers-and-gradient"> <LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers"> <LayoutRow class="pickers">
<LayoutCol <LayoutCol
class="saturation-value-picker" class="saturation-value-picker"
data-tooltip-label="Saturation and Value" data-tooltip-label="Saturation and Value"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""} data-tooltip-description={`To move only along the saturation (X) or value (Y) axis, perform the shortcut shown.${disabled ? "\n\nDisabled (read-only)." : ""}`}
data-tooltip-shortcut={$tooltip.shiftClickShortcut?.shortcut ? JSON.stringify($tooltip.shiftClickShortcut.shortcut) : undefined}
on:pointerdown={onPointerDown} on:pointerdown={onPointerDown}
data-saturation-value-picker data-saturation-value-picker
> >
@ -449,7 +454,7 @@
<LayoutCol <LayoutCol
class="hue-picker" class="hue-picker"
data-tooltip-label="Hue" data-tooltip-label="Hue"
data-tooltip-description={`The shade along the spectrum of the rainbow.${disabled ? "\n\nDisabled (read-only)." : ""}`} data-tooltip-description={`${hueDescription}${disabled ? "\n\nDisabled (read-only)." : ""}`}
on:pointerdown={onPointerDown} on:pointerdown={onPointerDown}
data-hue-picker data-hue-picker
> >
@ -522,10 +527,8 @@
</LayoutRow> </LayoutRow>
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> --> <!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> -->
<LayoutRow> <LayoutRow>
<TextLabel {@const hexDescription = "Color code in hexadecimal format. 6 digits if opaque, 8 with alpha. Accepts input of CSS color values including named colors."}
tooltipLabel="Hex Color Code" <TextLabel tooltipLabel="Hex Color Code" tooltipDescription={hexDescription}>Hex</TextLabel>
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors.">Hex</TextLabel
>
<Separator type="Related" /> <Separator type="Related" />
<LayoutRow> <LayoutRow>
<TextInput <TextInput
@ -537,7 +540,7 @@
}} }}
centered={true} centered={true}
tooltipLabel="Hex Color Code" tooltipLabel="Hex Color Code"
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors." tooltipDescription={hexDescription}
bind:this={hexCodeInputWidget} bind:this={hexCodeInputWidget}
/> />
</LayoutRow> </LayoutRow>
@ -601,16 +604,17 @@
v: "Value Component", v: "Value Component",
}[channel]} }[channel]}
tooltipDescription={{ tooltipDescription={{
h: "The shade along the spectrum of the rainbow.", h: hueDescription,
s: "The vividness from grayscale to full color.", s: saturationDescription,
v: "The brightness from black to full color.", v: valueDescription,
}[channel]} }[channel]}
/> />
{/each} {/each}
</LayoutRow> </LayoutRow>
</LayoutRow> </LayoutRow>
<LayoutRow> <LayoutRow>
<TextLabel tooltipLabel="Alpha" tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%).">Alpha</TextLabel> {@const alphaDescription = "The level of translucency, from transparent (0%) to opaque (100%)."}
<TextLabel tooltipLabel="Alpha" tooltipDescription={alphaDescription}>Alpha</TextLabel>
<Separator type="Related" /> <Separator type="Related" />
<NumberInput <NumberInput
value={!isNone ? alpha * 100 : undefined} value={!isNone ? alpha * 100 : undefined}
@ -630,7 +634,7 @@
mode="Range" mode="Range"
displayDecimalPlaces={1} displayDecimalPlaces={1}
tooltipLabel="Alpha" tooltipLabel="Alpha"
tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%)." tooltipDescription={alphaDescription}
/> />
</LayoutRow> </LayoutRow>
<LayoutRow class="leftover-space" /> <LayoutRow class="leftover-space" />
@ -670,7 +674,7 @@
data-pure-tile={name.toLowerCase()} data-pure-tile={name.toLowerCase()}
style:--pure-color={color} style:--pure-color={color}
style:--pure-color-gray={gray} style:--pure-color-gray={gray}
data-tooltip-label="Set to Red" data-tooltip-label={`Set to ${name}`}
data-tooltip-description={disabled ? "Disabled (read-only)." : ""} data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
/> />
{/each} {/each}

View File

@ -15,8 +15,8 @@
let self: FloatingMenu | undefined; let self: FloatingMenu | undefined;
$: label = filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim()); $: label = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim()));
$: description = filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim()); $: description = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim()));
$: shortcutJSON = $tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim(); $: shortcutJSON = $tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim();
$: shortcut = ((shortcutJSON) => { $: shortcut = ((shortcutJSON) => {
if (!shortcutJSON) return undefined; if (!shortcutJSON) return undefined;
@ -32,6 +32,26 @@
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return ""; if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
return text; return text;
} }
function parseMarkdown(markdown: string | undefined): string | undefined {
if (!markdown) return undefined;
return (
markdown
// .split("\n")
// .map((line) => line.trim())
// .join("\n")
// .split("\n\n")
// .map((paragraph) => paragraph.replaceAll("\n", " "))
// .join("\n\n")
// Bold
.replace(/\*\*((?:(?!\*\*).)+)\*\*/g, "<strong>$1</strong>")
// Italic
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Backticks
.replace(/`([^`]+)`/g, "<code>$1</code>")
);
}
</script> </script>
{#if label || description} {#if label || description}
@ -40,7 +60,7 @@
{#if label || shortcut} {#if label || shortcut}
<LayoutRow class="tooltip-header"> <LayoutRow class="tooltip-header">
{#if label} {#if label}
<TextLabel class="tooltip-label">{label}</TextLabel> <TextLabel class="tooltip-label">{@html label}</TextLabel>
{/if} {/if}
{#if shortcut} {#if shortcut}
<ShortcutLabel shortcut={{ shortcut }} /> <ShortcutLabel shortcut={{ shortcut }} />
@ -48,7 +68,7 @@
</LayoutRow> </LayoutRow>
{/if} {/if}
{#if description} {#if description}
<TextLabel class="tooltip-description">{description}</TextLabel> <TextLabel class="tooltip-description">{@html description}</TextLabel>
{/if} {/if}
</FloatingMenu> </FloatingMenu>
</div> </div>

View File

@ -9,10 +9,10 @@
UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarLeftLayout,
UpdateLayersPanelControlBarRightLayout, UpdateLayersPanelControlBarRightLayout,
UpdateLayersPanelBottomBarLayout, UpdateLayersPanelBottomBarLayout,
SendShortcutAltClick,
} from "@graphite/messages"; } from "@graphite/messages";
import type { ActionShortcut, DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages"; import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import { operatingSystem } from "@graphite/utility-functions/platform"; import { operatingSystem } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization"; import { extractPixelData } from "@graphite/utility-functions/rasterization";
@ -49,6 +49,7 @@
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph"); const nodeGraph = getContext<NodeGraphState>("nodeGraph");
const tooltip = getContext<TooltipState>("tooltip");
let list: LayoutCol | undefined; let list: LayoutCol | undefined;
@ -73,13 +74,7 @@
let layersPanelControlBarRightLayout: Layout = []; let layersPanelControlBarRightLayout: Layout = [];
let layersPanelBottomBarLayout: Layout = []; let layersPanelBottomBarLayout: Layout = [];
let altClickShortcut: ActionShortcut | undefined;
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
altClickShortcut = data.shortcut;
});
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => { editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
patchLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout); patchLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout; layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
@ -628,7 +623,7 @@
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)" ? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") + : "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")} (listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined} data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined}
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)} on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0" tabindex="0"
></button> ></button>
@ -639,8 +634,9 @@
<IconLabel <IconLabel
icon="Clipped" icon="Clipped"
class="clipped-arrow" class="clipped-arrow"
tooltipDescription="Clipping mask is active. To release it, perform the shortcut on the layer border." tooltipLabel="Layer Clipped"
tooltipShortcut={altClickShortcut} tooltipDescription="Clipping mask is active. To release it, target the bottom border of the layer and perform the shortcut shown."
tooltipShortcut={$tooltip.altClickShortcut}
/> />
{/if} {/if}
<div class="thumbnail"> <div class="thumbnail">

View File

@ -501,7 +501,7 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth} style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0} style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`} data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={` data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""} ${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()} `.trim()}
@ -651,7 +651,7 @@
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`} data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={` data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""} ${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()} `.trim()}

View File

@ -72,7 +72,8 @@
font-style: italic; font-style: italic;
} }
&.monospace { &.monospace,
code {
font-family: "Source Code Pro", monospace; font-family: "Source Code Pro", monospace;
font-size: 12px; font-size: 12px;
} }
@ -94,5 +95,10 @@
a { a {
color: inherit; color: inherit;
} }
code {
background: var(--color-3-darkgray);
padding: 0 2px;
}
} }
</style> </style>

View File

@ -1,24 +1,15 @@
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from "svelte"; import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { ActionShortcut } from "@graphite/messages";
import { SendShortcutF11 } from "@graphite/messages";
import type { FullscreenState } from "@graphite/state-providers/fullscreen"; import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
const fullscreen = getContext<FullscreenState>("fullscreen"); const fullscreen = getContext<FullscreenState>("fullscreen");
const editor = getContext<Editor>("editor"); const tooltip = getContext<TooltipState>("tooltip");
let f11Shortcut: ActionShortcut | undefined = undefined;
onMount(() => {
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
f11Shortcut = data.shortcut;
});
});
async function handleClick() { async function handleClick() {
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen(); if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
@ -31,7 +22,7 @@
on:click={handleClick} on:click={handleClick}
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"} tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""} tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""}
tooltipShortcut={f11Shortcut} tooltipShortcut={$tooltip.f11Shortcut}
> >
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} /> <IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
</LayoutRow> </LayoutRow>

View File

@ -481,11 +481,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
The browser's clipboard permission has been denied. The browser's clipboard permission has been denied.
Open the browser's website settings (usually accessible Open the browser's website settings (usually accessible
just left of the URL) to allow this permission. just left of the URL bar) to allow this permission.
`; `;
const nothing = stripIndents` const nothing = stripIndents`
No valid clipboard data was found. You may have better No valid clipboard data was found. You may have better
luck pasting with the standard keyboard shortcut instead. success pasting with the standard keyboard shortcut instead.
`; `;
const matchMessage = { const matchMessage = {

View File

@ -119,6 +119,11 @@ export class SendShortcutAltClick extends JsMessage {
readonly shortcut!: ActionShortcut | undefined; readonly shortcut!: ActionShortcut | undefined;
} }
export class SendShortcutShiftClick extends JsMessage {
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
readonly shortcut!: ActionShortcut | undefined;
}
export class UpdateNodeThumbnail extends JsMessage { export class UpdateNodeThumbnail extends JsMessage {
readonly id!: bigint; readonly id!: bigint;
@ -1696,6 +1701,7 @@ export const messageMakers: Record<string, MessageMaker> = {
SendUIMetadata, SendUIMetadata,
SendShortcutF11, SendShortcutF11,
SendShortcutAltClick, SendShortcutAltClick,
SendShortcutShiftClick,
TriggerAboutGraphiteLocalizedCommitDate, TriggerAboutGraphiteLocalizedCommitDate,
TriggerDisplayThirdPartyLicensesDialog, TriggerDisplayThirdPartyLicensesDialog,
TriggerExportImage, TriggerExportImage,

View File

@ -9,7 +9,7 @@ export function createAppWindowState(editor: Editor) {
maximized: false, maximized: false,
fullscreen: false, fullscreen: false,
viewportHolePunch: false, viewportHolePunch: false,
uiScale: 1.0, uiScale: 1,
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation

View File

@ -1,12 +1,18 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor";
import { SendShortcutAltClick, SendShortcutF11, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages";
const SHOW_TOOLTIP_DELAY_MS = 500; const SHOW_TOOLTIP_DELAY_MS = 500;
export function createTooltipState() { export function createTooltipState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
visible: false, visible: false,
element: undefined as Element | undefined, element: undefined as Element | undefined,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
shiftClickShortcut: undefined as ActionShortcut | undefined,
altClickShortcut: undefined as ActionShortcut | undefined,
f11Shortcut: undefined as ActionShortcut | undefined,
}); });
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined; let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
@ -45,6 +51,25 @@ export function createTooltipState() {
}, SHOW_TOOLTIP_DELAY_MS); }, SHOW_TOOLTIP_DELAY_MS);
}); });
editor.subscriptions.subscribeJsMessage(SendShortcutShiftClick, async (data) => {
update((state) => {
state.shiftClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
update((state) => {
state.altClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
update((state) => {
state.f11Shortcut = data.shortcut;
return state;
});
});
document.addEventListener("mousedown", closeTooltip); document.addEventListener("mousedown", closeTooltip);
document.addEventListener("keydown", closeTooltip); document.addEventListener("keydown", closeTooltip);

View File

@ -7,7 +7,7 @@ use glam::DVec2;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
use std::ops::Sub; use std::ops::Sub;
/// Represents different ways of calculating the centroid. /// Represents different geometric interpretations of calculating the centroid (center of mass).
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)] #[widget(Radio)]
pub enum CentroidType { pub enum CentroidType {

View File

@ -194,7 +194,6 @@ impl From<Fill> for FillChoice {
} }
} }
/// Enum describing the type of [Fill].
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)] #[widget(Radio)]

View File

@ -38,7 +38,7 @@ fn math<T: num_traits::float::Float>(
/// The value of "A" when calculating the expression. /// The value of "A" when calculating the expression.
#[implementations(f64, f32)] #[implementations(f64, f32)]
operand_a: T, operand_a: T,
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2". /// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
#[default(A + B)] #[default(A + B)]
expression: String, expression: String,
/// The value of "B" when calculating the expression. /// The value of "B" when calculating the expression.
@ -76,98 +76,100 @@ fn math<T: num_traits::float::Float>(
} }
} }
/// The addition operation (+) calculates the sum of two numbers. /// The addition operation (`+`) calculates the sum of two scalar numbers or vectors.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>( fn add<A: Add<B>, B>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the addition operation. /// The left-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[implementations(f64, f32, u32, DVec2, f64, DVec2)]
augend: U, augend: A,
/// The right-hand side of the addition operation. /// The right-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] #[implementations(f64, f32, u32, DVec2, DVec2, f64)]
addend: T, addend: B,
) -> <U as Add<T>>::Output { ) -> <A as Add<B>>::Output {
augend + addend augend + addend
} }
/// The subtraction operation (-) calculates the difference between two numbers. /// The subtraction operation (`-`) calculates the difference between two scalar numbers or vectors.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>( fn subtract<A: Sub<B>, B>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the subtraction operation. /// The left-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[implementations(f64, f32, u32, DVec2, f64, DVec2)]
minuend: U, minuend: A,
/// The right-hand side of the subtraction operation. /// The right-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] #[implementations(f64, f32, u32, DVec2, DVec2, f64)]
subtrahend: T, subtrahend: B,
) -> <U as Sub<T>>::Output { ) -> <A as Sub<B>>::Output {
minuend - subtrahend minuend - subtrahend
} }
/// The multiplication operation (×) calculates the product of two numbers. /// The multiplication operation (`×`) calculates the product of two scalar numbers, vectors, or transforms.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>( fn multiply<A: Mul<B>, B>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the multiplication operation. /// The left-hand side of the multiplication operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2, DAffine2)] #[implementations(f64, f32, u32, DVec2, f64, DVec2, DAffine2)]
multiplier: U, multiplier: A,
/// The right-hand side of the multiplication operation. /// The right-hand side of the multiplication operation.
#[default(1.)] #[default(1.)]
#[implementations(f64, f32, u32, DVec2, DVec2, f64, DAffine2)] #[implementations(f64, f32, u32, DVec2, DVec2, f64, DAffine2)]
multiplicand: T, multiplicand: B,
) -> <U as Mul<T>>::Output { ) -> <A as Mul<B>>::Output {
multiplier * multiplicand multiplier * multiplicand
} }
/// The division operation (÷) calculates the quotient of two numbers. /// The division operation (`÷`) calculates the quotient of two scalar numbers or vectors.
/// ///
/// Produces 0 if the denominator is 0. /// Produces 0 if the denominator is 0.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>( fn divide<A: Div<B> + Default + PartialEq, B: Default + PartialEq>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the division operation. /// The left-hand side of the division operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] #[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U, numerator: A,
/// The right-hand side of the division operation. /// The right-hand side of the division operation.
#[default(1.)] #[default(1.)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[implementations(f64, f32, u32, DVec2, f64, DVec2)]
denominator: T, denominator: B,
) -> <U as Div<T>>::Output ) -> <A as Div<B>>::Output
where where
<U as Div<T>>::Output: Default, <A as Div<B>>::Output: Default,
{ {
if denominator == T::default() { if denominator == B::default() {
return <U as Div<T>>::Output::default(); return <A as Div<B>>::Output::default();
} }
numerator / denominator numerator / denominator
} }
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled. /// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors.
///
/// The sign of the result shares the sign of the numerator unless *Always Positive* is enabled.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>( fn modulo<A: Rem<B, Output: Add<B, Output: Rem<B, Output = A::Output>>>, B: Copy>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the modulo operation. /// The left-hand side of the modulo operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] #[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U, numerator: A,
/// The right-hand side of the modulo operation. /// The right-hand side of the modulo operation.
#[default(2.)] #[default(2.)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[implementations(f64, f32, u32, DVec2, f64, DVec2)]
modulus: T, modulus: B,
/// Ensures the result will always be positive, even if the numerator is negative. /// Ensures the result is always positive, even if the numerator is negative.
#[default(true)] #[default(true)]
always_positive: bool, always_positive: bool,
) -> <U as Rem<T>>::Output { ) -> <A as Rem<B>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus } if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
} }
/// The exponent operation (^) calculates the result of raising a number to a power. /// The exponent operation (`^`) calculates the result of raising a number to a power.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn exponent<T: Pow<T>>( fn exponent<T: Pow<T>>(
_: impl Ctx, _: impl Ctx,
/// The base number that will be raised to the power. /// The base number that is raised to the power.
#[implementations(f64, f32, u32)] #[implementations(f64, f32, u32)]
base: T, base: T,
/// The power to which the base number will be raised. /// The power to which the base number is raised.
#[implementations(f64, f32, u32)] #[implementations(f64, f32, u32)]
#[default(2.)] #[default(2.)]
power: T, power: T,
@ -175,15 +177,18 @@ fn exponent<T: Pow<T>>(
base.pow(power) base.pow(power)
} }
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n. /// The `n`th root operation (`√`) calculates the inverse of exponentiation. Square root inverts squaring, cube root inverts cubing, and so on.
///
/// This is equivalent to raising the number to the power of `1/n`.
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn root<T: num_traits::float::Float>( fn root<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The number for which the nth root will be calculated. /// The number inside the radical for which the `n`th root is calculated.
#[default(2.)] #[default(2.)]
#[implementations(f64, f32)] #[implementations(f64, f32)]
radicand: T, radicand: T,
/// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on. /// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on.
/// Degrees 0 or less are invalid and will produce an output of 0.
#[default(2.)] #[default(2.)]
#[implementations(f64, f32)] #[implementations(f64, f32)]
degree: T, degree: T,
@ -192,16 +197,18 @@ fn root<T: num_traits::float::Float>(
radicand.sqrt() radicand.sqrt()
} else if degree == T::from(3.).unwrap() { } else if degree == T::from(3.).unwrap() {
radicand.cbrt() radicand.cbrt()
} else if degree <= T::from(0.).unwrap() {
T::from(0.).unwrap()
} else { } else {
radicand.powf(T::from(1.).unwrap() / degree) radicand.powf(T::from(1.).unwrap() / degree)
} }
} }
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e". /// The logarithmic function (`log`) calculates the logarithm of a number with a specified base. If the natural logarithm function (`ln`) is desired, set the base to "e".
#[node_macro::node(category("Math: Arithmetic"))] #[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<T: num_traits::float::Float>( fn logarithm<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The number for which the logarithm will be calculated. /// The number for which the logarithm is calculated.
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm). /// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
@ -220,7 +227,7 @@ fn logarithm<T: num_traits::float::Float>(
} }
} }
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length. /// The sine trigonometric function (`sin`) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn sine<T: num_traits::float::Float>( fn sine<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
@ -233,7 +240,7 @@ fn sine<T: num_traits::float::Float>(
if radians { theta.sin() } else { theta.to_radians().sin() } if radians { theta.sin() } else { theta.to_radians().sin() }
} }
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length. /// The cosine trigonometric function (`cos`) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn cosine<T: num_traits::float::Float>( fn cosine<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
@ -246,7 +253,7 @@ fn cosine<T: num_traits::float::Float>(
if radians { theta.cos() } else { theta.to_radians().cos() } if radians { theta.cos() } else { theta.to_radians().cos() }
} }
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length. /// The tangent trigonometric function (`tan`) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn tangent<T: num_traits::float::Float>( fn tangent<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
@ -259,41 +266,43 @@ fn tangent<T: num_traits::float::Float>(
if radians { theta.tan() } else { theta.to_radians().tan() } if radians { theta.tan() } else { theta.to_radians().tan() }
} }
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value. /// The inverse sine trigonometric function (`asin`) calculates the angle whose sine is the input value.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn sine_inverse<T: num_traits::float::Float>( fn sine_inverse<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN. /// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
/// Whether the resulting angle should be given in as radians instead of degrees. /// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool, radians: bool,
) -> T { ) -> T {
if radians { value.asin() } else { value.asin().to_degrees() } let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).asin();
if radians { angle } else { angle.to_degrees() }
} }
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value. /// The inverse cosine trigonometric function (`acos`) calculates the angle whose cosine is the input value.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<T: num_traits::float::Float>( fn cosine_inverse<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN. /// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
/// Whether the resulting angle should be given in as radians instead of degrees. /// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool, radians: bool,
) -> T { ) -> T {
if radians { value.acos() } else { value.acos().to_degrees() } let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).acos();
if radians { angle } else { angle.to_degrees() }
} }
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates: /// The inverse tangent trigonometric function (`atan` or `atan2`, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number. /// `atan`: the angle whose tangent is the input scalar number.
/// atan2: the angle of a ray from the origin to the specified vec2. /// `atan2`: the angle of a ray from the origin to the input vec2.
/// ///
/// The resulting angle is always in the range [-90°, 90°] or, in radians, [-π/2, π/2]. /// The resulting angle is always in the range `[-90°, 90°]` or, in radians, `[-π/2, π/2]`.
#[node_macro::node(category("Math: Trig"))] #[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<T: TangentInverse>( fn tangent_inverse<T: TangentInverse>(
_: impl Ctx, _: impl Ctx,
/// The given value for which the angle will be calculated. /// The given value for which the angle is calculated.
#[implementations(f64, f32, DVec2)] #[implementations(f64, f32, DVec2)]
value: T, value: T,
/// Whether the resulting angle should be given in as radians instead of degrees. /// Whether the resulting angle should be given in as radians instead of degrees.
@ -325,18 +334,30 @@ impl TangentInverse for DVec2 {
} }
} }
/// Linearly maps an input value from one range to another. The ranges may be reversed.
///
/// For example, 0.5 in the input range `[0, 1]` would map to 0 in the output range `[-180, 180]`.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn remap<U: num_traits::float::Float>( fn remap<U: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
#[implementations(f64, f32)] value: U, /// The value to be mapped between ranges.
#[implementations(f64, f32)] input_min: U, #[implementations(f64, f32)]
value: U,
/// The lower bound of the input range.
#[implementations(f64, f32)]
input_min: U,
/// The upper bound of the input range.
#[implementations(f64, f32)] #[implementations(f64, f32)]
#[default(1.)] #[default(1.)]
input_max: U, input_max: U,
#[implementations(f64, f32)] output_min: U, /// The lower bound of the output range.
#[implementations(f64, f32)]
output_min: U,
/// The upper bound of the output range.
#[implementations(f64, f32)] #[implementations(f64, f32)]
#[default(1.)] #[default(1.)]
output_max: U, output_max: U,
/// Whether to constrain the result within the output range instead of extrapolating beyond its bounds.
clamped: bool, clamped: bool,
) -> U { ) -> U {
let input_range = input_max - input_min; let input_range = input_max - input_min;
@ -363,17 +384,17 @@ fn remap<U: num_traits::float::Float>(
} }
} }
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed. /// The random function (`rand`) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn random( fn random(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
/// Seed to determine the unique variation of which number will be generated. /// Seed to determine the unique variation of which number is generated.
seed: u64, seed: u64,
/// The smaller end of the range within which the random number will be generated. /// The smaller end of the range within which the random number is generated.
#[default(0.)] #[default(0.)]
min: f64, min: f64,
/// The larger end of the range within which the random number will be generated. /// The larger end of the range within which the random number is generated.
#[default(1.)] #[default(1.)]
max: f64, max: f64,
) -> f64 { ) -> f64 {
@ -404,89 +425,89 @@ fn to_f64(_: impl Ctx, value: f64) -> f64 {
value value
} }
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero. /// The rounding function (`round`) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn round<T: num_traits::float::Float>( fn round<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The number which will be rounded. /// The number to be rounded to the nearest whole number.
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
) -> T { ) -> T {
value.round() value.round()
} }
/// The floor function (floor) rounds down an input value to the nearest whole number, unless the input number is already whole. /// The floor function (`floor`) rounds down an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn floor<T: num_traits::float::Float>( fn floor<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The number which will be rounded down. /// The number to be rounded down.
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
) -> T { ) -> T {
value.floor() value.floor()
} }
/// The ceiling function (ceil) rounds up an input value to the nearest whole number, unless the input number is already whole. /// The ceiling function (`ceil`) rounds up an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn ceiling<T: num_traits::float::Float>( fn ceiling<T: num_traits::float::Float>(
_: impl Ctx, _: impl Ctx,
/// The number which will be rounded up. /// The number to be rounded up.
#[implementations(f64, f32)] #[implementations(f64, f32)]
value: T, value: T,
) -> T { ) -> T {
value.ceil() value.ceil()
} }
/// The absolute value function (abs) removes the negative sign from an input value, if present. /// The absolute value function (`abs`) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn absolute_value<T: num_traits::sign::Signed>( fn absolute_value<T: num_traits::sign::Signed>(
_: impl Ctx, _: impl Ctx,
/// The number which will be made positive. /// The number to be made positive.
#[implementations(f64, f32, i32, i64)] #[implementations(f64, f32, i32, i64)]
value: T, value: T,
) -> T { ) -> T {
value.abs() value.abs()
} }
/// The minimum function (min) picks the smaller of two numbers. /// The minimum function (`min`) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>( fn min<T: std::cmp::PartialOrd>(
_: impl Ctx, _: impl Ctx,
/// One of the two numbers, of which the lesser will be returned. /// One of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
value: T, value: T,
/// The other of the two numbers, of which the lesser will be returned. /// The other of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
other_value: T, other_value: T,
) -> T { ) -> T {
if value < other_value { value } else { other_value } if value < other_value { value } else { other_value }
} }
/// The maximum function (max) picks the larger of two numbers. /// The maximum function (`max`) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn max<T: std::cmp::PartialOrd>( fn max<T: std::cmp::PartialOrd>(
_: impl Ctx, _: impl Ctx,
/// One of the two numbers, of which the greater will be returned. /// One of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
value: T, value: T,
/// The other of the two numbers, of which the greater will be returned. /// The other of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
other_value: T, other_value: T,
) -> T { ) -> T {
if value > other_value { value } else { other_value } if value > other_value { value } else { other_value }
} }
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed. /// The clamp function (`clamp`) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn clamp<T: std::cmp::PartialOrd>( fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx, _: impl Ctx,
/// The number to be clamped, which will be restricted to the range between the minimum and maximum values. /// The number to be clamped, which is restricted to the range between the minimum and maximum values.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
value: T, value: T,
/// The left (smaller) side of the range. The output will never be less than this number. /// The left (smaller) side of the range. The output is never less than this number.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
min: T, min: T,
/// The right (greater) side of the range. The output will never be greater than this number. /// The right (greater) side of the range. The output is never greater than this number.
#[implementations(f64, f32, u32, &str)] #[implementations(f64, f32, u32, &str)]
max: T, max: T,
) -> T { ) -> T {
@ -504,10 +525,10 @@ fn clamp<T: std::cmp::PartialOrd>(
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops::SubAssign>( fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops::SubAssign>(
_: impl Ctx, _: impl Ctx,
/// One of the two numbers for which the GCD will be calculated. /// One of the two numbers for which the GCD is calculated.
#[implementations(u32, u64, i32)] #[implementations(u32, u64, i32)]
value: T, value: T,
/// The other of the two numbers for which the GCD will be calculated. /// The other of the two numbers for which the GCD is calculated.
#[implementations(u32, u64, i32)] #[implementations(u32, u64, i32)]
other_value: T, other_value: T,
) -> T { ) -> T {
@ -524,10 +545,10 @@ fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32
#[node_macro::node(category("Math: Numeric"))] #[node_macro::node(category("Math: Numeric"))]
fn least_common_multiple<T: num_traits::ToPrimitive + num_traits::FromPrimitive + num_traits::identities::Zero>( fn least_common_multiple<T: num_traits::ToPrimitive + num_traits::FromPrimitive + num_traits::identities::Zero>(
_: impl Ctx, _: impl Ctx,
/// One of the two numbers for which the LCM will be calculated. /// One of the two numbers for which the LCM is calculated.
#[implementations(u32, u64, i32)] #[implementations(u32, u64, i32)]
value: T, value: T,
/// The other of the two numbers for which the LCM will be calculated. /// The other of the two numbers for which the LCM is calculated.
#[implementations(u32, u64, i32)] #[implementations(u32, u64, i32)]
other_value: T, other_value: T,
) -> T { ) -> T {
@ -574,36 +595,8 @@ fn binary_gcd<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops:
a << shift a << shift
} }
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not. /// The less-than operation (`<`) compares two values and returns true if the first value is less than the second, or false if it is not.
#[node_macro::node(category("Math: Logic"))] /// If enabled with *Or Equal*, the less-than-or-equal operation (`<=`) is used instead.
fn equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
value: T,
/// The other of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
other_value: T,
) -> bool {
other_value == value
}
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
value: T,
/// The other of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
other_value: T,
) -> bool {
other_value != value
}
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
fn less_than<T: std::cmp::PartialOrd<T>>( fn less_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx, _: impl Ctx,
@ -613,14 +606,14 @@ fn less_than<T: std::cmp::PartialOrd<T>>(
/// The number on the right-hand side of the comparison. /// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)] #[implementations(f64, f32, u32)]
other_value: T, other_value: T,
/// Uses the less-than-or-equal operation (<=) instead of the less-than operation (<). /// Uses the less-than-or-equal operation (`<=`) instead of the less-than operation (`<`).
or_equal: bool, or_equal: bool,
) -> bool { ) -> bool {
if or_equal { value <= other_value } else { value < other_value } if or_equal { value <= other_value } else { value < other_value }
} }
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not. /// The greater-than operation (`>`) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead. /// If enabled with *Or Equal*, the greater-than-or-equal operation (`>=`) is used instead.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
fn greater_than<T: std::cmp::PartialOrd<T>>( fn greater_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx, _: impl Ctx,
@ -630,13 +623,41 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
/// The number on the right-hand side of the comparison. /// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)] #[implementations(f64, f32, u32)]
other_value: T, other_value: T,
/// Uses the greater-than-or-equal operation (>=) instead of the greater-than operation (>). /// Uses the greater-than-or-equal operation (`>=`) instead of the greater-than operation (`>`).
or_equal: bool, or_equal: bool,
) -> bool { ) -> bool {
if or_equal { value >= other_value } else { value > other_value } if or_equal { value >= other_value } else { value > other_value }
} }
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false. /// The equality operation (`==`, `XNOR`) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
value: T,
/// The other of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
other_value: T,
) -> bool {
other_value == value
}
/// The inequality operation (`!=`, `XOR`) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
value: T,
/// The other of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
other_value: T,
) -> bool {
other_value != value
}
/// The logical OR operation (`||`) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
fn logical_or( fn logical_or(
_: impl Ctx, _: impl Ctx,
@ -648,7 +669,7 @@ fn logical_or(
value || other_value value || other_value
} }
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false. /// The logical AND operation (`&&`) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
fn logical_and( fn logical_and(
_: impl Ctx, _: impl Ctx,
@ -660,7 +681,7 @@ fn logical_and(
value && other_value value && other_value
} }
/// The logical not operation (!) reverses true and false value of the input. /// The logical NOT operation (`!`) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
fn logical_not( fn logical_not(
_: impl Ctx, _: impl Ctx,
@ -736,20 +757,39 @@ fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100
} }
} }
/// The dot product operation (`·`) calculates the degree of similarity of a vec2 pair based on their angles and lengths.
///
/// Calculated as `‖a‖‖b‖cos(θ)`, it represents the product of their lengths (`‖a‖‖b‖`) scaled by the alignment of their directions (`cos(θ)`).
/// The output ranges from the positive to negative product of their lengths based on when they are pointing in the same or opposite directions.
/// If any vector has zero length, the output is 0.
#[node_macro::node(category("Math: Vector"))] #[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 { fn dot_product(
vector_a.dot(vector_b) _: impl Ctx,
/// An operand of the dot product operation.
vector_a: DVec2,
/// The other operand of the dot product operation.
#[default((1., 0.))]
vector_b: DVec2,
/// Whether to normalize both input vectors so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment.
normalize: bool,
) -> f64 {
if normalize {
vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero())
} else {
vector_a.dot(vector_b)
}
} }
/// Gets the length or magnitude of a vector. // TODO: Rename to "Magnitude"
/// The magnitude operator (`‖x‖`) calculates the length of a vec2, which is the distance from the base to the tip of the arrow represented by the vector.
#[node_macro::node(category("Math: Vector"))] #[node_macro::node(category("Math: Vector"))]
fn length(_: impl Ctx, vector: DVec2) -> f64 { fn length(_: impl Ctx, vector: DVec2) -> f64 {
vector.length() vector.length()
} }
/// Scales the input vector to unit length while preserving it's direction. This is equivalent to dividing the input vector by it's own magnitude. /// Scales the input vector to unit length while preserving its direction. This is equivalent to dividing the input vector by its own magnitude.
/// ///
/// Returns zero when the input vector is zero. /// Returns 0 when the input vector has zero length.
#[node_macro::node(category("Math: Vector"))] #[node_macro::node(category("Math: Vector"))]
fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 { fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 {
vector.normalize_or_zero() vector.normalize_or_zero()
@ -765,7 +805,7 @@ mod test {
pub fn dot_product_function() { pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.); let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.); let vector_b = DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b), 11.); assert_eq!(dot_product((), vector_a, vector_b, false), 11.);
} }
#[test] #[test]

View File

@ -574,7 +574,6 @@ fn vibrance<T: Adjust<Color>>(
image image
} }
/// Color Channel
#[repr(u32)] #[repr(u32)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
@ -586,7 +585,6 @@ pub enum RedGreenBlue {
Blue, Blue,
} }
/// Color Channel
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Radio)] #[widget(Radio)]
@ -599,7 +597,7 @@ pub enum RedGreenBlueAlpha {
Alpha, Alpha,
} }
/// Style of noise pattern /// Style of noise pattern.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)] #[widget(Dropdown)]
@ -616,9 +614,9 @@ pub enum NoiseType {
WhiteNoise, WhiteNoise,
} }
/// Style of layered levels of the noise pattern.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
/// Style of layered levels of the noise pattern
pub enum FractalType { pub enum FractalType {
#[default] #[default]
None, None,
@ -632,7 +630,7 @@ pub enum FractalType {
DomainWarpIndependent, DomainWarpIndependent,
} }
/// Distance function used by the cellular noise /// Distance function used by the cellular noise.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub enum CellularDistanceFunction { pub enum CellularDistanceFunction {
@ -663,7 +661,6 @@ pub enum CellularReturnType {
Division, Division,
} }
/// Type of domain warp
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)] #[widget(Dropdown)]