Merge branch 'master' into fix-range

This commit is contained in:
mTvare
2025-07-01 07:57:00 +05:30
committed by GitHub
280 changed files with 15589 additions and 28122 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<circle cx="6" cy="6" r="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 102 B

+2
View File
@@ -1,3 +1,5 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by tauri
gen/
+1 -3
View File
@@ -27,11 +27,9 @@ graphite-editor = { path = "../../editor", features = [
] }
# Workspace dependencies
serde_json = { workspace = true }
serde = { workspace = true }
axum = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
tokio = { workspace = true }
ron = { workspace = true }
log = { workspace = true }
fern = { workspace = true }
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["http:default"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","http:default","core:app:allow-app-show","core:app:allow-app-hide","shell:default","http:default"]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -566,7 +566,7 @@
background: var(--color-e-nearwhite);
color: var(--color-2-mildblack);
svg {
> .icon-label {
fill: var(--color-2-mildblack);
}
}
@@ -164,6 +164,7 @@
.text-label {
padding-left: 16px;
position: relative;
pointer-events: none;
&::before {
content: "";
+88 -8
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { getContext, onMount, tick } from "svelte";
import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Editor } from "@graphite/editor";
import { beginDraggingElement } from "@graphite/io-managers/drag";
@@ -55,6 +55,10 @@
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
let dragInPanel = false;
// Interactive clipping
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
let layerToClipAltKeyPressed = false;
// Layouts
let layersPanelControlBarLeftLayout = defaultWidgetLayout();
let layersPanelControlBarRightLayout = defaultWidgetLayout();
@@ -87,6 +91,16 @@
updateLayerInTree(targetId, targetLayer);
});
addEventListener("pointermove", clippingHover);
addEventListener("keydown", clippingKeyPress);
addEventListener("keyup", clippingKeyPress);
});
onDestroy(() => {
removeEventListener("pointermove", clippingHover);
removeEventListener("keydown", clippingKeyPress);
removeEventListener("keyup", clippingKeyPress);
});
type DocumentLayerStructure = {
@@ -208,12 +222,58 @@
// Get the state of the platform's accel key and its opposite platform's accel key
const [accel, oppositeAccel] = platformIsMac() ? [meta, ctrl] : [ctrl, meta];
// Alt-clicking to make a clipping mask
if (layerToClipAltKeyPressed && layerToClipUponClick && layerToClipUponClick.entry.clippable) clipLayer(layerToClipUponClick);
// Select the layer only if the accel and/or shift keys are pressed
if (!oppositeAccel && !alt) selectLayer(listing, accel, shift);
else if (!oppositeAccel && !alt) selectLayer(listing, accel, shift);
e.stopPropagation();
}
function clipLayer(listing: LayerListingInfo) {
editor.handle.clipLayer(listing.entry.id);
}
function clippingKeyPress(e: KeyboardEvent) {
layerToClipAltKeyPressed = e.altKey;
}
function clippingHover(e: PointerEvent) {
// Don't do anything if the user is dragging to rearrange layers
if (dragInPanel) return;
// Get the layer below the cursor
const target = (e.target instanceof HTMLElement && e.target.closest("[data-layer]")) || undefined;
if (!target) {
layerToClipUponClick = undefined;
return;
}
// Check if the cursor is near the border btween two layers
const DISTANCE = 6;
const distanceFromTop = e.clientY - target.getBoundingClientRect().top;
const distanceFromBottom = target.getBoundingClientRect().bottom - e.clientY;
const nearTop = distanceFromTop < DISTANCE;
const nearBottom = distanceFromBottom < DISTANCE;
// If we are not near the border, we don't want to clip
if (!nearTop && !nearBottom) {
layerToClipUponClick = undefined;
return;
}
// If we are near the border, we want to clip the layer above the border
const indexAttribute = target?.getAttribute("data-index") ?? undefined;
const index = indexAttribute ? Number(indexAttribute) : undefined;
const layer = index !== undefined && layers[nearTop ? index - 1 : index];
if (!layer) return;
// Update the state used to show the clipping action
layerToClipUponClick = layer;
layerToClipAltKeyPressed = e.altKey;
}
function selectLayer(listing: LayerListingInfo, accel: boolean, shift: boolean) {
// Don't select while we are entering text to rename the layer
if (listing.editingName) return;
@@ -433,7 +493,16 @@
<WidgetLayout layout={layersPanelControlBarRightLayout} />
</LayoutRow>
<LayoutRow class="list-area" scrollableY={true}>
<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={updateInsertLine} on:dragend={drop} on:drop={drop}>
<LayoutCol
class="list"
styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }}
data-layer-panel
bind:this={list}
on:click={() => deselectAllLayers()}
on:dragover={updateInsertLine}
on:dragend={drop}
on:drop={drop}
>
{#each layers as listing, index}
{@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected}
<LayoutRow
@@ -464,6 +533,11 @@
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
></button>
{:else}
<div class="expand-arrow-none"></div>
{/if}
{#if listing.entry.clipped}
<IconLabel icon="Clipped" class="clipped-arrow" tooltip={"Clipping mask is active (Alt-click border to release)"} />
{/if}
<div class="thumbnail">
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
@@ -589,6 +663,7 @@
.expand-arrow {
padding: 0;
margin: 0;
margin-right: 4px;
width: 16px;
height: 100%;
border: none;
@@ -625,10 +700,19 @@
}
}
.expand-arrow-none {
flex: 0 0 16px;
margin-right: 4px;
}
.clipped-arrow {
margin-left: 2px;
margin-right: 2px;
}
.thumbnail {
width: 36px;
height: 24px;
margin-left: 4px;
border-radius: 2px;
flex: 0 0 auto;
background-image: var(--color-transparent-checkered-background);
@@ -636,10 +720,6 @@
background-position: var(--color-transparent-checkered-background-position-mini);
background-repeat: var(--color-transparent-checkered-background-repeat);
&:first-child {
margin-left: 20px;
}
svg {
width: calc(100% - 4px);
height: calc(100% - 4px);
+6 -3
View File
@@ -137,7 +137,7 @@
await refreshWires();
}
function resolveWire(wire: FrontendNodeWire): { nodeOutput: SVGSVGElement | undefined; nodeInput: SVGSVGElement | undefined } {
function resolveWire(wire: FrontendNodeWire): { nodeOutput: SVGSVGElement; nodeInput: SVGSVGElement } | undefined {
// TODO: Avoid the linear search
const wireStartNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireStart as Node).nodeId);
let nodeOutputConnectors = outputs[wireStartNodeIdIndex + 1];
@@ -146,6 +146,7 @@
}
const indexOutput = Number(wire.wireStart.index);
const nodeOutput = nodeOutputConnectors?.[indexOutput] as SVGSVGElement | undefined;
if (nodeOutput === undefined) return undefined;
// TODO: Avoid the linear search
const wireEndNodeIdIndex = Array.from($nodeGraph.nodes.keys()).findIndex((nodeId) => nodeId === (wire.wireEnd as Node).nodeId);
@@ -155,6 +156,7 @@
}
const indexInput = Number(wire.wireEnd.index);
const nodeInput = nodeInputConnectors?.[indexInput] as SVGSVGElement | undefined;
if (nodeInput === undefined) return undefined;
return { nodeOutput, nodeInput };
}
@@ -177,8 +179,9 @@
nodeWirePaths = $nodeGraph.wires.flatMap((wire) => {
// TODO: This call contains linear searches, which combined with the loop we're in, causes O(n^2) complexity as the graph grows
const { nodeOutput, nodeInput } = resolveWire(wire);
if (!nodeOutput || !nodeInput) return [];
const resolvedWires = resolveWire(wire);
if (!resolvedWires) return [];
const { nodeOutput, nodeInput } = resolvedWires;
const wireStartNode = wire.wireStart.nodeId !== undefined ? $nodeGraph.nodes.get(wire.wireStart.nodeId) : undefined;
const wireStart = wireStartNode?.isLayer || false;
@@ -217,5 +217,4 @@
}
}
}
// paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding
</style>
@@ -30,6 +30,7 @@
let open = false;
$: watchSelectedIndex(selectedIndex);
$: watchEntries(entries);
$: watchActiveEntry(activeEntry);
$: watchOpen(open);
@@ -38,7 +39,13 @@
}
// Called only when `selectedIndex` is changed from outside this component
function watchSelectedIndex(_?: number) {
function watchSelectedIndex(_?: typeof selectedIndex) {
activeEntrySkipWatcher = true;
activeEntry = makeActiveEntry();
}
// Called only when `entries` is changed from outside this component
function watchEntries(_?: typeof entries) {
activeEntrySkipWatcher = true;
activeEntry = makeActiveEntry();
}
-12
View File
@@ -47,9 +47,6 @@ export function createEditor(): Editor {
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle = handle;
// Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
@@ -76,12 +73,3 @@ export function createEditor(): Editor {
return { raw, handle, subscriptions };
}
// TODO: Find a better way to do this, since no other code takes this approach.
// TODO: Then, delete the `(window as any).editorHandle = handle;` line above.
// This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system.
// Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code.
// export function injectImaginatePollServerStatus() {
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// (window as any).editorHandle?.injectImaginatePollServerStatus();
// }
+57 -29
View File
@@ -22,7 +22,7 @@ export const PRESS_REPEAT_DELAY_MS = 400;
export const PRESS_REPEAT_INTERVAL_MS = 72;
export const PRESS_REPEAT_INTERVAL_RAPID_MS = 10;
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield" | "pointerlockchange" | "pointerlockerror";
type EventListenerTarget = {
addEventListener: typeof window.addEventListener;
removeEventListener: typeof window.removeEventListener;
@@ -35,6 +35,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
let viewportPointerInteractionOngoing = false;
let textToolInteractiveInputElement = undefined as undefined | HTMLDivElement;
let canvasFocused = true;
let inPointerLock = false;
// Event listeners
@@ -55,6 +56,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
{ target: window.document, eventName: "contextmenu", action: (e: MouseEvent) => onContextMenu(e) },
{ target: window.document, eventName: "fullscreenchange", action: () => fullscreen.fullscreenModeChanged() },
{ target: window.document.body, eventName: "paste", action: (e: ClipboardEvent) => onPaste(e) },
{ target: window.document, eventName: "pointerlockchange", action: onPointerLockChange },
{ target: window.document, eventName: "pointerlockerror", action: onPointerLockChange },
];
// Event bindings
@@ -101,6 +104,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (["KeyC", "KeyI", "KeyJ"].includes(key) && accelKey && e.shiftKey) return false;
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
potentiallyRestoreCanvasFocus(e);
if (!canvasFocused && !targetIsTextField(e.target || undefined) && ["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key)) return false;
// Don't redirect if a MenuList is open
@@ -142,6 +146,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events and these are handled in the backend
function onPointerMove(e: PointerEvent) {
potentiallyRestoreCanvasFocus(e);
if (!e.buttons) viewportPointerInteractionOngoing = false;
// Don't redirect pointer movement to the backend if there's no ongoing interaction and it's over a floating menu, or the graph overlay, on top of the canvas
@@ -152,25 +158,15 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
const inGraphOverlay = get(document).graphViewOverlayOpen;
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
const { target } = e;
const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined);
if (newInCanvasArea && !canvasFocused) {
canvasFocused = true;
app?.focus();
}
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
}
function onMouseDown(e: MouseEvent) {
// Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it)
if (e.button === BUTTON_MIDDLE) e.preventDefault();
}
function onPointerDown(e: PointerEvent) {
potentiallyRestoreCanvasFocus(e);
const { target } = e;
const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
const inContextMenu = target instanceof Element && target.closest("[data-context-menu]");
const inTextInput = target === textToolInteractiveInputElement;
@@ -182,18 +178,23 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}
if (!inTextInput && !inContextMenu) {
const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT;
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
if (textToolInteractiveInputElement) {
const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT;
editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
} else {
viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
}
}
if (viewportPointerInteractionOngoing) {
if (viewportPointerInteractionOngoing && isTargetingCanvas instanceof Element) {
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
}
}
function onPointerUp(e: PointerEvent) {
potentiallyRestoreCanvasFocus(e);
// Don't let the browser navigate back or forward when using the buttons on some mice
// TODO: This works in Chrome but not in Firefox
// TODO: Possible workaround: use the browser's history API to block navigation:
@@ -208,10 +209,17 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
editor.handle.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
}
function onPotentialDoubleClick(e: MouseEvent) {
if (textToolInteractiveInputElement) return;
// Mouse events
// Allow only double-clicks
function onPotentialDoubleClick(e: MouseEvent) {
if (textToolInteractiveInputElement || inPointerLock) return;
// Allow only events within the viewport or node graph boundaries
const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
if (!(isTargetingCanvas instanceof Element)) return;
// Allow only repeated increments of double-clicks (not 1, 3, 5, etc.)
if (e.detail % 2 == 1) return;
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
@@ -226,11 +234,26 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
}
// Mouse events
function onMouseDown(e: MouseEvent) {
// Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it)
if (e.button === BUTTON_MIDDLE) e.preventDefault();
}
function onContextMenu(e: MouseEvent) {
if (!targetIsTextField(e.target || undefined) && e.target !== textToolInteractiveInputElement) {
e.preventDefault();
}
}
function onPointerLockChange() {
inPointerLock = Boolean(window.document.pointerLockElement);
}
// Wheel events
function onWheelScroll(e: WheelEvent) {
const { target } = e;
const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation
@@ -247,12 +270,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}
}
function onContextMenu(e: MouseEvent) {
if (!targetIsTextField(e.target || undefined) && e.target !== textToolInteractiveInputElement) {
e.preventDefault();
}
}
// Receives a custom event dispatched when the user begins interactively editing with the text tool.
// We keep a copy of the text input element to check against when it's active for text entry.
function onModifyInputField(e: CustomEvent) {
@@ -413,6 +430,17 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}
});
// Helper functions
function potentiallyRestoreCanvasFocus(e: Event) {
const { target } = e;
const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined);
if (!canvasFocused && newInCanvasArea) {
canvasFocused = true;
app?.focus();
}
}
// Initialization
// Bind the event listeners
+4 -10
View File
@@ -907,6 +907,10 @@ export class LayerPanelEntry {
ancestorOfSelected!: boolean;
descendantOfSelected!: boolean;
clipped!: boolean;
clippable!: boolean;
}
export class DisplayDialogDismiss extends JsMessage {}
@@ -936,15 +940,6 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
readonly commitDate!: string;
}
// TODO: Eventually remove this document upgrade code
export class TriggerUpgradeDocumentToVectorManipulationFormat extends JsMessage {
readonly documentId!: bigint;
readonly documentName!: string;
readonly documentIsAutoSaved!: boolean;
readonly documentIsSaved!: boolean;
readonly documentSerializedContent!: string;
}
// WIDGET PROPS
export abstract class WidgetProps {
@@ -1675,7 +1670,6 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerSavePreferences,
TriggerTextCommit,
TriggerTextCopy,
TriggerUpgradeDocumentToVectorManipulationFormat,
TriggerVisitLink,
UpdateActiveDocument,
UpdateBox,
@@ -10,7 +10,6 @@ import {
TriggerDownloadTextFile,
TriggerImport,
TriggerOpenDocument,
TriggerUpgradeDocumentToVectorManipulationFormat,
UpdateActiveDocument,
UpdateOpenDocumentsList,
UpdateSpreadsheetState,
@@ -104,11 +103,6 @@ export function createPortfolioState(editor: Editor) {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
}
});
editor.subscriptions.subscribeJsMessage(TriggerUpgradeDocumentToVectorManipulationFormat, async (triggerUpgradeDocumentToVectorManipulationFormat) => {
// TODO: Eventually remove this document upgrade code
const { documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent } = triggerUpgradeDocumentToVectorManipulationFormat;
editor.handle.triggerUpgradeDocumentToVectorManipulationFormat(documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent);
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
update((state) => {
+2
View File
@@ -13,6 +13,7 @@ import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg";
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
import Dot from "@graphite-frontend/assets/icon-12px-solid/dot.svg";
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
@@ -55,6 +56,7 @@ const SOLID_12PX = {
Clipped: { svg: Clipped, size: 12 },
CloseX: { svg: CloseX, size: 12 },
Delay: { svg: Delay, size: 12 },
Dot: { svg: Dot, size: 12 },
DropdownArrow: { svg: DropdownArrow, size: 12 },
Edit12px: { svg: Edit12px, size: 12 },
Empty12px: { svg: Empty12px, size: 12 },
-12
View File
@@ -1,12 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = [
# Currently disabled because of https://github.com/GraphiteEditor/Graphite/issues/1262
# The current simd implementation leads to undefined behavior
#"-C",
#"target-feature=+simd128",
"-C",
"target-feature=+bulk-memory",
"-C",
"link-arg=--max-memory=4294967296",
"--cfg=web_sys_unstable_apis",
]
+9 -20
View File
@@ -13,7 +13,7 @@ license = "Apache-2.0"
[features]
default = ["gpu"]
gpu = ["editor/gpu"]
tauri = ["ron", "editor/tauri"]
tauri = [ "editor/tauri"]
[lib]
crate-type = ["cdylib", "rlib"]
@@ -25,33 +25,19 @@ editor = { path = "../../editor", package = "graphite-editor", features = [
"resvg",
"vello",
] }
graphene-std = { workspace = true }
# Workspace dependencies
graph-craft = { workspace = true }
log = { workspace = true }
graphene-core = { workspace = true, features = ["std", "alloc"] }
serde = { workspace = true, features = ["derive"] }
serde = { workspace = true }
wasm-bindgen = { workspace = true }
serde-wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true }
bezier-rs = { workspace = true }
glam = { workspace = true }
futures = { workspace = true }
math-parser = { workspace = true }
wgpu = { workspace = true, features = [
"fragile-send-sync-non-atomic-wasm",
] } # We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
web-sys = { workspace = true, features = [
"Window",
"CanvasRenderingContext2d",
"Document",
"HtmlCanvasElement",
"IdleRequestOptions",
] }
# Optional workspace dependencies
ron = { workspace = true, optional = true }
wgpu = { workspace = true }
web-sys = { workspace = true }
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
@@ -59,7 +45,7 @@ wasm-opt = false
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
debug-js-glue = true
demangle-name-section = true
dwarf-debug-info = true
dwarf-debug-info = false
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Os", "-g"]
@@ -81,3 +67,6 @@ dwarf-debug-info = true
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(wasm_bindgen_unstable_test_coverage)',
] }
[package.metadata.cargo-shear]
ignored = ["wgpu"]
+10 -224
View File
@@ -11,12 +11,12 @@ use editor::consts::FILE_SAVE_SUFFIX;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, NodeTemplate};
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::utility_types::Platform;
use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
use graph_craft::document::NodeId;
use graphene_core::raster::color::Color;
use graphene_std::raster::color::Color;
use serde::Serialize;
use serde_wasm_bindgen::{self, from_value};
use std::cell::RefCell;
@@ -504,6 +504,13 @@ impl EditorHandle {
self.dispatch(message);
}
#[wasm_bindgen(js_name = clipLayer)]
pub fn clip_layer(&self, id: u64) {
let id = NodeId(id);
let message = DocumentMessage::ClipLayer { id };
self.dispatch(message);
}
/// Modify the layer selection based on the layer which is clicked while holding down the <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> modifier keys used for range selection behavior
#[wasm_bindgen(js_name = selectLayer)]
pub fn select_layer(&self, id: u64, ctrl: bool, shift: bool) {
@@ -620,7 +627,7 @@ impl EditorHandle {
insert_index: Option<usize>,
) {
let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y)));
let image = graphene_core::raster::Image::from_image_data(&image_data, width, height);
let image = graphene_std::raster::Image::from_image_data(&image_data, width, height);
let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) {
let insert_parent_id = NodeId(insert_parent_id);
@@ -727,227 +734,6 @@ impl EditorHandle {
};
self.dispatch(message);
}
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
// pub fn inject_imaginate_poll_server_status(&self) {
// self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
// }
// TODO: Eventually remove this document upgrade code
#[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]
pub async fn upgrade_document_to_vector_manipulation_format(
&self,
document_id: u64,
document_name: String,
document_is_auto_saved: bool,
document_is_saved: bool,
document_serialized_content: String,
) {
use editor::messages::portfolio::document::graph_operation::transform_utils::*;
use editor::messages::portfolio::document::graph_operation::utility_types::*;
use editor::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use editor::node_graph_executor::NodeRuntime;
use editor::node_graph_executor::replace_node_runtime;
use graph_craft::document::DocumentNodeImplementation;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_core::vector::*;
let (_, request_receiver) = std::sync::mpsc::channel();
let (response_sender, _) = std::sync::mpsc::channel();
let old_runtime = replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)).await;
let mut editor = Editor::new();
let document_id = DocumentId(document_id);
editor.handle_message(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name: document_name.clone(),
document_is_auto_saved,
document_is_saved,
document_serialized_content: document_serialized_content.clone(),
to_front: false,
});
let Some(document) = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut() else {
warn!("Document wasn't loaded");
return;
};
for node in document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Artboard"))
.map(|(id, _)| *id)
.collect::<Vec<_>>()
{
let Some(document_node) = document.network_interface.document_network().nodes.get(&node) else {
log::error!("Could not get document node in document network");
return;
};
if let Some(network) = document_node.implementation.get_network() {
let mut nodes_to_upgrade = Vec::new();
for (node_id, _) in network.nodes.iter().collect::<Vec<_>>() {
if document
.network_interface
.reference(node_id, &[])
.is_some_and(|reference| *reference == Some("To Artboard".to_string()))
&& document
.network_interface
.document_network()
.nodes
.get(node_id)
.is_some_and(|document_node| document_node.inputs.len() != 6)
{
nodes_to_upgrade.push(*node_id);
}
}
for node_id in nodes_to_upgrade {
document
.network_interface
.replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ToArtboardNode"));
document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "", "", &[node_id]);
}
}
}
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
portfolio
.executor
.submit_node_graph_evaluation(
portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(),
glam::UVec2::ONE,
Default::default(),
None,
true,
)
.unwrap();
editor::node_graph_executor::run_node_graph().await;
let mut messages = VecDeque::new();
if let Err(err) = editor.poll_node_graph_evaluation(&mut messages) {
log::warn!(
"While attempting to upgrade the old document format, the graph evaluation failed which is necessary for the upgrade process:\n{:#?}",
err
);
replace_node_runtime(old_runtime.unwrap()).await;
let document_name = document_name.clone() + "__DO_NOT_UPGRADE__";
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
to_front: false,
});
return;
}
let mut updated_nodes = HashSet::new();
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
document.network_interface.load_structure();
for node in document
.network_interface
.document_network_metadata()
.persistent_metadata
.node_metadata
.iter()
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Merge"))
.map(|(id, _)| *id)
.collect::<Vec<_>>()
{
let layer = LayerNodeIdentifier::new(node, &document.network_interface, &[]);
if layer.has_children(document.metadata()) {
continue;
}
let bounds = LayerBounds::new(document.metadata(), layer);
let mut responses = VecDeque::new();
let mut shape = None;
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, &mut document.network_interface, &mut responses) {
let Some(transform_node_id) = modify_inputs.existing_node_id("Transform", true) else {
return;
};
if !updated_nodes.insert(transform_node_id) {
return;
}
let Some(inputs) = modify_inputs.network_interface.document_network().nodes.get(&transform_node_id).map(|node| &node.inputs) else {
log::error!("Could not get transform node in document network");
return;
};
let transform = get_current_transform(inputs);
let upstream_transform = modify_inputs.network_interface.document_metadata().upstream_transform(transform_node_id);
let pivot_transform = glam::DAffine2::from_translation(upstream_transform.transform_point2(bounds.local_pivot(get_current_normalized_pivot(inputs))));
update_transform(&mut document.network_interface, &transform_node_id, pivot_transform * transform * pivot_transform.inverse());
}
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, &mut document.network_interface, &mut responses) {
let Some(shape_node_id) = modify_inputs.existing_node_id("Shape", true) else {
return;
};
if !updated_nodes.insert(shape_node_id) {
return;
}
let Some(shape_node) = modify_inputs.network_interface.document_network().nodes.get(&shape_node_id) else {
log::error!("Could not get shape node in document network");
return;
};
let path_data = match &shape_node.inputs[0].as_value() {
Some(TaggedValue::Subpaths(translation)) => translation,
_ => &Vec::new(),
};
let colinear_manipulators = match &shape_node.inputs[1].as_value() {
Some(TaggedValue::PointIds(translation)) => translation,
_ => &Vec::new(),
};
let mut vector_data = VectorData::from_subpaths(path_data, false);
vector_data.colinear_manipulators = colinear_manipulators
.iter()
.filter_map(|&point| ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
.collect();
shape = Some((shape_node_id, VectorModification::create_from_vector(&vector_data)));
}
if let Some((node_id, modification)) = shape {
let node_type = resolve_document_node_type("Path").unwrap();
let document_node = node_type
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VectorModification(Box::new(modification)), false))])
.document_node;
let node_metadata = document.network_interface.node_metadata(&node_id, &[]).cloned().unwrap_or_default();
document.network_interface.insert_node(
node_id,
NodeTemplate {
document_node,
persistent_node_metadata: node_metadata.persistent_metadata,
},
&[],
);
}
}
let document_serialized_content = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap().serialize_document();
replace_node_runtime(old_runtime.unwrap()).await;
self.dispatch(PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_is_auto_saved,
document_is_saved,
document_serialized_content,
to_front: false,
});
}
}
// ============================================================================