Build the node graph frontend with placeholder graph info (#581)

* Build the node graph frontend

* Graph pan and zoom

* Graph's dot grid now pans/zooms also

* Interactive horisontal to vertical curves

* Data types and zooming on wires

* Icon definitions code beautification

* Add a visibility toggle

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers 2022-05-10 22:34:25 -07:00
parent 5571f39b41
commit 19b2d3f859
32 changed files with 826 additions and 145 deletions

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M11.61,6.6C10.05,4.38,8.82,2.36,8,1C7.18,2.36,5.95,4.38,4.39,6.6s-1.81,4.74-0.74,6.39C4.67,14.34,6.31,15.1,8,15c1.69,0.1,3.33-0.66,4.35-2.01C13.42,11.34,13.17,8.9,11.61,6.6 M5.87,7.75c0.49,4.67,2.95,5.74,2.95,5.74C2.26,13.85,5.87,7.75,5.87,7.75" />
</svg>

After

Width:  |  Height:  |  Size: 327 B

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M9.49,11.76c-0.33,0.12-0.67,0.22-1,0.32c-0.42,1.03-1.26,1.87-2.35,2.24c-0.6,0.16-1.2,0.24-1.8,0.26c0.53,0.13,1.12,0.24,1.82,0.33c4.34,0.53,5.22-1.12,8.78-1.49C14.94,13.42,13.88,10.11,9.49,11.76z" />
<path d="M7.79,10.64C7.72,9.55,6.82,8.69,5.73,8.68C4.77,8.64,3.9,9.34,3.25,10.8C2.77,11.73,1.97,12.45,1,12.83c1.47,0.79,3.2,0.99,4.81,0.55C6.98,12.98,7.77,11.88,7.79,10.64" />
<path d="M14.03,2.23c-0.46-0.5-2.68,1.25-5.11,3.49C8.09,6.45,7.33,7.24,6.64,8.09c0.38,0.09,0.73,0.28,1.02,0.56c0.34,0.3,0.57,0.7,0.67,1.14c0.85-0.69,1.63-1.46,2.33-2.31C12.85,4.88,14.54,2.73,14.03,2.23" />
</svg>

After

Width:  |  Height:  |  Size: 661 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M10.43,9.65c7.82-3.53,2.77-8.99-1.77-8.64S0.83,4.86,1.62,9.34c0.76,4.33,6.79,6.72,8.76,5.2C12.4,12.98,6.85,11.27,10.43,9.65 M11.55,3.17c0.59,0,1.07,0.47,1.08,1.06S12.16,5.3,11.57,5.3c-0.59,0-1.07-0.47-1.08-1.06c0,0,0,0,0,0C10.49,3.65,10.96,3.17,11.55,3.17 M4.06,9.91c-0.59,0-1.07-0.47-1.07-1.06c0-0.59,0.47-1.07,1.06-1.07c0.59,0,1.07,0.47,1.07,1.06C5.13,9.42,4.65,9.91,4.06,9.91C4.06,9.91,4.06,9.91,4.06,9.91 M4.82,6.51c-0.59,0-1.07-0.47-1.07-1.06s0.47-1.07,1.06-1.07c0.59,0,1.07,0.47,1.07,1.06c0,0,0,0,0,0C5.89,6.03,5.41,6.51,4.82,6.51 M7.87,4.47C7.28,4.48,6.8,4,6.79,3.41s0.47-1.07,1.06-1.08c0.59,0,1.07,0.47,1.08,1.06c0,0,0,0,0,0C8.93,3.99,8.46,4.47,7.87,4.47" />
</svg>

After

Width:  |  Height:  |  Size: 745 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M14,1h-1c0.23,0.28,0.37,0.63,0.4,1l0,0c0,0.77-0.63,1.4-1.4,1.4c-0.77,0-1.4-0.63-1.4-1.4c0.03-0.37,0.17-0.72,0.4-1H9.5c0.2,0.29,0.31,0.64,0.3,1l0,0c0,0.99-0.81,1.8-1.8,1.8S6.2,2.99,6.2,2c0.04-0.35,0.14-0.69,0.3-1H5.7C5.9,1.22,6.01,1.5,6,1.8C6.11,2.9,5.3,3.89,4.2,4C5.3,4.11,6.11,5.1,6,6.2C5.9,7.15,5.15,7.9,4.2,8C5.3,8.11,6.11,9.1,6,10.2c-0.1,0.95-0.85,1.7-1.8,1.8c0.97,0.07,1.73,0.83,1.8,1.8c0,0.42-0.11,0.83-0.3,1.2h0.8c-0.2-0.29-0.31-0.64-0.3-1c0-0.03,0-0.06,0-0.08c0-0.94,0.76-1.71,1.7-1.72c0,0,0,0,0,0h0.01c0.99,0,1.79,0.81,1.79,1.8c0,0,0,0,0,0c0.01,0.36-0.1,0.71-0.3,1H11c-0.23-0.28-0.37-0.63-0.4-1c0-0.77,0.63-1.4,1.4-1.4c0.77,0,1.4,0.63,1.4,1.4l0,0c-0.03,0.37-0.17,0.72-0.4,1h1c0.55,0,1-0.45,1-1V2C15,1.45,14.55,1,14,1 M9.8,10c0,0.99-0.81,1.79-1.8,1.79c-0.99,0-1.79-0.81-1.79-1.8C6.22,9,7.02,8.2,8.01,8.2C9,8.2,9.8,9,9.8,9.99V10 M9.8,6c0,0.99-0.81,1.79-1.8,1.79c-0.99,0-1.79-0.81-1.79-1.8C6.22,5,7.02,4.2,8.01,4.2C9,4.2,9.8,5,9.8,5.99V6 M13.4,10c0,0.77-0.63,1.4-1.4,1.4s-1.4-0.63-1.4-1.4c0-0.77,0.63-1.4,1.4-1.4c0,0,0,0,0,0c0.75-0.02,1.38,0.58,1.4,1.33C13.4,9.96,13.4,9.98,13.4,10 M13.4,6c0,0.77-0.63,1.4-1.4,1.4c-0.77,0-1.4-0.63-1.4-1.4c0-0.77,0.63-1.4,1.4-1.4c0.75-0.02,1.38,0.58,1.4,1.33C13.4,5.95,13.4,5.98,13.4,6 M3.8,8C3.04,8.09,2.39,8.59,2.1,9.3C1.91,8.75,1.52,8.28,1,8c0.52-0.28,0.91-0.75,1.1-1.3C2.39,7.41,3.04,7.91,3.8,8 M3.8,4C3.05,4.12,2.41,4.61,2.1,5.3C1.91,4.75,1.52,4.28,1,4c0.52-0.28,0.91-0.75,1.1-1.3C2.41,3.39,3.05,3.88,3.8,4 M3.8,12c-0.76,0.09-1.41,0.59-1.7,1.3C1.88,12.76,1.49,12.31,1,12c0.52-0.28,0.91-0.75,1.1-1.3C2.39,11.41,3.04,11.91,3.8,12" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<polygon points="6.08,4.08 8,3.5 6.08,2.92 5.5,1 4.92,2.92 3,3.5 4.92,4.08 5.5,6" />
<polygon points="9.9,12.9 11.5,12.5 9.9,12.1 9.5,10.5 9.1,12.1 7.5,12.5 9.1,12.9 9.5,14.5" />
<polygon points="12.98,9.98 15,9.5 12.98,9.02 12.5,7 12.02,9.02 10,9.5 12.02,9.98 12.5,12" />
<path d="M12.65,4.65l-1.29-1.29c-0.2-0.2-0.51-0.2-0.71,0l-9.29,9.29c-0.2,0.2-0.2,0.51,0,0.71l1.29,1.29c0.2,0.2,0.51,0.2,0.71,0l9.29-9.29C12.84,5.16,12.84,4.84,12.65,4.65z M11.82,5.18L9.18,7.82c-0.1,0.1-0.26,0.1-0.35,0L8.18,7.18c-0.1-0.1-0.1-0.26,0-0.35l2.65-2.65c0.1-0.1,0.26-0.1,0.35,0l0.65,0.65C11.92,4.92,11.92,5.08,11.82,5.18z" />
</svg>

After

Width:  |  Height:  |  Size: 680 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M13.391,5.529c-.414-.037-1.91-1.088-2.941-1.027S8.663,5.1,8,5.1s-1.418-.54-2.449-.6-2.527.99-2.941,1.027c-.721.065-.952-.246-1.61-.736a13.341,13.341,0,0,0,.459,3.454A4.249,4.249,0,0,0,4.5,11.389a3.063,3.063,0,0,0,2.2-.2A3.129,3.129,0,0,1,8,10.7a3.133,3.133,0,0,1,1.3.49,3.061,3.061,0,0,0,2.2.2,4.249,4.249,0,0,0,3.042-3.142A13.338,13.338,0,0,0,15,4.793c-.657.49-.888.8-1.609.736M6.94,9.232c-.486.438-2.157.257-2.7-.038-.652-.353-1.465-1.218-.881-1.7a2.646,2.646,0,0,1,2.876-.113c.869.719.928,1.651.707,1.85m4.823-.038c-.545.3-2.216.476-2.7.038-.221-.2-.162-1.131.707-1.85a2.645,2.645,0,0,1,2.875.113c.585.481-.229,1.346-.88,1.7" />
</svg>

After

Width:  |  Height:  |  Size: 710 B

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.06,4.18l0.87-2.19C4.14,1.43,0.05,6.67,2.9,11.07C1.32,7.09,4.37,2.89,8.06,4.18z" />
<path d="M7.94,11.82l-0.87,2.19c4.79,0.56,8.88-4.68,6.03-9.08C14.68,8.91,11.63,13.11,7.94,11.82z" />
<path d="M10.04,2.72c-0.49,0.75-1.37,2.46-1.8,3.3C8.16,6.01,8.08,6,8,6C6.9,6,6,6.9,6,8c0,0.15,0.02,0.3,0.05,0.44c-1.28,1.16-4.62,4.26-5.02,5.51c-0.42,1.32,3.38,1.68,4.93-0.68c0.49-0.75,1.37-2.46,1.8-3.3C7.84,9.99,7.92,10,8,10c1.1,0,2-0.9,2-2c0-0.15-0.02-0.3-0.05-0.44c1.28-1.16,4.62-4.26,5.02-5.51C15.39,0.72,11.59,0.36,10.04,2.72z M8,9C7.45,9,7,8.55,7,8c0-0.55,0.45-1,1-1s1,0.45,1,1C9,8.55,8.55,9,8,9z" />
</svg>

After

Width:  |  Height:  |  Size: 674 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M7,2L3,6v8h10V2H7z M12,13H4V7h4V3h4V13z" />
</svg>

After

Width:  |  Height:  |  Size: 122 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M15,12l-5-3v2H6.95C6.75,10.02,5.98,9.25,5,9.05V6h2L4,1L1,6h2v3.51c-0.6,0.46-1,1.17-1,1.99C2,12.88,3.12,14,4.5,14c0.82,0,1.53-0.4,1.99-1H10v2L15,12z M4.5,12.5c-0.55,0-1-0.45-1-1c0-0.55,0.45-1,1-1s1,0.45,1,1C5.5,12.05,5.05,12.5,4.5,12.5z" />
</svg>

After

Width:  |  Height:  |  Size: 318 B

View file

@ -70,8 +70,12 @@
--color-data-general-rgb: 197, 197, 197;
--color-data-vector: #65bbe5;
--color-data-vector-rgb: 101, 187, 229;
--color-data-vector-dim: #4b778c;
--color-data-vector-dim-rgb: 75, 119, 140;
--color-data-raster: #e4bb72;
--color-data-raster-rgb: 228, 187, 114;
--color-data-raster-dim: #8b7752;
--color-data-raster-dim-rgb: 139, 119, 82;
--color-data-mask: #8d85c7;
--color-data-mask-rgb: 141, 133, 199;
--color-data-unused1: #d6536e;
@ -258,9 +262,10 @@ import { createAutoSaveManager } from "@/lifetime/auto-save";
import { initErrorHandling } from "@/lifetime/errors";
import { createInputManager, InputManager } from "@/lifetime/input";
import { createDialogState, DialogState } from "@/state/dialog";
import { createDocumentsState, DocumentsState } from "@/state/documents";
import { createFullscreenState, FullscreenState } from "@/state/fullscreen";
import { createPortfolioState, PortfolioState } from "@/state/portfolio";
import { createEditorState, EditorState } from "@/state/wasm-loader";
import { createWorkspaceState, WorkspaceState } from "@/state/workspace";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -270,7 +275,8 @@ import MainWindow from "@/components/window/MainWindow.vue";
declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
dialog: DialogState;
documents: DocumentsState;
portfolio: PortfolioState;
workspace: WorkspaceState;
fullscreen: FullscreenState;
editor: EditorState;
// This must be set to optional because there is a time in the lifecycle of the component where inputManager is undefined.
@ -284,7 +290,8 @@ export default defineComponent({
return {
editor: this.editor,
dialog: this.dialog,
documents: this.documents,
portfolio: this.portfolio,
workspace: this.workspace,
fullscreen: this.fullscreen,
inputManager: this.inputManager,
};
@ -295,15 +302,17 @@ export default defineComponent({
// Initialize other stateful Vue systems
const dialog = createDialogState(editor);
const documents = createDocumentsState(editor);
const portfolio = createPortfolioState(editor);
const workspace = createWorkspaceState(editor);
const fullscreen = createFullscreenState();
initErrorHandling(editor, dialog);
createAutoSaveManager(editor, documents);
createAutoSaveManager(editor, portfolio);
return {
editor,
dialog,
documents,
portfolio,
workspace,
fullscreen,
showUnsupportedModal: !("BigInt64Array" in window),
inputManager: undefined as undefined | InputManager,
@ -315,7 +324,7 @@ export default defineComponent({
},
},
mounted() {
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen);
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen);
},
beforeUnmount() {
this.inputManager?.removeListeners();

View file

@ -1,5 +1,5 @@
<template>
<LayoutCol class="layer-tree-panel">
<LayoutCol class="layer-tree">
<LayoutRow class="options-bar">
<DropdownInput
v-model:selectedIndex="blendModeSelectedIndex"
@ -32,7 +32,7 @@
<IconButton :action="createEmptyFolder" :icon="'NodeFolder'" title="New Folder (Ctrl+Shift+N)" :size="24" />
<IconButton :action="deleteSelectedLayers" :icon="'Trash'" title="Delete Selected (Del)" :size="24" />
</LayoutRow>
<LayoutRow class="layer-tree" :scrollableY="true">
<LayoutRow class="layer-tree-rows" :scrollableY="true">
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
<LayoutRow
class="layer-row"
@ -70,10 +70,10 @@
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null"
>
<LayoutRow class="layer-type-icon">
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" title="Image" />
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" title="Shape" />
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" title="Path" />
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :style="'node'" title="Folder" />
<IconLabel v-else-if="listing.entry.layer_type === 'Image'" :icon="'NodeImage'" :style="'node'" title="Image" />
<IconLabel v-else-if="listing.entry.layer_type === 'Shape'" :icon="'NodeShape'" :style="'node'" title="Shape" />
<IconLabel v-else-if="listing.entry.layer_type === 'Text'" :icon="'NodeText'" :style="'node'" title="Path" />
</LayoutRow>
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
<input
@ -98,7 +98,7 @@
</template>
<style lang="scss">
.layer-tree-panel {
.layer-tree {
min-height: 0;
.options-bar {
@ -117,7 +117,7 @@
}
}
.layer-tree {
.layer-tree-rows {
margin-top: 4px;
// Crop away the 1px border below the bottom layer entry when it uses the full space of this panel
margin-bottom: -1px;
@ -202,12 +202,6 @@
.layer-type-icon {
flex: 0 0 auto;
margin: 0 4px;
.icon-label {
border-radius: 2px;
background: var(--color-node-background);
fill: var(--color-node-icon);
}
}
.layer-name {
@ -289,7 +283,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { BlendMode, DisplayDocumentLayerTreeStructure, UpdateDocumentLayer, LayerPanelEntry } from "@/dispatcher/js-messages";
import { BlendMode, UpdateDocumentLayerTreeStructure, UpdateDocumentLayer, LayerPanelEntry } from "@/dispatcher/js-messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -573,14 +567,14 @@ export default defineComponent({
},
},
mounted() {
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[];
this.layers = [] as LayerListingInfo[];
const recurse = (folder: DisplayDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
const recurse = (folder: UpdateDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
folder.children.forEach((item, index) => {
// TODO: fix toString
const layerId = BigInt(item.layerId.toString());
@ -603,7 +597,7 @@ export default defineComponent({
});
};
recurse(displayDocumentLayerTreeStructure, this.layers, this.layerCache);
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
});
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayer, (updateDocumentLayer) => {

View file

@ -0,0 +1,488 @@
<template>
<LayoutCol class="node-graph">
<LayoutRow class="options-bar"></LayoutRow>
<LayoutRow
class="graph"
@wheel="(e) => scroll(e)"
ref="graph"
@pointerdown="(e) => pointerDown(e)"
@pointermove="(e) => pointerMove(e)"
@pointerup="(e) => pointerUp(e)"
:style="`--grid-spacing: ${gridSpacing}px; --grid-offset-x: ${transform.x * transform.scale}px; --grid-offset-y: ${transform.y * transform.scale}px; --dot-radius: ${dotRadius}px`"
>
<div
class="nodes"
ref="nodesContainer"
:style="{
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
transformOrigin: `0 0`,
}"
>
<div class="node" style="--offset-left: 3; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeImage'" :style="'node'" />
<TextLabel>Image</TextLabel>
</div>
</div>
<div class="node" style="--offset-left: 9; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<div class="input port" data-datatype="raster">
<div></div>
</div>
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeImage'" :style="'node'" />
<TextLabel>Mask</TextLabel>
</div>
<div class="arguments">
<div class="argument">
<div class="ports">
<div class="input port" data-datatype="raster" style="--data-color: var(--color-data-raster); --data-color-dim: var(--color-data-vector-dim)">
<div></div>
</div>
<!-- <div class="output port" data-datatype="raster">
<div></div>
</div> -->
</div>
<TextLabel>Stencil</TextLabel>
</div>
</div>
</div>
<div class="node" style="--offset-left: 15; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeTransform'" :style="'node'" />
<TextLabel>Transform</TextLabel>
</div>
</div>
<div class="node" style="--offset-left: 21; --offset-top: 2; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<div class="input port" data-datatype="raster">
<div></div>
</div>
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeMotionBlur'" :style="'node'" />
<TextLabel>Motion Blur</TextLabel>
</div>
<div class="arguments">
<div class="argument">
<div class="ports">
<div class="input port" data-datatype="raster">
<div></div>
</div>
<!-- <div class="output port" data-datatype="raster">
<div></div>
</div> -->
</div>
<TextLabel>Strength</TextLabel>
</div>
</div>
</div>
<div class="node" style="--offset-left: 2; --offset-top: 5; --data-color: var(--color-data-vector); --data-color-dim: var(--color-data-vector-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="vector">
<div></div>
</div> -->
<div class="output port" data-datatype="vector">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeShape'" :style="'node'" />
<TextLabel>Shape</TextLabel>
</div>
</div>
<div class="node" style="--offset-left: 6; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeBrushwork'" :style="'node'" />
<TextLabel>Brushwork</TextLabel>
</div>
</div>
<div class="node" style="--offset-left: 12; --offset-top: 7; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeBlur'" :style="'node'" />
<TextLabel>Blur</TextLabel>
</div>
</div>
<div class="node" style="--offset-left: 12; --offset-top: 9; --data-color: var(--color-data-raster); --data-color-dim: var(--color-data-raster-dim)">
<div class="primary">
<div class="ports">
<!-- <div class="input port" data-datatype="raster">
<div></div>
</div> -->
<div class="output port" data-datatype="raster">
<div></div>
</div>
</div>
<IconLabel :icon="'NodeGradient'" :style="'node'" />
<TextLabel>Gradient</TextLabel>
</div>
</div>
</div>
<div
class="wires"
:style="{
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
transformOrigin: `0 0`,
}"
>
<svg ref="wiresContainer"></svg>
</div>
</LayoutRow>
</LayoutCol>
</template>
<style lang="scss">
.node-graph {
height: 100%;
.options-bar {
height: 32px;
margin: 0 4px;
flex: 0 0 auto;
align-items: center;
}
.graph {
position: relative;
background: var(--color-2-mildblack);
width: calc(100% - 8px);
margin-left: 4px;
margin-bottom: 4px;
border-radius: 2px;
overflow: hidden;
// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-size: var(--grid-spacing) var(--grid-spacing);
background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius));
background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0);
image-rendering: pixelated;
}
}
.nodes,
.wires {
position: absolute;
width: 100%;
height: 100%;
&.wires {
width: 100%;
height: 100%;
pointer-events: none;
svg {
width: 100%;
height: 100%;
path {
fill: none;
// stroke: var(--color-data-raster-dim);
stroke: var(--data-color-dim);
stroke-width: 2px;
}
}
}
&.nodes {
.node {
position: absolute;
display: flex;
flex-direction: column;
min-width: 120px;
border-radius: 4px;
background: var(--color-4-dimgray);
left: calc(var(--offset-left) * 24px);
top: calc(var(--offset-top) * 24px);
.primary {
display: flex;
align-items: center;
position: relative;
gap: 4px;
width: 100%;
height: 24px;
background: var(--color-6-lowergray);
border-radius: 4px;
.icon-label {
margin-left: 4px;
}
.text-label {
margin-right: 4px;
}
}
.arguments {
display: flex;
width: 100%;
position: relative;
.argument {
position: relative;
display: flex;
align-items: center;
height: 24px;
width: 100%;
margin-left: 24px;
margin-right: 24px;
}
// Squares to cover up the rounded corners of the primary area and make them have a straight edge
&::before,
&::after {
content: "";
position: absolute;
background: var(--color-6-lowergray);
width: 4px;
height: 4px;
top: -4px;
}
&::before {
left: 0;
}
&::after {
right: 0;
}
}
.ports {
position: absolute;
width: 100%;
height: 100%;
.port {
position: absolute;
margin: auto 0;
top: 0;
bottom: 0;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--data-color-dim);
// background: var(--color-data-raster-dim);
div {
background: var(--data-color);
// background: var(--color-data-raster);
width: 8px;
height: 8px;
border-radius: 50%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
&.input {
left: calc(-12px - 6px);
}
&.output {
right: calc(-12px - 6px);
}
}
}
}
}
}
}
</style>
<script lang="ts">
import { defineComponent } from "vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
const WHEEL_RATE = 1 / 600;
const GRID_COLLAPSE_SPACING = 10;
const GRID_SIZE = 24;
export default defineComponent({
inject: ["editor"],
data() {
return {
transform: { scale: 1, x: 0, y: 0 },
panning: false,
drawing: undefined as { port: HTMLElement; output: boolean; path: SVGElement } | undefined,
};
},
computed: {
gridSpacing(): number {
const dense = this.transform.scale * GRID_SIZE;
let sparse = dense;
while (sparse > 0 && sparse < GRID_COLLAPSE_SPACING) {
sparse *= 2;
}
return sparse;
},
dotRadius(): number {
return 1 + Math.floor(this.transform.scale + 0.001) / 2;
},
},
methods: {
buildWirePathString(outputBounds: DOMRect, inputBounds: DOMRect, verticalOut: boolean, verticalIn: boolean): string {
const containerBounds = (this.$refs.nodesContainer as HTMLElement).getBoundingClientRect();
const outX = verticalOut ? outputBounds.x + outputBounds.width / 2 : outputBounds.x + outputBounds.width - 1;
const outY = verticalOut ? outputBounds.y + 1 : outputBounds.y + outputBounds.height / 2;
const outConnectorX = (outX - containerBounds.x) / this.transform.scale;
const outConnectorY = (outY - containerBounds.y) / this.transform.scale;
const inX = verticalIn ? inputBounds.x + inputBounds.width / 2 : inputBounds.x + 1;
const inY = verticalIn ? inputBounds.y + inputBounds.height - 1 : inputBounds.y + inputBounds.height / 2;
const inConnectorX = (inX - containerBounds.x) / this.transform.scale;
const inConnectorY = (inY - containerBounds.y) / this.transform.scale;
// debugger;
const horizontalGap = Math.abs(outConnectorX - inConnectorX);
const verticalGap = Math.abs(outConnectorY - inConnectorY);
const curveLength = 200;
const curveFalloffRate = curveLength * Math.PI * 2;
const horizontalCurveAmount = -(2 ** ((-10 * horizontalGap) / curveFalloffRate)) + 1;
const verticalCurveAmount = -(2 ** ((-10 * verticalGap) / curveFalloffRate)) + 1;
const horizontalCurve = horizontalCurveAmount * curveLength;
const verticalCurve = verticalCurveAmount * curveLength;
return `M${outConnectorX},${outConnectorY} C${verticalOut ? outConnectorX : outConnectorX + horizontalCurve},${verticalOut ? outConnectorY - verticalCurve : outConnectorY} ${
verticalIn ? inConnectorX : inConnectorX - horizontalCurve
},${verticalIn ? inConnectorY + verticalCurve : inConnectorY} ${inConnectorX},${inConnectorY}`;
},
createWirePath(outputPort: HTMLElement, inputPort: HTMLElement, verticalOut: boolean, verticalIn: boolean): SVGPathElement {
const pathString = this.buildWirePathString(outputPort.getBoundingClientRect(), inputPort.getBoundingClientRect(), verticalOut, verticalIn);
const dataType = outputPort.dataset.datatype;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pathString);
path.setAttribute("style", `--data-color: var(--color-data-${dataType}); --data-color-dim: var(--color-data-${dataType}-dim)`);
(this.$refs.wiresContainer as HTMLElement).appendChild(path);
return path;
},
scroll(e: WheelEvent) {
const scroll = e.deltaY;
let zoomFactor = 1 + Math.abs(scroll) * WHEEL_RATE;
if (scroll > 0) zoomFactor = 1 / zoomFactor;
const { x, y, width, height } = ((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).getBoundingClientRect();
this.transform.scale *= zoomFactor;
const newViewportX = width / zoomFactor;
const newViewportY = height / zoomFactor;
const deltaSizeX = width - newViewportX;
const deltaSizeY = height - newViewportY;
const deltaX = deltaSizeX * ((e.x - x) / width);
const deltaY = deltaSizeY * ((e.y - y) / height);
this.transform.x -= (deltaX / this.transform.scale) * zoomFactor;
this.transform.y -= (deltaY / this.transform.scale) * zoomFactor;
},
pointerDown(e: PointerEvent) {
const port = (e.target as HTMLElement).closest(".port") as HTMLElement;
if (port) {
const output = port.classList.contains("output");
const path = this.createWirePath(port, port, false, false);
this.drawing = { port, output, path };
} else {
this.panning = true;
}
((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).setPointerCapture(e.pointerId);
},
pointerMove(e: PointerEvent) {
if (this.panning) {
this.transform.x += e.movementX / this.transform.scale;
this.transform.y += e.movementY / this.transform.scale;
} else if (this.drawing) {
const mouse = new DOMRect(e.x, e.y);
const port = this.drawing.port.getBoundingClientRect();
const output = this.drawing.output ? port : mouse;
const input = this.drawing.output ? mouse : port;
const pathString = this.buildWirePathString(output, input, false, false);
this.drawing.path.setAttribute("d", pathString);
}
},
pointerUp(e: PointerEvent) {
((this.$refs.graph as typeof LayoutCol).$el as HTMLElement).releasePointerCapture(e.pointerId);
this.panning = false;
this.drawing = undefined;
},
},
mounted() {
{
const outputPort = document.querySelectorAll(".output.port")[4] as HTMLElement;
const inputPort = document.querySelectorAll(".input.port")[1] as HTMLElement;
this.createWirePath(outputPort, inputPort, true, true);
}
{
const outputPort = document.querySelectorAll(".output.port")[6] as HTMLElement;
const inputPort = document.querySelectorAll(".input.port")[3] as HTMLElement;
this.createWirePath(outputPort, inputPort, true, false);
}
},
components: {
LayoutRow,
LayoutCol,
IconLabel,
TextLabel,
},
});
</script>

View file

@ -154,7 +154,14 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
{
label: "View",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
children: [
[
{
label: "Show/Hide Node Graph (In Development)",
action: async (): Promise<void> => editor.instance.toggle_node_graph_visibility(),
},
],
],
},
{
label: "Help",
@ -190,7 +197,7 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
}
export default defineComponent({
inject: ["editor", "dialog"],
inject: ["workspace", "editor", "dialog"],
methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof MenuList) {
if (ref) menuEntry.ref = ref;

View file

@ -1,5 +1,5 @@
<template>
<LayoutRow class="icon-label" :class="`size-${icons[icon].size}`">
<LayoutRow :class="['icon-label', iconSize, iconStyle]">
<component :is="icon" />
</LayoutRow>
</template>
@ -23,13 +23,19 @@
width: 24px;
height: 24px;
}
&.node-style {
border-radius: 2px;
background: var(--color-node-background);
fill: var(--color-node-icon);
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { IconName, icons } from "@/utilities/icons";
import { IconName, IconStyle, icons, iconComponents } from "@/utilities/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -37,16 +43,20 @@ export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
gapAfter: { type: Boolean as PropType<boolean>, default: false },
style: { type: String as PropType<IconStyle>, default: "" },
},
data() {
return {
icons,
};
computed: {
iconSize(): string {
return `size-${icons[this.icon].size}`;
},
iconStyle(): string {
if (!this.style) return "";
return `${this.style}-style`;
},
},
components: {
LayoutRow,
// Import the components of all the icons
...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])),
...iconComponents,
},
});
</script>

View file

@ -50,14 +50,14 @@ import WindowTitle from "@/components/window/title-bar/WindowTitle.vue";
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
export default defineComponent({
inject: ["documents"],
inject: ["portfolio"],
props: {
platform: { type: String as PropType<Platform>, required: true },
maximized: { type: Boolean as PropType<boolean>, required: true },
},
computed: {
activeDocumentDisplayName() {
return this.documents.state.documents[this.documents.state.activeDocumentIndex].displayName;
return this.portfolio.state.documents[this.portfolio.state.activeDocumentIndex].displayName;
},
},
components: {

View file

@ -29,7 +29,7 @@
<style lang="scss">
.panel {
background: var(--color-1-nearblack);
border-radius: 8px;
border-radius: 6px;
overflow: hidden;
.tab-bar {
@ -64,7 +64,7 @@
&.active {
background: var(--color-3-darkgray);
border-radius: 8px 8px 0 0;
border-radius: 6px 6px 0 0;
position: relative;
&:not(:first-child)::before,
@ -152,6 +152,7 @@ import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Document from "@/components/panels/Document.vue";
import LayerTree from "@/components/panels/LayerTree.vue";
import NodeGraph from "@/components/panels/NodeGraph.vue";
import Properties from "@/components/panels/Properties.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
@ -160,13 +161,14 @@ const panelComponents = {
Document,
Properties,
LayerTree,
NodeGraph,
IconButton,
PopoverButton,
};
type PanelTypes = keyof typeof panelComponents;
export default defineComponent({
inject: ["documents"],
inject: ["portfolio"],
props: {
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },

View file

@ -2,16 +2,22 @@
<LayoutRow class="workspace" data-workspace>
<LayoutRow class="workspace-grid-subdivision">
<LayoutCol class="workspace-grid-subdivision">
<Panel
:panelType="'Document'"
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)"
:clickAction="(tabIndex) => editor.instance.select_document(documents.state.documents[tabIndex].id)"
:closeAction="(tabIndex) => editor.instance.close_document_with_confirmation(documents.state.documents[tabIndex].id)"
:tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel"
/>
<LayoutRow class="workspace-grid-subdivision">
<Panel
:panelType="'Document'"
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="portfolio.state.documents.map((doc) => doc.displayName)"
:clickAction="(tabIndex) => editor.instance.select_document(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex) => editor.instance.close_document_with_confirmation(portfolio.state.documents[tabIndex].id)"
:tabActiveIndex="portfolio.state.activeDocumentIndex"
ref="documentsPanel"
/>
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
<Panel :panelType="'NodeGraph'" :tabLabels="['Node Graph']" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
@ -68,7 +74,7 @@ import Panel from "@/components/workspace/Panel.vue";
const MIN_PANEL_SIZE = 100;
export default defineComponent({
inject: ["documents", "dialog", "editor"],
inject: ["workspace", "portfolio", "dialog", "editor"],
components: {
LayoutRow,
LayoutCol,
@ -77,7 +83,10 @@ export default defineComponent({
},
computed: {
activeDocumentIndex() {
return this.documents.state.activeDocumentIndex;
return this.portfolio.state.activeDocumentIndex;
},
nodeGraphVisible() {
return this.workspace.state.nodeGraphVisible;
},
},
methods: {

View file

@ -40,6 +40,10 @@ export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: BigInt;
}
export class UpdateNodeGraphVisibility extends JsMessage {
readonly visible!: boolean;
}
export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => FrontendDocumentDetails)
readonly open_documents!: FrontendDocumentDetails[];
@ -234,8 +238,8 @@ export class TriggerRasterDownload extends JsMessage {
export class DocumentChanged extends JsMessage {}
export class DisplayDocumentLayerTreeStructure extends JsMessage {
constructor(readonly layerId: BigInt, readonly children: DisplayDocumentLayerTreeStructure[]) {
export class UpdateDocumentLayerTreeStructure extends JsMessage {
constructor(readonly layerId: BigInt, readonly children: UpdateDocumentLayerTreeStructure[]) {
super();
}
}
@ -245,7 +249,7 @@ interface DataBuffer {
length: BigInt;
}
export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure {
export function newUpdateDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): UpdateDocumentLayerTreeStructure {
const pointerNum = Number(input.data_buffer.pointer);
const lengthNum = Number(input.data_buffer.length);
@ -262,7 +266,7 @@ export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataB
const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8);
let layersEncountered = 0;
let currentFolder = new DisplayDocumentLayerTreeStructure(BigInt(-1), []);
let currentFolder = new UpdateDocumentLayerTreeStructure(BigInt(-1), []);
const currentFolderStack = [currentFolder];
for (let i = 0; i < structureSectionLength; i += 1) {
@ -277,7 +281,7 @@ export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataB
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
layersEncountered += 1;
const childLayer = new DisplayDocumentLayerTreeStructure(layerId, []);
const childLayer = new UpdateDocumentLayerTreeStructure(layerId, []);
currentFolder.children.push(childLayer);
}
@ -546,7 +550,7 @@ type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageMakers: Record<string, MessageMaker> = {
DisplayDialog,
DisplayDialogPanic,
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
UpdateDocumentLayerTreeStructure: newUpdateDocumentLayerTreeStructure,
DisplayEditableTextbox,
UpdateImageData,
DisplayRemoveEditableTextbox,
@ -576,6 +580,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDocumentScrollbars,
UpdateInputHints,
UpdateMouseCursor,
UpdateNodeGraphVisibility,
UpdateOpenDocumentsList,
UpdatePropertyPanelOptionsLayout,
UpdatePropertyPanelSectionsLayout,

View file

@ -1,5 +1,5 @@
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument } from "@/dispatcher/js-messages";
import { DocumentsState } from "@/state/documents";
import { PortfolioState } from "@/state/portfolio";
import { EditorState, getWasmInstance } from "@/state/wasm-loader";
const GRAPHITE_INDEXED_DB_VERSION = 2;
@ -31,7 +31,7 @@ const databaseConnection: Promise<IDBDatabase> = new Promise((resolve) => {
});
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createAutoSaveManager(editor: EditorState, documents: DocumentsState) {
export function createAutoSaveManager(editor: EditorState, portfolio: PortfolioState) {
const openAutoSavedDocuments = async (): Promise<void> => {
const db = await databaseConnection;
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readonly");
@ -61,7 +61,7 @@ export function createAutoSaveManager(editor: EditorState, documents: DocumentsS
const storeDocumentOrder = (): void => {
// Make sure to store as string since JSON does not play nice with BigInt
const documentOrder = documents.state.documents.map((doc) => doc.id.toString());
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
};

View file

@ -1,6 +1,6 @@
import { DialogState } from "@/state/dialog";
import { DocumentsState } from "@/state/documents";
import { FullscreenState } from "@/state/fullscreen";
import { PortfolioState } from "@/state/portfolio";
import { EditorState } from "@/state/wasm-loader";
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
@ -10,7 +10,7 @@ interface EventListenerTarget {
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createInputManager(editor: EditorState, container: HTMLElement, dialog: DialogState, document: DocumentsState, fullscreen: FullscreenState) {
export function createInputManager(editor: EditorState, container: HTMLElement, dialog: DialogState, document: PortfolioState, fullscreen: FullscreenState) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
{ target: window, eventName: "resize", action: (): void => onWindowResize(container) },

View file

@ -6,7 +6,7 @@ import { EditorState } from "@/state/wasm-loader";
import { download, downloadBlob, upload } from "@/utilities/files";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDocumentsState(editor: EditorState) {
export function createPortfolioState(editor: EditorState) {
const state = reactive({
unsaved: false,
documents: [] as FrontendDocumentDetails[],
@ -76,4 +76,4 @@ export function createDocumentsState(editor: EditorState) {
state: readonly(state),
};
}
export type DocumentsState = ReturnType<typeof createDocumentsState>;
export type PortfolioState = ReturnType<typeof createPortfolioState>;

View file

@ -0,0 +1,22 @@
/* eslint-disable max-classes-per-file */
import { reactive, readonly } from "vue";
import { UpdateNodeGraphVisibility } from "@/dispatcher/js-messages";
import { EditorState } from "@/state/wasm-loader";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createWorkspaceState(editor: EditorState) {
const state = reactive({
nodeGraphVisible: false,
});
// Set up message subscriptions on creation
editor.dispatcher.subscribeJsMessage(UpdateNodeGraphVisibility, (updateNodeGraphVisibility) => {
state.nodeGraphVisible = updateNodeGraphVisibility.visible;
});
return {
state: readonly(state),
};
}
export type WorkspaceState = ReturnType<typeof createWorkspaceState>;

View file

@ -1,3 +1,5 @@
/* eslint-disable import/first */
// 12px Solid
import Checkmark from "@/../assets/12px-solid/checkmark.svg";
import CloseX from "@/../assets/12px-solid/close-x.svg";
@ -29,78 +31,7 @@ import WindowButtonWinMaximize from "@/../assets/12px-solid/window-button-win-ma
import WindowButtonWinMinimize from "@/../assets/12px-solid/window-button-win-minimize.svg";
import WindowButtonWinRestoreDown from "@/../assets/12px-solid/window-button-win-restore-down.svg";
// 16px Solid
import AlignBottom from "@/../assets/16px-solid/align-bottom.svg";
import AlignHorizontalCenter from "@/../assets/16px-solid/align-horizontal-center.svg";
import AlignLeft from "@/../assets/16px-solid/align-left.svg";
import AlignRight from "@/../assets/16px-solid/align-right.svg";
import AlignTop from "@/../assets/16px-solid/align-top.svg";
import AlignVerticalCenter from "@/../assets/16px-solid/align-vertical-center.svg";
import BooleanDifference from "@/../assets/16px-solid/boolean-difference.svg";
import BooleanIntersect from "@/../assets/16px-solid/boolean-intersect.svg";
import BooleanSubtractBack from "@/../assets/16px-solid/boolean-subtract-back.svg";
import BooleanSubtractFront from "@/../assets/16px-solid/boolean-subtract-front.svg";
import BooleanUnion from "@/../assets/16px-solid/boolean-union.svg";
import Copy from "@/../assets/16px-solid/copy.svg";
import EyeHidden from "@/../assets/16px-solid/eye-hidden.svg";
import EyeVisible from "@/../assets/16px-solid/eye-visible.svg";
import File from "@/../assets/16px-solid/file.svg";
import FlipHorizontal from "@/../assets/16px-solid/flip-horizontal.svg";
import FlipVertical from "@/../assets/16px-solid/flip-vertical.svg";
import GraphiteLogo from "@/../assets/16px-solid/graphite-logo.svg";
import NodeArtboard from "@/../assets/16px-solid/node-artboard.svg";
import NodeFolder from "@/../assets/16px-solid/node-folder.svg";
import NodeImage from "@/../assets/16px-solid/node-image.svg";
import NodeShape from "@/../assets/16px-solid/node-shape.svg";
import NodeText from "@/../assets/16px-solid/node-text.svg";
import Paste from "@/../assets/16px-solid/paste.svg";
import Trash from "@/../assets/16px-solid/trash.svg";
import ViewModeNormal from "@/../assets/16px-solid/view-mode-normal.svg";
import ViewModeOutline from "@/../assets/16px-solid/view-mode-outline.svg";
import ViewModePixels from "@/../assets/16px-solid/view-mode-pixels.svg";
import ViewportDesignMode from "@/../assets/16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@/../assets/16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@/../assets/16px-solid/viewport-select-mode.svg";
import ZoomIn from "@/../assets/16px-solid/zoom-in.svg";
import ZoomOut from "@/../assets/16px-solid/zoom-out.svg";
import ZoomReset from "@/../assets/16px-solid/zoom-reset.svg";
// 16px Two-Tone
import MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg";
import MouseHintLmbDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-drag.svg";
import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
import MouseHintMmbDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg";
import MouseHintMmb from "@/../assets/16px-two-tone/mouse-hint-mmb.svg";
import MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg";
import MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg";
import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.svg";
import MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg";
// 24px Two-Tone
import GeneralArtboardTool from "@/../assets/24px-two-tone/general-artboard-tool.svg";
import GeneralEyedropperTool from "@/../assets/24px-two-tone/general-eyedropper-tool.svg";
import GeneralFillTool from "@/../assets/24px-two-tone/general-fill-tool.svg";
import GeneralGradientTool from "@/../assets/24px-two-tone/general-gradient-tool.svg";
import GeneralNavigateTool from "@/../assets/24px-two-tone/general-navigate-tool.svg";
import GeneralSelectTool from "@/../assets/24px-two-tone/general-select-tool.svg";
import RasterBrushTool from "@/../assets/24px-two-tone/raster-brush-tool.svg";
import RasterCloneTool from "@/../assets/24px-two-tone/raster-clone-tool.svg";
import RasterDetailTool from "@/../assets/24px-two-tone/raster-detail-tool.svg";
import RasterHealTool from "@/../assets/24px-two-tone/raster-heal-tool.svg";
import RasterPatchTool from "@/../assets/24px-two-tone/raster-patch-tool.svg";
import RasterRelightTool from "@/../assets/24px-two-tone/raster-relight-tool.svg";
import VectorEllipseTool from "@/../assets/24px-two-tone/vector-ellipse-tool.svg";
import VectorFreehandTool from "@/../assets/24px-two-tone/vector-freehand-tool.svg";
import VectorLineTool from "@/../assets/24px-two-tone/vector-line-tool.svg";
import VectorPathTool from "@/../assets/24px-two-tone/vector-path-tool.svg";
import VectorPenTool from "@/../assets/24px-two-tone/vector-pen-tool.svg";
import VectorRectangleTool from "@/../assets/24px-two-tone/vector-rectangle-tool.svg";
import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
const ICON_LIST = {
const SOLID_12PX = {
Checkmark: { component: Checkmark, size: 12 },
CloseX: { component: CloseX, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
@ -130,7 +61,54 @@ const ICON_LIST = {
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
} as const;
// 16px Solid
import AlignBottom from "@/../assets/16px-solid/align-bottom.svg";
import AlignHorizontalCenter from "@/../assets/16px-solid/align-horizontal-center.svg";
import AlignLeft from "@/../assets/16px-solid/align-left.svg";
import AlignRight from "@/../assets/16px-solid/align-right.svg";
import AlignTop from "@/../assets/16px-solid/align-top.svg";
import AlignVerticalCenter from "@/../assets/16px-solid/align-vertical-center.svg";
import BooleanDifference from "@/../assets/16px-solid/boolean-difference.svg";
import BooleanIntersect from "@/../assets/16px-solid/boolean-intersect.svg";
import BooleanSubtractBack from "@/../assets/16px-solid/boolean-subtract-back.svg";
import BooleanSubtractFront from "@/../assets/16px-solid/boolean-subtract-front.svg";
import BooleanUnion from "@/../assets/16px-solid/boolean-union.svg";
import Copy from "@/../assets/16px-solid/copy.svg";
import EyeHidden from "@/../assets/16px-solid/eye-hidden.svg";
import EyeVisible from "@/../assets/16px-solid/eye-visible.svg";
import File from "@/../assets/16px-solid/file.svg";
import FlipHorizontal from "@/../assets/16px-solid/flip-horizontal.svg";
import FlipVertical from "@/../assets/16px-solid/flip-vertical.svg";
import GraphiteLogo from "@/../assets/16px-solid/graphite-logo.svg";
import NodeArtboard from "@/../assets/16px-solid/node-artboard.svg";
import NodeBlur from "@/../assets/16px-solid/node-blur.svg";
import NodeBrushwork from "@/../assets/16px-solid/node-brushwork.svg";
import NodeColorCorrection from "@/../assets/16px-solid/node-color-correction.svg";
import NodeFolder from "@/../assets/16px-solid/node-folder.svg";
import NodeGradient from "@/../assets/16px-solid/node-gradient.svg";
import NodeImage from "@/../assets/16px-solid/node-image.svg";
import NodeMagicWand from "@/../assets/16px-solid/node-magic-wand.svg";
import NodeMask from "@/../assets/16px-solid/node-mask.svg";
import NodeMotionBlur from "@/../assets/16px-solid/node-motion-blur.svg";
import NodeOutput from "@/../assets/16px-solid/node-output.svg";
import NodeShape from "@/../assets/16px-solid/node-shape.svg";
import NodeText from "@/../assets/16px-solid/node-text.svg";
import NodeTransform from "@/../assets/16px-solid/node-transform.svg";
import Paste from "@/../assets/16px-solid/paste.svg";
import Trash from "@/../assets/16px-solid/trash.svg";
import ViewModeNormal from "@/../assets/16px-solid/view-mode-normal.svg";
import ViewModeOutline from "@/../assets/16px-solid/view-mode-outline.svg";
import ViewModePixels from "@/../assets/16px-solid/view-mode-pixels.svg";
import ViewportDesignMode from "@/../assets/16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@/../assets/16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@/../assets/16px-solid/viewport-select-mode.svg";
import ZoomIn from "@/../assets/16px-solid/zoom-in.svg";
import ZoomOut from "@/../assets/16px-solid/zoom-out.svg";
import ZoomReset from "@/../assets/16px-solid/zoom-reset.svg";
const SOLID_16PX = {
AlignBottom: { component: AlignBottom, size: 16 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
AlignLeft: { component: AlignLeft, size: 16 },
@ -150,10 +128,19 @@ const ICON_LIST = {
FlipVertical: { component: FlipVertical, size: 16 },
GraphiteLogo: { component: GraphiteLogo, size: 16 },
NodeArtboard: { component: NodeArtboard, size: 16 },
NodeBlur: { component: NodeBlur, size: 16 },
NodeBrushwork: { component: NodeBrushwork, size: 16 },
NodeColorCorrection: { component: NodeColorCorrection, size: 16 },
NodeFolder: { component: NodeFolder, size: 16 },
NodeGradient: { component: NodeGradient, size: 16 },
NodeImage: { component: NodeImage, size: 16 },
NodeMagicWand: { component: NodeMagicWand, size: 16 },
NodeMask: { component: NodeMask, size: 16 },
NodeMotionBlur: { component: NodeMotionBlur, size: 16 },
NodeOutput: { component: NodeOutput, size: 16 },
NodeShape: { component: NodeShape, size: 16 },
NodeText: { component: NodeText, size: 16 },
NodeTransform: { component: NodeTransform, size: 16 },
Paste: { component: Paste, size: 16 },
Trash: { component: Trash, size: 16 },
ViewModeNormal: { component: ViewModeNormal, size: 16 },
@ -165,18 +152,57 @@ const ICON_LIST = {
ZoomIn: { component: ZoomIn, size: 16 },
ZoomOut: { component: ZoomOut, size: 16 },
ZoomReset: { component: ZoomReset, size: 16 },
} as const;
// 16px Two-Tone
import MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg";
import MouseHintLmbDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-drag.svg";
import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
import MouseHintMmbDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg";
import MouseHintMmb from "@/../assets/16px-two-tone/mouse-hint-mmb.svg";
import MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg";
import MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg";
import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.svg";
import MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg";
const TWO_TONE_16PX = {
MouseHintDrag: { component: MouseHintDrag, size: 16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
MouseHintLmb: { component: MouseHintLmb, size: 16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
MouseHintMmb: { component: MouseHintMmb, size: 16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
MouseHintNone: { component: MouseHintNone, size: 16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
MouseHintRmb: { component: MouseHintRmb, size: 16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
} as const;
// 24px Two-Tone
import GeneralArtboardTool from "@/../assets/24px-two-tone/general-artboard-tool.svg";
import GeneralEyedropperTool from "@/../assets/24px-two-tone/general-eyedropper-tool.svg";
import GeneralFillTool from "@/../assets/24px-two-tone/general-fill-tool.svg";
import GeneralGradientTool from "@/../assets/24px-two-tone/general-gradient-tool.svg";
import GeneralNavigateTool from "@/../assets/24px-two-tone/general-navigate-tool.svg";
import GeneralSelectTool from "@/../assets/24px-two-tone/general-select-tool.svg";
import RasterBrushTool from "@/../assets/24px-two-tone/raster-brush-tool.svg";
import RasterCloneTool from "@/../assets/24px-two-tone/raster-clone-tool.svg";
import RasterDetailTool from "@/../assets/24px-two-tone/raster-detail-tool.svg";
import RasterHealTool from "@/../assets/24px-two-tone/raster-heal-tool.svg";
import RasterPatchTool from "@/../assets/24px-two-tone/raster-patch-tool.svg";
import RasterRelightTool from "@/../assets/24px-two-tone/raster-relight-tool.svg";
import VectorEllipseTool from "@/../assets/24px-two-tone/vector-ellipse-tool.svg";
import VectorFreehandTool from "@/../assets/24px-two-tone/vector-freehand-tool.svg";
import VectorLineTool from "@/../assets/24px-two-tone/vector-line-tool.svg";
import VectorPathTool from "@/../assets/24px-two-tone/vector-path-tool.svg";
import VectorPenTool from "@/../assets/24px-two-tone/vector-pen-tool.svg";
import VectorRectangleTool from "@/../assets/24px-two-tone/vector-rectangle-tool.svg";
import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
const TWO_TONE_24PX = {
GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
@ -199,10 +225,22 @@ const ICON_LIST = {
VectorSplineTool: { component: VectorSplineTool, size: 24 },
VectorTextTool: { component: VectorTextTool, size: 24 },
} as const;
// All icons
const ICON_LIST = {
...SOLID_12PX,
...SOLID_16PX,
...TWO_TONE_16PX,
...TWO_TONE_24PX,
} as const;
// Exported icons and types
export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
export const iconComponents = Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component]));
export type IconName = keyof typeof icons;
export type IconSize = 12 | 16 | 24 | 32;
export type IconStyle = "node" | "";
// The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
// icon definitions. It lets TypeScript do that for us. Our goal is to define the big icons key-value pair by constraining its values, but inferring its keys.

View file

@ -84,14 +84,18 @@ impl JsEditorHandle {
}
// ========================================================================
// Add additional JS -> Rust wrapper functions below as needed for calling the
// backend from the web frontend.
// Add additional JS -> Rust wrapper functions below as needed for calling
// the backend from the web frontend.
// ========================================================================
pub fn has_crashed(&self) -> bool {
EDITOR_HAS_CRASHED.load(Ordering::SeqCst)
}
pub fn toggle_node_graph_visibility(&self) {
self.dispatch(WorkspaceMessage::NodeGraphToggleVisibility);
}
/// Modify the currently selected tool in the document state store
pub fn select_tool(&self, tool: String) -> Result<(), JsValue> {
match translate_tool_type(&tool) {