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>
3
frontend/assets/16px-solid/node-blur.svg
Normal 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 |
5
frontend/assets/16px-solid/node-brushwork.svg
Normal 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 |
3
frontend/assets/16px-solid/node-color-correction.svg
Normal 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 |
3
frontend/assets/16px-solid/node-gradient.svg
Normal 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 |
6
frontend/assets/16px-solid/node-magic-wand.svg
Normal 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 |
3
frontend/assets/16px-solid/node-mask.svg
Normal 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 |
5
frontend/assets/16px-solid/node-motion-blur.svg
Normal 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 |
3
frontend/assets/16px-solid/node-output.svg
Normal 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 |
3
frontend/assets/16px-solid/node-transform.svg
Normal 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 |
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
488
frontend/src/components/panels/NodeGraph.vue
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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>;
|
22
frontend/src/state/workspace.ts
Normal 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>;
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|