mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 13:28:12 +08:00
Merge branch 'master' into fix-range
This commit is contained in:
@@ -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 |
@@ -1,3 +1,5 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
# Generated by tauri
|
||||
gen/
|
||||
|
||||
@@ -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: "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user