mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Merge pull request #4667 from Comfy-Org/bl-merge-lg-fe
Merge ComfyUI_frontend and litegraph.js
This commit is contained in:
@@ -171,53 +171,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
|
||||
### Step 7: Analyze Dependency Updates
|
||||
|
||||
1. **Check for dependency version changes:**
|
||||
```bash
|
||||
# Compare package.json between versions to detect dependency updates
|
||||
PREV_PACKAGE_JSON=$(git show ${BASE_TAG}:package.json 2>/dev/null || echo '{}')
|
||||
CURRENT_PACKAGE_JSON=$(cat package.json)
|
||||
|
||||
# Extract litegraph versions
|
||||
PREV_LITEGRAPH=$(echo "$PREV_PACKAGE_JSON" | grep -o '"@comfyorg/litegraph": "[^"]*"' | grep -o '[0-9][^"]*' || echo "not found")
|
||||
CURRENT_LITEGRAPH=$(echo "$CURRENT_PACKAGE_JSON" | grep -o '"@comfyorg/litegraph": "[^"]*"' | grep -o '[0-9][^"]*' || echo "not found")
|
||||
|
||||
echo "Litegraph version change: ${PREV_LITEGRAPH} → ${CURRENT_LITEGRAPH}"
|
||||
```
|
||||
|
||||
2. **Generate litegraph changelog if version changed:**
|
||||
```bash
|
||||
if [ "$PREV_LITEGRAPH" != "$CURRENT_LITEGRAPH" ] && [ "$PREV_LITEGRAPH" != "not found" ]; then
|
||||
echo "📦 Fetching litegraph changes between v${PREV_LITEGRAPH} and v${CURRENT_LITEGRAPH}..."
|
||||
|
||||
# Clone or update litegraph repo for changelog analysis
|
||||
if [ ! -d ".temp-litegraph" ]; then
|
||||
git clone https://github.com/comfyanonymous/litegraph.js.git .temp-litegraph
|
||||
else
|
||||
cd .temp-litegraph && git fetch --all && cd ..
|
||||
fi
|
||||
|
||||
# Get litegraph changelog between versions
|
||||
LITEGRAPH_CHANGES=$(cd .temp-litegraph && git log v${PREV_LITEGRAPH}..v${CURRENT_LITEGRAPH} --oneline --no-merges 2>/dev/null || \
|
||||
git log --oneline --no-merges --since="$(git log -1 --format=%ci ${BASE_TAG})" --until="$(git log -1 --format=%ci HEAD)" 2>/dev/null || \
|
||||
echo "Unable to fetch litegraph changes")
|
||||
|
||||
# Categorize litegraph changes
|
||||
LITEGRAPH_FEATURES=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(feat|feature|add)" || echo "")
|
||||
LITEGRAPH_FIXES=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(fix|bug)" || echo "")
|
||||
LITEGRAPH_BREAKING=$(echo "$LITEGRAPH_CHANGES" | grep -iE "(break|breaking)" || echo "")
|
||||
LITEGRAPH_OTHER=$(echo "$LITEGRAPH_CHANGES" | grep -viE "(feat|feature|add|fix|bug|break|breaking)" || echo "")
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf .temp-litegraph
|
||||
|
||||
echo "✅ Litegraph changelog extracted"
|
||||
else
|
||||
echo "ℹ️ No litegraph version change detected"
|
||||
LITEGRAPH_CHANGES=""
|
||||
fi
|
||||
```
|
||||
|
||||
3. **Check other significant dependency updates:**
|
||||
1. **Check significant dependency updates:**
|
||||
```bash
|
||||
# Extract all dependency changes for major version bumps
|
||||
OTHER_DEP_CHANGES=""
|
||||
|
||||
21
.cursor/rules/unit-test.mdc
Normal file
21
.cursor/rules/unit-test.mdc
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Creating unit tests
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Creating unit tests
|
||||
|
||||
- This project uses `vitest` for unit testing
|
||||
- Tests are stored in the `test/` directory
|
||||
- Tests should be cross-platform compatible; able to run on Windows, macOS, and linux
|
||||
- e.g. the use of `path.resolve`, or `path.join` and `path.sep` to ensure that tests work the same on all platforms
|
||||
- Tests should be mocked properly
|
||||
- Mocks should be cleanly written and easy to understand
|
||||
- Mocks should be re-usable where possible
|
||||
|
||||
## Unit test style
|
||||
|
||||
- Prefer the use of `test.extend` over loose variables
|
||||
- To achieve this, import `test as baseTest` from `vitest`
|
||||
- Never use `it`; `test` should be used in place of this
|
||||
43
.github/workflows/update-litegraph.yaml
vendored
43
.github/workflows/update-litegraph.yaml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Update Litegraph Dependency
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-litegraph:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Update litegraph
|
||||
run: npm install @comfyorg/litegraph@latest
|
||||
|
||||
- name: Get new version
|
||||
id: get-version
|
||||
run: |
|
||||
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/litegraph'].version)")
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
title: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
Automated update of litegraph to version ${{ steps.get-version.outputs.NEW_VERSION }}.
|
||||
Ref: https://github.com/Comfy-Org/litegraph.js/releases/tag/v${{ steps.get-version.outputs.NEW_VERSION }}
|
||||
branch: update-litegraph-${{ steps.get-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
labels: |
|
||||
dependencies
|
||||
@@ -694,14 +694,7 @@ For detailed instructions on adding and using custom icons, see [src/assets/icon
|
||||
|
||||
### litegraph.js
|
||||
|
||||
This repo is using litegraph package hosted on <https://github.com/Comfy-Org/litegraph.js>. Any changes to litegraph should be submitted in that repo instead.
|
||||
|
||||
#### Test litegraph.js changes
|
||||
|
||||
- Run `npm link` in the local litegraph repo.
|
||||
- Run `npm link @comfyorg/litegraph` in this repo.
|
||||
|
||||
This will replace the litegraph package in this repo with the local litegraph repo.
|
||||
The litegraph library is now included as a git subtree in `src/lib/litegraph`. Any changes to litegraph should be made directly in this location.
|
||||
|
||||
### i18n
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -12,7 +12,6 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.17.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -976,12 +975,6 @@
|
||||
"integrity": "sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.17.1.tgz",
|
||||
"integrity": "sha512-SaDDWFvoH1bCfibvZjtX0JoLvFTJw2MUOWzrjyeuWVs00JpxiJ1I5f6oH/AO8lJqKdASWBVPzpC9zPMG45w4IQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.17.1",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -16,9 +16,7 @@ const typesPackage = {
|
||||
homepage: mainPackage.homepage,
|
||||
description: `TypeScript definitions for ${mainPackage.name}`,
|
||||
license: mainPackage.license,
|
||||
dependencies: {
|
||||
'@comfyorg/litegraph': mainPackage.dependencies['@comfyorg/litegraph']
|
||||
},
|
||||
dependencies: {},
|
||||
peerDependencies: {
|
||||
vue: mainPackage.dependencies.vue,
|
||||
zod: mainPackage.dependencies.zod
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Message from 'primevue/message'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { compareVersions } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
@@ -78,6 +77,7 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
|
||||
@@ -70,13 +70,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LiteGraph,
|
||||
isOverNodeInput,
|
||||
isOverNodeOutput
|
||||
} from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import {
|
||||
LiteGraph,
|
||||
isOverNodeInput,
|
||||
isOverNodeOutput
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { createBounds } from '@comfyorg/litegraph'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
@@ -13,13 +13,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphGroup, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { type CSSProperties, computed, ref, watch } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
// Mock the litegraph module
|
||||
vi.mock('@comfyorg/litegraph', async () => {
|
||||
const actual = await vi.importActual('@comfyorg/litegraph')
|
||||
vi.mock('@/lib/litegraph/src/litegraph', async () => {
|
||||
const actual = await vi.importActual('@/lib/litegraph/src/litegraph')
|
||||
return {
|
||||
...actual,
|
||||
LGraphCanvas: {
|
||||
|
||||
@@ -44,13 +44,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ColorOption as CanvasColorOption } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph, isColorable } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ColorOption as CanvasColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeId } from '@comfyorg/litegraph'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
@@ -33,18 +33,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@comfyorg/litegraph'
|
||||
import { Point } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import type { CanvasPointerEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
|
||||
|
||||
import { Point } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LiteGraphCanvasEvent
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface CanvasTransformSyncOptions {
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
LGraphNode,
|
||||
Positionable,
|
||||
Reroute
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Size, Vector2 } from '@comfyorg/litegraph'
|
||||
import { CSSProperties, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import type { Size, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LGraphCanvas, Vector2 } from '@comfyorg/litegraph'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
|
||||
import type { LGraphCanvas, Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Convert between canvas and client positions
|
||||
* @param canvasElement - The canvas element
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
|
||||
import { createImageHost } from '@/scripts/ui/imagePreview'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useImagePreviewWidget } from '@/composables/widgets/useImagePreviewWidget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
type DragHandler = (e: DragEvent) => boolean
|
||||
type DropHandler<T> = (files: File[]) => Promise<T[]>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
interface FileInputOptions {
|
||||
accept?: string
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
type PasteHandler<T> = (files: File[]) => Promise<T>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
/**
|
||||
* Function that calculates dynamic pricing based on node widget values
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computedWithControl } from '@vueuse/core'
|
||||
import { type ComputedRef, ref } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
export interface UseComputedWithWidgetWatchOptions {
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { NodeProperty } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
import { groupBy } from 'lodash'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { st, te } from '@/i18n'
|
||||
import type {
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
INodeInputSlot,
|
||||
IWidget
|
||||
} from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { st, te } from '@/i18n'
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import { Point } from '@comfyorg/litegraph'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import {
|
||||
@@ -13,6 +5,13 @@ import {
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Assign all properties of LiteGraph to window to make it backward compatible.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { CanvasPointer, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import {
|
||||
CanvasPointer,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ISerialisedGraph } from '@comfyorg/litegraph/dist/types/serialisation'
|
||||
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { validateComfyWorkflow } from '@/schemas/comfyWorkflowSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type InputSpec,
|
||||
isBooleanInputSpec
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
ComboInputSpec,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import {
|
||||
type InputSpec,
|
||||
isFloatInputSpec
|
||||
|
||||
@@ -3,12 +3,11 @@ import {
|
||||
type CanvasPointer,
|
||||
type LGraphNode,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { useNodeImageUpload } from '@/composables/node/useNodeImageUpload'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
type InputSpec,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { Editor as TiptapEditor } from '@tiptap/core'
|
||||
import TiptapLink from '@tiptap/extension-link'
|
||||
import TiptapTable from '@tiptap/extension-table'
|
||||
@@ -8,6 +7,7 @@ import TiptapTableRow from '@tiptap/extension-table-row'
|
||||
import TiptapStarterKit from '@tiptap/starter-kit'
|
||||
import { Markdown as TiptapMarkdown } from 'tiptap-markdown'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { type InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import axios from 'axios'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type InputSpec,
|
||||
isStringInputSpec
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { LinkMarkerShape, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
|
||||
import type { Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { LGraphCanvas, LiteGraph, isComboWidget } from '@comfyorg/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LiteGraph,
|
||||
isComboWidget
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { t } from '@/i18n'
|
||||
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
type ExecutionId,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import { type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
ComfyLink,
|
||||
ComfyNode,
|
||||
|
||||
@@ -2,8 +2,7 @@ import {
|
||||
type LGraphNode,
|
||||
type LGraphNodeConstructor,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
import { type ComfyApp, app } from '../../scripts/app'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { LGraphGroup } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
||||
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
@@ -7,6 +6,7 @@ import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { t } from '@/i18n'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
import { CameraManager } from './CameraManager'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { AnimationManager } from './AnimationManager'
|
||||
import Load3d from './Load3d'
|
||||
import { Load3DOptions } from './interfaces'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { NodeStorageInterface } from './interfaces'
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
@@ -8,6 +7,7 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
export type Load3DNodeType = 'Load3D' | 'Preview3D'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { IContextMenuValue } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IStringWidget
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
@@ -10,6 +5,11 @@ import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IStringWidget
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import {
|
||||
type CallbackParams,
|
||||
useChainCallback
|
||||
} from '@/composables/functional/useChainCallback'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
LLink,
|
||||
Vector2
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { CanvasPointerEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import {
|
||||
type CallbackParams,
|
||||
useChainCallback
|
||||
} from '@/composables/functional/useChainCallback'
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
||||
|
||||
203
src/lib/litegraph/API.md
Normal file
203
src/lib/litegraph/API.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# API
|
||||
|
||||
This document is intended to provide a brief introduction to new Litegraph APIs.
|
||||
|
||||
<detail open>
|
||||
|
||||
<summary>
|
||||
|
||||
# CanvasPointer API
|
||||
|
||||
</summary>
|
||||
|
||||
CanvasPointer replaces much of the original pointer handling code. It provides a standard click, double-click, and drag UX for users.
|
||||
|
||||
<detail open>
|
||||
|
||||
<summary>
|
||||
|
||||
## Default behaviour changes
|
||||
|
||||
</summary>
|
||||
|
||||
- Dragging multiple items no longer requires that the shift key be held down
|
||||
- Clicking an item when multiple nodes / etc are selected will still deselect everything else
|
||||
- Clicking a connected link on an input no longer disconnects and reconnects it
|
||||
- Double-clicking requires that both clicks occur nearby
|
||||
- Provides much room for extension, configuration, and changes
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Intermittent issue where clicking a node slightly displaces it
|
||||
- Alt-clicking to add a reroute creates two undo steps
|
||||
|
||||
### Selecting multiple items
|
||||
|
||||
- `Ctrl + drag` - Begin multi-select
|
||||
- `Ctrl + Shift + drag` - Add to selection
|
||||
- `Ctrl + drag`, `Shift` - Alternate add to selection
|
||||
- `Ctrl + drag`, `Alt` - Remove from selection
|
||||
|
||||
### Click "drift"
|
||||
|
||||
A small amount of buffering is performed between down/up events to prevent accidental micro-drag events. If either of the two controls are exceeded, the event will be considered a drag event, not a click.
|
||||
|
||||
- `buffterTime` is the maximum time that tiny movements can be ignored (Default: 150ms)
|
||||
- `maxClickDrift` controls how far a click can drift from its down event before it is considered a drag (Default: 6)
|
||||
|
||||
### Double-click
|
||||
|
||||
When double clicking, the double click callback is executed shortly after one normal click callback (if present). At present, dragging from the second click simply invalidates the event - nothing will happen.
|
||||
|
||||
- `doubleClickTime` is the maximum time between two `down` events for them to be considered a double click (Default: 300ms)
|
||||
- Distance between the two events must be less than `3 * maxClickDrift`
|
||||
|
||||
### Configuration
|
||||
|
||||
All above configuration is via class static.
|
||||
|
||||
```ts
|
||||
CanvasPointer.bufferTime = 150
|
||||
CanvasPointer.maxClickDrift = 6
|
||||
CanvasPointer.doubleClickTime = 300
|
||||
```
|
||||
|
||||
</detail>
|
||||
|
||||
<detail open>
|
||||
|
||||
<summary>
|
||||
|
||||
## Implementing
|
||||
|
||||
</summary>
|
||||
|
||||
Clicking, double-clicking, and dragging can now all be configured during the initial `pointerdown` event, and the correct callback(s) will be executed.
|
||||
|
||||
A click event can be as simple as:
|
||||
|
||||
```ts
|
||||
if (node.isClickedInSpot(e.canvasX, e.canvasY))
|
||||
this.pointer.onClick = () => node.gotClickInSpot()
|
||||
```
|
||||
|
||||
Full usage can be seen in the old `processMouseDown` handler, which is still in place (several monkey patches in the wild).
|
||||
|
||||
### Registering a click or drag event
|
||||
|
||||
Example usage:
|
||||
|
||||
```typescript
|
||||
const { pointer } = this
|
||||
// Click / double click - executed on pointerup
|
||||
pointer.onClick = e => node.executeClick(e)
|
||||
pointer.onDoubleClick = node.gotDoubleClick
|
||||
|
||||
// Drag events - executed on pointermove
|
||||
pointer.onDragStart = e => {
|
||||
node.isBeingDragged = true
|
||||
canvas.startedDragging(e)
|
||||
}
|
||||
pointer.onDrag = () => {}
|
||||
// finally() is preferred where possible, as it is guaranteed to run
|
||||
pointer.onDragEnd = () => {}
|
||||
|
||||
// Always run, regardless of outcome
|
||||
pointer.finally = () => (node.isBeingDragged = false)
|
||||
```
|
||||
|
||||
## Widgets
|
||||
|
||||
Adds `onPointerDown` callback to node widgets. A few benefits of the new API:
|
||||
|
||||
- Simplified usage
|
||||
- Exposes callbacks like "double click", removing the need to time / measure multiple pointer events
|
||||
- Unified UX - same API as used in the rest of Litegraph
|
||||
- Honours the user's click speed and pointer accuracy settings
|
||||
|
||||
#### Usage
|
||||
|
||||
```ts
|
||||
// Callbacks for each pointer action can be configured ahead of time
|
||||
widget.onPointerDown = function (pointer, node, canvas) {
|
||||
const e = pointer.eDown
|
||||
const offsetFromNode = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]
|
||||
|
||||
// Click events - no overlap with drag events
|
||||
pointer.onClick = upEvent => {
|
||||
// Provides access to the whole lifecycle of events in every callback
|
||||
console.log(pointer.eDown)
|
||||
console.log(pointer.eMove ?? "Pointer didn't move")
|
||||
console.log(pointer.eUp)
|
||||
}
|
||||
pointer.onDoubleClick = upEvent => this.customFunction(upEvent)
|
||||
|
||||
// Runs once before the first onDrag event
|
||||
pointer.onDragStart = () => {}
|
||||
// Receives every movement event
|
||||
pointer.onDrag = moveEvent => {}
|
||||
// The pointerup event of a drag
|
||||
pointer.onDragEnd = upEvent => {}
|
||||
|
||||
// Semantics of a "finally" block (try/catch). Once set, the block always executes.
|
||||
pointer.finally = () => {}
|
||||
|
||||
// Return true to cancel regular Litegraph handling of this click / drag
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
</detail>
|
||||
|
||||
### TypeScript & JSDoc
|
||||
|
||||
In-IDE typing is available for use in at least mainstream editors. TypeScript definitions are available in the litegraph library.
|
||||
|
||||
```ts
|
||||
/** @import { IWidget } from './path/to/litegraph/litegraph.d.ts' */
|
||||
/** @type IWidget */
|
||||
const widget = node.widgets[0]
|
||||
widget.onPointerDown = function (pointer, node, canvas) {}
|
||||
```
|
||||
|
||||
#### VS Code
|
||||
|
||||

|
||||
|
||||
## Hovering over
|
||||
|
||||
Adds API for downstream consumers to handle custom cursors. A bitwise enum of items,
|
||||
|
||||
```typescript
|
||||
type LGraphCanvasState = {
|
||||
/** If `true`, pointer move events will set the canvas cursor style. */
|
||||
shouldSetCursor: boolean,
|
||||
/** Bit flags indicating what is currently below the pointer. */
|
||||
hoveringOver: CanvasItem,
|
||||
...
|
||||
}
|
||||
|
||||
// Disable litegraph cursors
|
||||
canvas.state.shouldSetCursor = false
|
||||
|
||||
// Checking state - bit operators
|
||||
if (canvas.state.hoveringOver & CanvasItem.ResizeSe) element.style.cursor = 'se-resize'
|
||||
```
|
||||
|
||||
</detail>
|
||||
|
||||
# Removed public interfaces
|
||||
|
||||
All are unused and incomplete. Have bugs beyond just typescript typing, and are (currently) not worth maintaining. If any of these features are desired down the track, they can be reimplemented.
|
||||
|
||||
- Live mode
|
||||
- Subgraph
|
||||
- `dragged_node`
|
||||
|
||||
## LiteGraph
|
||||
|
||||
These features have not been maintained, and would require refactoring / rewrites. As code search revealed them to be unused, they are being removed.
|
||||
|
||||
- addNodeMethod
|
||||
- compareObjects
|
||||
- auto_sort_node_types (option)
|
||||
62
src/lib/litegraph/CLAUDE.md
Normal file
62
src/lib/litegraph/CLAUDE.md
Normal file
@@ -0,0 +1,62 @@
|
||||
- This codebase has extensive eslint autofix rules and IDEs are configured to use eslint as the format on save tool. Run ESLint instead of manually figuring out whitespace fixes or other trivial style concerns. Review the results and correct any remaining eslint errors.
|
||||
- Take advantage of `TypedArray` `subarray` when appropriate.
|
||||
- The `size` and `pos` properties of `Rectangle` share the same array buffer (`subarray`); they may be used to set the rectangles size and position.
|
||||
- Prefer single line `if` syntax over adding curly braces, when the statement has a very concise expression and concise, single line statement.
|
||||
- Do not replace `&&=` or `||=` with `=` when there is no reason to do so. If you do find a reason to remove either `&&=` or `||=`, leave a comment explaining why the removal occurred.
|
||||
- You are allowed to research code on https://developer.mozilla.org/ and https://stackoverflow.com without asking.
|
||||
- When adding features, always write vitest unit tests using cursor rules in @.cursor
|
||||
- When writing methods, prefer returning idiomatic JavaScript `undefined` over `null`.
|
||||
|
||||
# Bash commands
|
||||
|
||||
- `npm run typecheck` Run the typechecker
|
||||
- `npm run build` Build the project
|
||||
- `npm run lint:fix` Run ESLint
|
||||
|
||||
# Code style
|
||||
|
||||
- Always prefer best practices when writing code.
|
||||
- Write using concise, legible, and easily maintainable code.
|
||||
- Avoid repetition where possible, but not at the expense of code legibility.
|
||||
- Type assertions are an absolute last resort. In almost all cases, they are a crutch that leads to brittle code.
|
||||
|
||||
# Workflow
|
||||
|
||||
- Be sure to typecheck when you’re done making a series of code changes
|
||||
- Prefer running single tests, and not the whole test suite, for performance
|
||||
|
||||
# Testing Guidelines
|
||||
|
||||
## Avoiding Circular Dependencies in Tests
|
||||
|
||||
**CRITICAL**: When writing tests for subgraph-related code, always import from the barrel export to avoid circular dependency issues:
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use barrel import
|
||||
import { LGraph, Subgraph, SubgraphNode } from "@/litegraph"
|
||||
|
||||
// ❌ WRONG - Direct imports cause circular dependency
|
||||
import { LGraph } from "@/LGraph"
|
||||
import { Subgraph } from "@/subgraph/Subgraph"
|
||||
import { SubgraphNode } from "@/subgraph/SubgraphNode"
|
||||
```
|
||||
|
||||
**Root cause**: `LGraph` and `Subgraph` have a circular dependency:
|
||||
- `LGraph.ts` imports `Subgraph` (creates instances with `new Subgraph()`)
|
||||
- `Subgraph.ts` extends `LGraph`
|
||||
|
||||
The barrel export (`@/litegraph`) handles this properly, but direct imports cause module loading failures.
|
||||
|
||||
## Test Setup for Subgraphs
|
||||
|
||||
Use the provided test helpers for consistent setup:
|
||||
|
||||
```typescript
|
||||
import { createTestSubgraph, createTestSubgraphNode } from "./fixtures/subgraphHelpers"
|
||||
|
||||
function createTestSetup() {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
return { subgraph, subgraphNode }
|
||||
}
|
||||
```
|
||||
9
src/lib/litegraph/CONTRIBUTING.md
Executable file
9
src/lib/litegraph/CONTRIBUTING.md
Executable file
@@ -0,0 +1,9 @@
|
||||
# Contribution Rules
|
||||
There are some simple rules that everyone should follow:
|
||||
|
||||
### Do not commit files from build folder
|
||||
> I usually have horrible merge conflicts when I upload the build version that take me too much time to solve, but I want to keep the build version in the repo, so I guess it would be better if only one of us does the built, which would be me.
|
||||
> https://github.com/jagenjo/litegraph.js/pull/155#issuecomment-656602861
|
||||
Those files will be updated by owner.
|
||||
|
||||
|
||||
19
src/lib/litegraph/LICENSE
Executable file
19
src/lib/litegraph/LICENSE
Executable file
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2013 by Javi Agenjo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
173
src/lib/litegraph/README.md
Executable file
173
src/lib/litegraph/README.md
Executable file
@@ -0,0 +1,173 @@
|
||||
# @ComfyOrg/litegraph
|
||||
|
||||
This is the litegraph version used in [ComfyUI_frontend](https://github.com/Comfy-Org/ComfyUI_frontend).
|
||||
|
||||
It is a fork of the original `litegraph.js`. Some APIs may by unchanged, however it is largely incompatible with the original.
|
||||
|
||||
Some early highlights:
|
||||
|
||||
- Accumulated comfyUI custom changes (2024-01 ~ 2024-05) (https://github.com/Comfy-Org/litegraph.js/pull/1)
|
||||
- Type schema change for ComfyUI_frontend TS migration (https://github.com/Comfy-Org/litegraph.js/pull/3)
|
||||
- Zoom fix (https://github.com/Comfy-Org/litegraph.js/pull/7)
|
||||
- Emit search box triggering custom events (<https://github.com/Comfy-Org/litegraph.js/pull/10>)
|
||||
- Truncate overflowing combo widget text (<https://github.com/Comfy-Org/litegraph.js/pull/17>)
|
||||
- Sort node based on ID on graph serialization (<https://github.com/Comfy-Org/litegraph.js/pull/21>)
|
||||
- Fix empty input not used when connecting links (<https://github.com/Comfy-Org/litegraph.js/pull/24>)
|
||||
- Batch output connection move/disconnect (<https://github.com/Comfy-Org/litegraph.js/pull/39>)
|
||||
- And now with hundreds more...
|
||||
|
||||
# Usage
|
||||
|
||||
This library is included as a git subtree in the ComfyUI frontend project at `src/lib/litegraph`.
|
||||
|
||||
# litegraph.js
|
||||
|
||||
A TypeScript library to create graphs in the browser similar to Unreal Blueprints.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Description of the original litegraph.js</summary>
|
||||
|
||||
A library in Javascript to create graphs in the browser similar to Unreal Blueprints. Nodes can be programmed easily and it includes an editor to construct and tests the graphs.
|
||||
|
||||
It can be integrated easily in any existing web applications and graphs can be run without the need of the editor.
|
||||
|
||||
</details>
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Renders on Canvas2D (zoom in/out and panning, easy to render complex interfaces, can be used inside a WebGLTexture)
|
||||
- Easy to use editor (searchbox, keyboard shortcuts, multiple selection, context menu, ...)
|
||||
- Optimized to support hundreds of nodes per graph (on editor but also on execution)
|
||||
- Customizable theme (colors, shapes, background)
|
||||
- Callbacks to personalize every action/drawing/event of nodes
|
||||
- Graphs can be executed in NodeJS
|
||||
- Highly customizable nodes (color, shape, widgets, custom rendering)
|
||||
- Easy to integrate in any JS application (one single file, no dependencies)
|
||||
- Typescript support
|
||||
|
||||
## Integration
|
||||
|
||||
This library is integrated as a git subtree in the ComfyUI frontend project. To use it in your code:
|
||||
|
||||
```typescript
|
||||
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph'
|
||||
```
|
||||
|
||||
## How to code a new Node type
|
||||
|
||||
Here is an example of how to build a node that sums two inputs:
|
||||
|
||||
```ts
|
||||
import { LiteGraph, LGraphNode } from "./litegraph"
|
||||
|
||||
class MyAddNode extends LGraphNode {
|
||||
// Name to show
|
||||
title = "Sum"
|
||||
|
||||
constructor() {
|
||||
this.addInput("A", "number")
|
||||
this.addInput("B", "number")
|
||||
this.addOutput("A+B", "number")
|
||||
this.properties.precision = 1
|
||||
}
|
||||
|
||||
// Function to call when the node is executed
|
||||
onExecute() {
|
||||
var A = this.getInputData(0)
|
||||
if (A === undefined) A = 0
|
||||
var B = this.getInputData(1)
|
||||
if (B === undefined) B = 0
|
||||
this.setOutputData(0, A + B)
|
||||
}
|
||||
}
|
||||
|
||||
// Register the node type
|
||||
LiteGraph.registerNodeType("basic/sum", MyAddNode)
|
||||
```
|
||||
|
||||
## Server side
|
||||
|
||||
It also works server-side using NodeJS although some nodes do not work in server (audio, graphics, input, etc).
|
||||
|
||||
```ts
|
||||
import { LiteGraph, LGraph } from "./litegraph.js"
|
||||
|
||||
const graph = new LGraph()
|
||||
|
||||
const firstNode = LiteGraph.createNode("basic/sum")
|
||||
graph.add(firstNode)
|
||||
|
||||
const secondNode = LiteGraph.createNode("basic/sum")
|
||||
graph.add(secondNode)
|
||||
|
||||
firstNode.connect(0, secondNode, 1)
|
||||
|
||||
graph.start()
|
||||
```
|
||||
|
||||
## Projects using it
|
||||
|
||||
### [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
|
||||
|
||||

|
||||
|
||||
### Projects using the original litegraph.js
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
### [webglstudio.org](http://webglstudio.org)
|
||||
|
||||

|
||||
|
||||
### [MOI Elephant](http://moiscript.weebly.com/elephant-systegraveme-nodal.html)
|
||||
|
||||

|
||||
|
||||
### Mynodes
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Feedback
|
||||
|
||||
Please [open an issue](https://github.com/Comfy-Org/litegraph.js/issues/) on the GitHub repo.
|
||||
|
||||
# Development
|
||||
|
||||
Litegraph has no runtime dependencies. The build tooling has been tested on Node.JS 20.18.x
|
||||
|
||||
## Releasing
|
||||
|
||||
Use GitHub actions to release normal versions.
|
||||
|
||||
1. Run the `Release a New Version` action, selecting the version incrment type
|
||||
1. Merge the resultion PR
|
||||
1. A GitHub release is automatically published on merge
|
||||
|
||||
### Pre-release
|
||||
|
||||
The action directly translates `Version increment type` to the npm version command. `Pre-release ID (suffix)` is the option for the `--preid` argument.
|
||||
|
||||
e.g. Use `prerelease` increment type to automatically bump the patch version and create a pre-release version. Subsequent runs of prerelease will update the prerelease version only.
|
||||
Use `patch` when ready to remove the pre-release suffix.
|
||||
|
||||
## Contributors
|
||||
|
||||
You can find the [current list of contributors](https://github.com/Comfy-Org/litegraph.js/graphs/contributors) on GitHub.
|
||||
|
||||
### Contributors (pre-fork)
|
||||
|
||||
- atlasan
|
||||
- kriffe
|
||||
- rappestad
|
||||
- InventivetalentDev
|
||||
- NateScarlet
|
||||
- coderofsalvation
|
||||
- ilyabesk
|
||||
- gausszhou
|
||||
BIN
src/lib/litegraph/imgs/elephant.gif
Executable file
BIN
src/lib/litegraph/imgs/elephant.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
BIN
src/lib/litegraph/imgs/mynodes.png
Executable file
BIN
src/lib/litegraph/imgs/mynodes.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
src/lib/litegraph/imgs/node_graph_example.png
Executable file
BIN
src/lib/litegraph/imgs/node_graph_example.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
src/lib/litegraph/imgs/webglstudio.gif
Executable file
BIN
src/lib/litegraph/imgs/webglstudio.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
638
src/lib/litegraph/public/css/litegraph.css
Normal file
638
src/lib/litegraph/public/css/litegraph.css
Normal file
@@ -0,0 +1,638 @@
|
||||
/* this CSS contains only the basic CSS needed to run the app and use it */
|
||||
|
||||
.lgraphcanvas {
|
||||
/*cursor: crosshair;*/
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
outline: none;
|
||||
font-family: Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
.lgraphcanvas * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu {
|
||||
font-family: Tahoma, sans-serif;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
min-width: 100px;
|
||||
color: #aaf;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
background-color: #2e2e2e !important;
|
||||
z-index: 10;
|
||||
max-height: -webkit-fill-available;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Enable scrolling overflow in Firefox */
|
||||
@supports not (max-height: -webkit-fill-available) {
|
||||
.litegraph.litecontextmenu {
|
||||
max-height: 80vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu.dark {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-title img {
|
||||
margin-top: 2px;
|
||||
margin-left: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-entry {
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu .litemenu-entry.submenu {
|
||||
background-color: #2e2e2e !important;
|
||||
}
|
||||
|
||||
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
||||
.litegraph .litemenubar ul {
|
||||
font-family: Tahoma, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.litegraph .litemenubar li {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.litegraph .litemenubar li:hover {
|
||||
background-color: #777;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.litegraph .litegraph .litemenubar-panel {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
min-width: 100px;
|
||||
background-color: #444;
|
||||
box-shadow: 0 0 3px black;
|
||||
padding: 4px;
|
||||
border-bottom: 2px solid #aaf;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry,
|
||||
.litemenu-title {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
padding: 0 0 0 4px;
|
||||
margin: 2px;
|
||||
padding-left: 2px;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry .icon {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 2px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.checked .icon {
|
||||
background-color: #aaf;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry .more {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.separator {
|
||||
display: block;
|
||||
border-top: 1px solid #333;
|
||||
border-bottom: 1px solid #666;
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
margin: 3px 0 2px 0;
|
||||
background-color: transparent;
|
||||
padding: 0 !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.has_submenu {
|
||||
border-right: 2px solid cyan;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-title {
|
||||
color: #dde;
|
||||
background-color: #111;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) {
|
||||
background-color: #444 !important;
|
||||
color: #eee;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry .property_name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
min-width: 80px;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry .property_value {
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
min-height: 1.2em;
|
||||
vertical-align: middle;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.litegraph.litesearchbox {
|
||||
font-family: Tahoma, sans-serif;
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.litegraph.litesearchbox input,
|
||||
.litegraph.litesearchbox select {
|
||||
margin-top: 3px;
|
||||
min-width: 60px;
|
||||
min-height: 1.5em;
|
||||
background-color: black;
|
||||
border: 0;
|
||||
color: white;
|
||||
padding-left: 10px;
|
||||
margin-right: 5px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.litegraph.litesearchbox .name {
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
min-height: 1.5em;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.litegraph.litesearchbox .helper {
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.litegraph.lite-search-item {
|
||||
font-family: Tahoma, sans-serif;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.litegraph.lite-search-item.not_in_filter {
|
||||
/*background-color: rgba(50, 50, 50, 0.5);*/
|
||||
/*color: #999;*/
|
||||
color: #b99;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.litegraph.lite-search-item.generic_type {
|
||||
/*background-color: rgba(50, 50, 50, 0.5);*/
|
||||
/*color: #DD9;*/
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.litegraph.lite-search-item:hover,
|
||||
.litegraph.lite-search-item.selected {
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.litegraph.lite-search-item-type {
|
||||
display: inline-block;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
padding: 2px 5px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
opacity: 0.8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* DIALOGs ******/
|
||||
|
||||
.litegraph .dialog {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -150px;
|
||||
margin-left: -200px;
|
||||
|
||||
background-color: #2a2a2a;
|
||||
|
||||
min-width: 400px;
|
||||
min-height: 200px;
|
||||
box-shadow: 0 0 4px #111;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.litegraph .dialog.settings {
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
height: calc(100% - 20px);
|
||||
margin: auto;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.litegraph .dialog.centered {
|
||||
top: 50px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
min-width: 600px;
|
||||
min-height: 300px;
|
||||
height: calc(100% - 100px);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.litegraph .dialog .close {
|
||||
float: right;
|
||||
margin: 4px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.litegraph .dialog .close:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header {
|
||||
color: #aaa;
|
||||
border-bottom: 1px solid #161616;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header {
|
||||
height: 40px;
|
||||
}
|
||||
.litegraph .dialog .dialog-footer {
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-header .dialog-title {
|
||||
font: 20px "Arial";
|
||||
margin: 4px;
|
||||
padding: 4px 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-content,
|
||||
.litegraph .dialog .dialog-alt-content {
|
||||
height: calc(100% - 90px);
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
/*background-color: black;*/
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-content h3 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-content .connections {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.litegraph .dialog .dialog-content .connections .connections_side {
|
||||
width: calc(50% - 5px);
|
||||
min-height: 100px;
|
||||
background-color: black;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.litegraph .dialog .node_type {
|
||||
font-size: 1.2em;
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .node_desc {
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .separator {
|
||||
display: block;
|
||||
width: calc(100% - 4px);
|
||||
height: 1px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #333;
|
||||
margin: 10px 2px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property {
|
||||
margin-bottom: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property:hover {
|
||||
background: #545454;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property_name {
|
||||
color: #737373;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
width: 160px;
|
||||
padding-left: 4px;
|
||||
overflow: hidden;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property:hover .property_name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property_value {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: #aaa;
|
||||
background-color: #1a1a1a;
|
||||
/*width: calc( 100% - 122px );*/
|
||||
max-width: calc(100% - 162px);
|
||||
min-width: 200px;
|
||||
max-height: 300px;
|
||||
min-height: 20px;
|
||||
padding: 4px;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property_value:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.litegraph .dialog .property.boolean .property_value {
|
||||
padding-right: 30px;
|
||||
color: #a88;
|
||||
/*width: auto;
|
||||
float: right;*/
|
||||
}
|
||||
|
||||
.litegraph .dialog .property.boolean.bool-on .property_name {
|
||||
color: #8a8;
|
||||
}
|
||||
.litegraph .dialog .property.boolean.bool-on .property_value {
|
||||
color: #8a8;
|
||||
}
|
||||
|
||||
.litegraph .dialog .btn {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 20px;
|
||||
margin-left: 0px;
|
||||
background-color: #060606;
|
||||
color: #8e8e8e;
|
||||
}
|
||||
|
||||
.litegraph .dialog .btn:hover {
|
||||
background-color: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.litegraph .dialog .btn.delete:hover {
|
||||
background-color: #f33;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.litegraph .bullet_icon {
|
||||
margin-left: 10px;
|
||||
border-radius: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #666;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
margin-right: 4px;
|
||||
transition: background-color 0.1s ease 0s;
|
||||
-moz-transition: background-color 0.1s ease 0s;
|
||||
}
|
||||
|
||||
.litegraph .bullet_icon:hover {
|
||||
background-color: #698;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* OLD */
|
||||
|
||||
.graphcontextmenu {
|
||||
padding: 4px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.graphcontextmenu-title {
|
||||
color: #dde;
|
||||
background-color: #222;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.graphmenu-entry {
|
||||
box-sizing: border-box;
|
||||
margin: 2px;
|
||||
padding-left: 20px;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
transition: all linear 0.3s;
|
||||
}
|
||||
|
||||
.graphmenu-entry.event,
|
||||
.litemenu-entry.event {
|
||||
border-left: 8px solid orange;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.graphmenu-entry.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.graphmenu-entry.submenu {
|
||||
border-right: 2px solid #eee;
|
||||
}
|
||||
|
||||
.graphmenu-entry:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.graphmenu-entry.separator {
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid #666;
|
||||
height: 1px;
|
||||
width: calc(100% - 20px);
|
||||
-moz-width: calc(100% - 20px);
|
||||
-webkit-width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.graphmenu-entry .property_name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
min-width: 80px;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.graphmenu-entry .property_value,
|
||||
.litemenu-entry .property_value {
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
min-height: 1.2em;
|
||||
vertical-align: middle;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.graphdialog {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
min-height: 2em;
|
||||
background-color: #333;
|
||||
font-size: 1.2em;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.graphdialog.rounded {
|
||||
border-radius: 12px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.graphdialog .name {
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
min-height: 1.5em;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.graphdialog input,
|
||||
.graphdialog textarea,
|
||||
.graphdialog select {
|
||||
margin: 3px;
|
||||
min-width: 60px;
|
||||
min-height: 1.5em;
|
||||
background-color: black;
|
||||
border: 0;
|
||||
color: white;
|
||||
padding-left: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.graphdialog textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.graphdialog button {
|
||||
margin-top: 3px;
|
||||
vertical-align: top;
|
||||
background-color: #999;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.graphdialog button.rounded,
|
||||
.graphdialog input.rounded {
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
.graphdialog .helper {
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.graphdialog .help-item {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.graphdialog .help-item:hover,
|
||||
.graphdialog .help-item.selected {
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.litegraph .dialog {
|
||||
min-height: 0;
|
||||
}
|
||||
.litegraph .dialog .dialog-content {
|
||||
display: block;
|
||||
}
|
||||
.litegraph .graphdialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
padding: 4px 10px;
|
||||
position: fixed;
|
||||
}
|
||||
.litegraph .graphdialog .name {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.litegraph .graphdialog .value {
|
||||
font-size: 16px;
|
||||
min-height: 0;
|
||||
margin: 0 10px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.litegraph .graphdialog input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.litegraph .graphdialog button {
|
||||
padding: 4px 18px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
292
src/lib/litegraph/src/CanvasPointer.ts
Normal file
292
src/lib/litegraph/src/CanvasPointer.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import type { CompassCorners } from './interfaces'
|
||||
import { dist2 } from './measure'
|
||||
import type { CanvasPointerEvent } from './types/events'
|
||||
|
||||
/**
|
||||
* Allows click and drag actions to be declared ahead of time during a pointerdown event.
|
||||
*
|
||||
* By default, it retains the most recent event of each type until it is reset (on pointerup).
|
||||
* - {@link eDown}
|
||||
* - {@link eMove}
|
||||
* - {@link eUp}
|
||||
*
|
||||
* Depending on whether the user clicks or drags the pointer, only the appropriate callbacks are called:
|
||||
* - {@link onClick}
|
||||
* - {@link onDoubleClick}
|
||||
* - {@link onDragStart}
|
||||
* - {@link onDrag}
|
||||
* - {@link onDragEnd}
|
||||
* - {@link finally}
|
||||
* @see
|
||||
* - {@link LGraphCanvas.processMouseDown}
|
||||
* - {@link LGraphCanvas.processMouseMove}
|
||||
* - {@link LGraphCanvas.processMouseUp}
|
||||
*/
|
||||
export class CanvasPointer {
|
||||
/** Maximum time in milliseconds to ignore click drift */
|
||||
static bufferTime = 150
|
||||
|
||||
/** Maximum gap between pointerup and pointerdown events to be considered as a double click */
|
||||
static doubleClickTime = 300
|
||||
|
||||
/** Maximum offset from click location */
|
||||
static get maxClickDrift() {
|
||||
return this.#maxClickDrift
|
||||
}
|
||||
|
||||
static set maxClickDrift(value) {
|
||||
this.#maxClickDrift = value
|
||||
this.#maxClickDrift2 = value * value
|
||||
}
|
||||
|
||||
static #maxClickDrift = 6
|
||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||
|
||||
/** The element this PointerState should capture input against when dragging. */
|
||||
element: Element
|
||||
/** Pointer ID used by drag capture. */
|
||||
pointerId?: number
|
||||
|
||||
/** Set to true when if the pointer moves far enough after a down event, before the corresponding up event is fired. */
|
||||
dragStarted: boolean = false
|
||||
|
||||
/** The {@link eUp} from the last successful click */
|
||||
eLastDown?: CanvasPointerEvent
|
||||
|
||||
/** Used downstream for touch event support. */
|
||||
isDouble: boolean = false
|
||||
/** Used downstream for touch event support. */
|
||||
isDown: boolean = false
|
||||
|
||||
/** The resize handle currently being hovered or dragged */
|
||||
resizeDirection?: CompassCorners
|
||||
|
||||
/**
|
||||
* If `true`, {@link eDown}, {@link eMove}, and {@link eUp} will be set to
|
||||
* `undefined` when {@link reset} is called.
|
||||
*
|
||||
* Default: `true`
|
||||
*/
|
||||
clearEventsOnReset: boolean = true
|
||||
|
||||
/** The last pointerdown event for the primary button */
|
||||
eDown?: CanvasPointerEvent
|
||||
/** The last pointermove event for the primary button */
|
||||
eMove?: CanvasPointerEvent
|
||||
/** The last pointerup event for the primary button */
|
||||
eUp?: CanvasPointerEvent
|
||||
|
||||
/**
|
||||
* If set, as soon as the mouse moves outside the click drift threshold, this action is run once.
|
||||
* @param pointer [DEPRECATED] This parameter will be removed in a future release.
|
||||
* @param eMove The pointermove event of this ongoing drag action.
|
||||
*
|
||||
* It is possible for no `pointermove` events to occur, but still be far from
|
||||
* the original `pointerdown` event. In this case, {@link eMove} will be null, and
|
||||
* {@link onDragEnd} will be called immediately after {@link onDragStart}.
|
||||
*/
|
||||
onDragStart?(pointer: this, eMove?: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Called on pointermove whilst dragging.
|
||||
* @param eMove The pointermove event of this ongoing drag action
|
||||
*/
|
||||
onDrag?(eMove: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Called on pointerup after dragging (i.e. not called if clicked).
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onDragEnd?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onClick?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Callback that will be run once, the next time a pointerup event appears to be a normal click.
|
||||
* @param upEvent The pointerup or pointermove event that triggered this callback
|
||||
*/
|
||||
onDoubleClick?(upEvent: CanvasPointerEvent): unknown
|
||||
|
||||
/**
|
||||
* Run-once callback, called at the end of any click or drag, whether or not it was successful in any way.
|
||||
*
|
||||
* The setter of this callback will call the existing value before replacing it.
|
||||
* Therefore, simply setting this value twice will execute the first callback.
|
||||
*/
|
||||
get finally() {
|
||||
return this.#finally
|
||||
}
|
||||
|
||||
set finally(value) {
|
||||
try {
|
||||
this.#finally?.()
|
||||
} finally {
|
||||
this.#finally = value
|
||||
}
|
||||
}
|
||||
|
||||
#finally?: () => unknown
|
||||
|
||||
constructor(element: Element) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointerdown` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointerdown` event
|
||||
*/
|
||||
down(e: CanvasPointerEvent): void {
|
||||
this.reset()
|
||||
this.eDown = e
|
||||
this.pointerId = e.pointerId
|
||||
this.element.setPointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointermove` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointermove` event
|
||||
*/
|
||||
move(e: CanvasPointerEvent): void {
|
||||
const { eDown } = this
|
||||
if (!eDown) return
|
||||
|
||||
// No buttons down, but eDown exists - clean up & leave
|
||||
if (!e.buttons) {
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Primary button released - treat as pointerup.
|
||||
if (!(e.buttons & eDown.buttons)) {
|
||||
this.#completeClick(e)
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
this.eMove = e
|
||||
this.onDrag?.(e)
|
||||
|
||||
// Dragging, but no callback to run
|
||||
if (this.dragStarted) return
|
||||
|
||||
const longerThanBufferTime =
|
||||
e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
|
||||
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
|
||||
this.#setDragStarted(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `pointerup` events. To be used as the event handler (or called by it).
|
||||
* @param e The `pointerup` event
|
||||
*/
|
||||
up(e: CanvasPointerEvent): boolean {
|
||||
if (e.button !== this.eDown?.button) return false
|
||||
|
||||
this.#completeClick(e)
|
||||
const { dragStarted } = this
|
||||
this.reset()
|
||||
return !dragStarted
|
||||
}
|
||||
|
||||
#completeClick(e: CanvasPointerEvent): void {
|
||||
const { eDown } = this
|
||||
if (!eDown) return
|
||||
|
||||
this.eUp = e
|
||||
|
||||
if (this.dragStarted) {
|
||||
// A move event already started drag
|
||||
this.onDragEnd?.(e)
|
||||
} else if (!this.#hasSamePosition(e, eDown)) {
|
||||
// Teleport without a move event (e.g. tab out, move, tab back)
|
||||
this.#setDragStarted()
|
||||
this.onDragEnd?.(e)
|
||||
} else if (this.onDoubleClick && this.#isDoubleClick()) {
|
||||
// Double-click event
|
||||
this.onDoubleClick(e)
|
||||
this.eLastDown = undefined
|
||||
} else {
|
||||
// Normal click event
|
||||
this.onClick?.(e)
|
||||
this.eLastDown = eDown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two events occurred near each other - not further apart than the maximum click drift.
|
||||
* @param a The first event to compare
|
||||
* @param b The second event to compare
|
||||
* @param tolerance2 The maximum distance (squared) before the positions are considered different
|
||||
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
|
||||
*/
|
||||
#hasSamePosition(
|
||||
a: PointerEvent,
|
||||
b: PointerEvent,
|
||||
tolerance2 = CanvasPointer.#maxClickDrift2
|
||||
): boolean {
|
||||
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
|
||||
return drift <= tolerance2
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the pointer is currently past the max click drift threshold.
|
||||
* @returns `true` if the latest pointer event is past the the click drift threshold
|
||||
*/
|
||||
#isDoubleClick(): boolean {
|
||||
const { eDown, eLastDown } = this
|
||||
if (!eDown || !eLastDown) return false
|
||||
|
||||
// Use thrice the drift distance for double-click gap
|
||||
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
|
||||
const diff = eDown.timeStamp - eLastDown.timeStamp
|
||||
return (
|
||||
diff > 0 &&
|
||||
diff < CanvasPointer.doubleClickTime &&
|
||||
this.#hasSamePosition(eDown, eLastDown, tolerance2)
|
||||
)
|
||||
}
|
||||
|
||||
#setDragStarted(eMove?: CanvasPointerEvent): void {
|
||||
this.dragStarted = true
|
||||
this.onDragStart?.(this, eMove)
|
||||
delete this.onDragStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of this {@link CanvasPointer} instance.
|
||||
*
|
||||
* The {@link finally} callback is first executed, then all callbacks and intra-click
|
||||
* state is cleared.
|
||||
*/
|
||||
reset(): void {
|
||||
// The setter executes the callback before clearing it
|
||||
this.finally = undefined
|
||||
delete this.onClick
|
||||
delete this.onDoubleClick
|
||||
delete this.onDragStart
|
||||
delete this.onDrag
|
||||
delete this.onDragEnd
|
||||
|
||||
this.isDown = false
|
||||
this.isDouble = false
|
||||
this.dragStarted = false
|
||||
this.resizeDirection = undefined
|
||||
|
||||
if (this.clearEventsOnReset) {
|
||||
this.eDown = undefined
|
||||
this.eMove = undefined
|
||||
this.eUp = undefined
|
||||
}
|
||||
|
||||
const { element, pointerId } = this
|
||||
this.pointerId = undefined
|
||||
if (typeof pointerId === 'number' && element.hasPointerCapture(pointerId)) {
|
||||
element.releasePointerCapture(pointerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
411
src/lib/litegraph/src/ContextMenu.ts
Normal file
411
src/lib/litegraph/src/ContextMenu.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import type {
|
||||
ContextMenuDivElement,
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue
|
||||
} from './interfaces'
|
||||
import { LiteGraph } from './litegraph'
|
||||
|
||||
// TODO: Replace this pattern with something more modern.
|
||||
export interface ContextMenu<TValue = unknown> {
|
||||
constructor: new (
|
||||
...args: ConstructorParameters<typeof ContextMenu<TValue>>
|
||||
) => ContextMenu<TValue>
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextMenu from LiteGUI
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export class ContextMenu<TValue = unknown> {
|
||||
options: IContextMenuOptions<TValue>
|
||||
parentMenu?: ContextMenu<TValue>
|
||||
root: ContextMenuDivElement<TValue>
|
||||
current_submenu?: ContextMenu<TValue>
|
||||
lock?: boolean
|
||||
|
||||
controller: AbortController = new AbortController()
|
||||
|
||||
/**
|
||||
* @todo Interface for values requires functionality change - currently accepts
|
||||
* an array of strings, functions, objects, nulls, or undefined.
|
||||
* @param values (allows object { title: "Nice text", callback: function ... })
|
||||
* @param options [optional] Some options:\
|
||||
* - title: title to show on top of the menu
|
||||
* - callback: function to call when an option is clicked, it receives the item information
|
||||
* - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback
|
||||
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
|
||||
*/
|
||||
constructor(
|
||||
values: readonly (string | IContextMenuValue<TValue> | null)[],
|
||||
options: IContextMenuOptions<TValue>
|
||||
) {
|
||||
options ||= {}
|
||||
this.options = options
|
||||
|
||||
// to link a menu with its parent
|
||||
const parent = options.parentMenu
|
||||
if (parent) {
|
||||
if (!(parent instanceof ContextMenu)) {
|
||||
console.error('parentMenu must be of class ContextMenu, ignoring it')
|
||||
options.parentMenu = undefined
|
||||
} else {
|
||||
this.parentMenu = parent
|
||||
this.parentMenu.lock = true
|
||||
this.parentMenu.current_submenu = this
|
||||
}
|
||||
if (parent.options?.className === 'dark') {
|
||||
options.className = 'dark'
|
||||
}
|
||||
}
|
||||
|
||||
// use strings because comparing classes between windows doesnt work
|
||||
const eventClass = options.event ? options.event.constructor.name : null
|
||||
if (
|
||||
eventClass !== 'MouseEvent' &&
|
||||
eventClass !== 'CustomEvent' &&
|
||||
eventClass !== 'PointerEvent'
|
||||
) {
|
||||
console.error(
|
||||
`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`
|
||||
)
|
||||
options.event = undefined
|
||||
}
|
||||
|
||||
const root: ContextMenuDivElement<TValue> = document.createElement('div')
|
||||
let classes = 'litegraph litecontextmenu litemenubar-panel'
|
||||
if (options.className) classes += ` ${options.className}`
|
||||
root.className = classes
|
||||
root.style.minWidth = '100'
|
||||
root.style.minHeight = '100'
|
||||
|
||||
// Close the context menu when a click occurs outside this context menu or its submenus
|
||||
const { signal } = this.controller
|
||||
const eventOptions = { capture: true, signal }
|
||||
|
||||
if (!this.parentMenu) {
|
||||
document.addEventListener(
|
||||
'pointerdown',
|
||||
(e) => {
|
||||
if (e.target instanceof Node && !this.containsNode(e.target)) {
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
eventOptions
|
||||
)
|
||||
}
|
||||
|
||||
// this prevents the default context browser menu to open in case this menu was created when pressing right button
|
||||
root.addEventListener('pointerup', (e) => e.preventDefault(), eventOptions)
|
||||
|
||||
// Right button
|
||||
root.addEventListener(
|
||||
'contextmenu',
|
||||
(e) => {
|
||||
if (e.button === 2) e.preventDefault()
|
||||
},
|
||||
eventOptions
|
||||
)
|
||||
|
||||
root.addEventListener(
|
||||
'pointerdown',
|
||||
(e) => {
|
||||
if (e.button == 2) {
|
||||
this.close()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
eventOptions
|
||||
)
|
||||
|
||||
this.root = root
|
||||
|
||||
// title
|
||||
if (options.title) {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'litemenu-title'
|
||||
element.innerHTML = options.title
|
||||
root.append(element)
|
||||
}
|
||||
|
||||
// entries
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i]
|
||||
let name = Array.isArray(values) ? value : String(i)
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
name =
|
||||
name != null
|
||||
? name.content === undefined
|
||||
? String(name)
|
||||
: name.content
|
||||
: name
|
||||
}
|
||||
|
||||
this.addItem(name, value, options)
|
||||
}
|
||||
|
||||
// insert before checking position
|
||||
const ownerDocument = (options.event?.target as Node | null | undefined)
|
||||
?.ownerDocument
|
||||
const root_document = ownerDocument || document
|
||||
|
||||
if (root_document.fullscreenElement)
|
||||
root_document.fullscreenElement.append(root)
|
||||
else root_document.body.append(root)
|
||||
|
||||
// compute best position
|
||||
let left = options.left || 0
|
||||
let top = options.top || 0
|
||||
if (options.event) {
|
||||
left = options.event.clientX - 10
|
||||
top = options.event.clientY - 10
|
||||
if (options.title) top -= 20
|
||||
|
||||
if (parent) {
|
||||
const rect = parent.root.getBoundingClientRect()
|
||||
left = rect.left + rect.width
|
||||
}
|
||||
|
||||
const body_rect = document.body.getBoundingClientRect()
|
||||
const root_rect = root.getBoundingClientRect()
|
||||
if (body_rect.height == 0)
|
||||
console.error(
|
||||
'document.body height is 0. That is dangerous, set html,body { height: 100%; }'
|
||||
)
|
||||
|
||||
if (body_rect.width && left > body_rect.width - root_rect.width - 10)
|
||||
left = body_rect.width - root_rect.width - 10
|
||||
if (body_rect.height && top > body_rect.height - root_rect.height - 10)
|
||||
top = body_rect.height - root_rect.height - 10
|
||||
}
|
||||
|
||||
root.style.left = `${left}px`
|
||||
root.style.top = `${top}px`
|
||||
|
||||
if (LiteGraph.context_menu_scaling && options.scale) {
|
||||
root.style.transform = `scale(${Math.round(options.scale * 4) * 0.25})`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if {@link node} is inside this context menu or any of its submenus
|
||||
* @param node The {@link Node} to check
|
||||
* @param visited A set of visited menus to avoid circular references
|
||||
* @returns `true` if {@link node} is inside this context menu or any of its submenus
|
||||
*/
|
||||
containsNode(node: Node, visited: Set<this> = new Set()): boolean {
|
||||
if (visited.has(this)) return false
|
||||
visited.add(this)
|
||||
|
||||
return (
|
||||
this.current_submenu?.containsNode(node, visited) ||
|
||||
this.root.contains(node)
|
||||
)
|
||||
}
|
||||
|
||||
addItem(
|
||||
name: string | null,
|
||||
value: string | IContextMenuValue<TValue> | null,
|
||||
options: IContextMenuOptions<TValue>
|
||||
): HTMLElement {
|
||||
options ||= {}
|
||||
|
||||
const element: ContextMenuDivElement<TValue> = document.createElement('div')
|
||||
element.className = 'litemenu-entry submenu'
|
||||
|
||||
let disabled = false
|
||||
|
||||
if (value === null) {
|
||||
element.classList.add('separator')
|
||||
} else {
|
||||
const innerHtml = name === null ? '' : String(name)
|
||||
if (typeof value === 'string') {
|
||||
element.innerHTML = innerHtml
|
||||
} else {
|
||||
element.innerHTML = value?.title ?? innerHtml
|
||||
|
||||
if (value.disabled) {
|
||||
disabled = true
|
||||
element.classList.add('disabled')
|
||||
element.setAttribute('aria-disabled', 'true')
|
||||
}
|
||||
if (value.submenu || value.has_submenu) {
|
||||
element.classList.add('has_submenu')
|
||||
element.setAttribute('aria-haspopup', 'true')
|
||||
element.setAttribute('aria-expanded', 'false')
|
||||
}
|
||||
if (value.className) element.className += ` ${value.className}`
|
||||
}
|
||||
element.value = value
|
||||
element.setAttribute('role', 'menuitem')
|
||||
|
||||
if (typeof value === 'function') {
|
||||
element.dataset['value'] = String(name)
|
||||
element.onclick_callback = value
|
||||
} else {
|
||||
element.dataset['value'] = String(value)
|
||||
}
|
||||
}
|
||||
|
||||
this.root.append(element)
|
||||
if (!disabled) element.addEventListener('click', inner_onclick)
|
||||
if (!disabled && options.autoopen)
|
||||
element.addEventListener('pointerenter', inner_over)
|
||||
|
||||
const setAriaExpanded = () => {
|
||||
const entries = this.root.querySelectorAll(
|
||||
'div.litemenu-entry.has_submenu'
|
||||
)
|
||||
if (entries) {
|
||||
for (const entry of entries) {
|
||||
entry.setAttribute('aria-expanded', 'false')
|
||||
}
|
||||
}
|
||||
element.setAttribute('aria-expanded', 'true')
|
||||
}
|
||||
|
||||
function inner_over(this: ContextMenuDivElement<TValue>, e: MouseEvent) {
|
||||
const value = this.value
|
||||
if (!value || !(value as IContextMenuValue).has_submenu) return
|
||||
|
||||
// if it is a submenu, autoopen like the item was clicked
|
||||
inner_onclick.call(this, e)
|
||||
setAriaExpanded()
|
||||
}
|
||||
|
||||
// menu option clicked
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const that = this
|
||||
function inner_onclick(this: ContextMenuDivElement<TValue>, e: MouseEvent) {
|
||||
const value = this.value
|
||||
let close_parent = true
|
||||
|
||||
that.current_submenu?.close(e)
|
||||
if (
|
||||
(value as IContextMenuValue)?.has_submenu ||
|
||||
(value as IContextMenuValue)?.submenu
|
||||
) {
|
||||
setAriaExpanded()
|
||||
}
|
||||
|
||||
// global callback
|
||||
if (options.callback) {
|
||||
const r = options.callback.call(
|
||||
this,
|
||||
value,
|
||||
options,
|
||||
e,
|
||||
that,
|
||||
options.node
|
||||
)
|
||||
if (r === true) close_parent = false
|
||||
}
|
||||
|
||||
// special cases
|
||||
if (typeof value === 'object') {
|
||||
if (
|
||||
value.callback &&
|
||||
!options.ignore_item_callbacks &&
|
||||
value.disabled !== true
|
||||
) {
|
||||
// item callback
|
||||
const r = value.callback.call(
|
||||
this,
|
||||
value,
|
||||
options,
|
||||
e,
|
||||
that,
|
||||
options.extra
|
||||
)
|
||||
if (r === true) close_parent = false
|
||||
}
|
||||
if (value.submenu) {
|
||||
if (!value.submenu.options) throw 'ContextMenu submenu needs options'
|
||||
|
||||
new that.constructor(value.submenu.options, {
|
||||
callback: value.submenu.callback,
|
||||
event: e,
|
||||
parentMenu: that,
|
||||
ignore_item_callbacks: value.submenu.ignore_item_callbacks,
|
||||
title: value.submenu.title,
|
||||
extra: value.submenu.extra,
|
||||
autoopen: options.autoopen
|
||||
})
|
||||
close_parent = false
|
||||
}
|
||||
}
|
||||
|
||||
if (close_parent && !that.lock) that.close()
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
close(e?: MouseEvent, ignore_parent_menu?: boolean): void {
|
||||
this.controller.abort()
|
||||
this.root.remove()
|
||||
if (this.parentMenu && !ignore_parent_menu) {
|
||||
this.parentMenu.lock = false
|
||||
this.parentMenu.current_submenu = undefined
|
||||
if (e === undefined) {
|
||||
this.parentMenu.close()
|
||||
} else if (
|
||||
e &&
|
||||
!ContextMenu.isCursorOverElement(e, this.parentMenu.root)
|
||||
) {
|
||||
ContextMenu.trigger(
|
||||
this.parentMenu.root,
|
||||
`${LiteGraph.pointerevents_method}leave`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
this.current_submenu?.close(e, true)
|
||||
}
|
||||
|
||||
/** @deprecated Likely unused, however code search was inconclusive (too many results to check by hand). */
|
||||
// this code is used to trigger events easily (used in the context menu mouseleave
|
||||
static trigger(
|
||||
element: HTMLDivElement,
|
||||
event_name: string,
|
||||
params: MouseEvent
|
||||
): CustomEvent {
|
||||
const evt = document.createEvent('CustomEvent')
|
||||
evt.initCustomEvent(event_name, true, true, params)
|
||||
if (element.dispatchEvent) element.dispatchEvent(evt)
|
||||
// else nothing seems bound here so nothing to do
|
||||
return evt
|
||||
}
|
||||
|
||||
// returns the top most menu
|
||||
getTopMenu(): ContextMenu<TValue> {
|
||||
return this.options.parentMenu ? this.options.parentMenu.getTopMenu() : this
|
||||
}
|
||||
|
||||
getFirstEvent(): MouseEvent | undefined {
|
||||
return this.options.parentMenu
|
||||
? this.options.parentMenu.getFirstEvent()
|
||||
: this.options.event
|
||||
}
|
||||
|
||||
/** @deprecated Unused. */
|
||||
static isCursorOverElement(
|
||||
event: MouseEvent,
|
||||
element: HTMLDivElement
|
||||
): boolean {
|
||||
const left = event.clientX
|
||||
const top = event.clientY
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (!rect) return false
|
||||
|
||||
if (
|
||||
top > rect.top &&
|
||||
top < rect.top + rect.height &&
|
||||
left > rect.left &&
|
||||
left < rect.left + rect.width
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
197
src/lib/litegraph/src/CurveEditor.ts
Normal file
197
src/lib/litegraph/src/CurveEditor.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { Point, Rect } from './interfaces'
|
||||
import { LGraphCanvas, clamp } from './litegraph'
|
||||
import { distance } from './measure'
|
||||
|
||||
// used by some widgets to render a curve editor
|
||||
|
||||
export class CurveEditor {
|
||||
points: Point[]
|
||||
selected: number
|
||||
nearest: number
|
||||
size: Rect | null
|
||||
must_update: boolean
|
||||
margin: number
|
||||
_nearest?: number
|
||||
|
||||
constructor(points: Point[]) {
|
||||
this.points = points
|
||||
this.selected = -1
|
||||
this.nearest = -1
|
||||
// stores last size used
|
||||
this.size = null
|
||||
this.must_update = true
|
||||
this.margin = 5
|
||||
}
|
||||
|
||||
static sampleCurve(f: number, points: Point[]): number | undefined {
|
||||
if (!points) return
|
||||
|
||||
for (let i = 0; i < points.length - 1; ++i) {
|
||||
const p = points[i]
|
||||
const pn = points[i + 1]
|
||||
if (pn[0] < f) continue
|
||||
|
||||
const r = pn[0] - p[0]
|
||||
if (Math.abs(r) < 0.000_01) return p[1]
|
||||
|
||||
const local_f = (f - p[0]) / r
|
||||
return p[1] * (1.0 - local_f) + pn[1] * local_f
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
size: Rect,
|
||||
// @ts-expect-error - LGraphCanvas parameter type needs fixing
|
||||
graphcanvas?: LGraphCanvas,
|
||||
background_color?: string,
|
||||
line_color?: string,
|
||||
inactive = false
|
||||
): void {
|
||||
const points = this.points
|
||||
if (!points) return
|
||||
|
||||
this.size = size
|
||||
const w = size[0] - this.margin * 2
|
||||
const h = size[1] - this.margin * 2
|
||||
|
||||
line_color = line_color || '#666'
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(this.margin, this.margin)
|
||||
|
||||
if (background_color) {
|
||||
ctx.fillStyle = '#111'
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
ctx.fillStyle = '#222'
|
||||
ctx.fillRect(w * 0.5, 0, 1, h)
|
||||
ctx.strokeStyle = '#333'
|
||||
ctx.strokeRect(0, 0, w, h)
|
||||
}
|
||||
ctx.strokeStyle = line_color
|
||||
if (inactive) ctx.globalAlpha = 0.5
|
||||
ctx.beginPath()
|
||||
for (const p of points) {
|
||||
ctx.lineTo(p[0] * w, (1.0 - p[1]) * h)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
if (!inactive) {
|
||||
for (const [i, p] of points.entries()) {
|
||||
ctx.fillStyle =
|
||||
this.selected == i ? '#FFF' : this.nearest == i ? '#DDD' : '#AAA'
|
||||
ctx.beginPath()
|
||||
ctx.arc(p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// localpos is mouse in curve editor space
|
||||
onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean | undefined {
|
||||
const points = this.points
|
||||
if (!points) return
|
||||
if (localpos[1] < 0) return
|
||||
|
||||
// this.captureInput(true);
|
||||
if (this.size == null)
|
||||
throw new Error('CurveEditor.size was null or undefined.')
|
||||
const w = this.size[0] - this.margin * 2
|
||||
const h = this.size[1] - this.margin * 2
|
||||
const x = localpos[0] - this.margin
|
||||
const y = localpos[1] - this.margin
|
||||
const pos: Point = [x, y]
|
||||
const max_dist = 30 / graphcanvas.ds.scale
|
||||
// search closer one
|
||||
this.selected = this.getCloserPoint(pos, max_dist)
|
||||
// create one
|
||||
if (this.selected == -1) {
|
||||
const point: Point = [x / w, 1 - y / h]
|
||||
points.push(point)
|
||||
points.sort(function (a, b) {
|
||||
return a[0] - b[0]
|
||||
})
|
||||
this.selected = points.indexOf(point)
|
||||
this.must_update = true
|
||||
}
|
||||
if (this.selected != -1) return true
|
||||
}
|
||||
|
||||
onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void {
|
||||
const points = this.points
|
||||
if (!points) return
|
||||
|
||||
const s = this.selected
|
||||
if (s < 0) return
|
||||
|
||||
if (this.size == null)
|
||||
throw new Error('CurveEditor.size was null or undefined.')
|
||||
const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2)
|
||||
const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2)
|
||||
const curvepos: Point = [
|
||||
localpos[0] - this.margin,
|
||||
localpos[1] - this.margin
|
||||
]
|
||||
const max_dist = 30 / graphcanvas.ds.scale
|
||||
this._nearest = this.getCloserPoint(curvepos, max_dist)
|
||||
const point = points[s]
|
||||
if (point) {
|
||||
const is_edge_point = s == 0 || s == points.length - 1
|
||||
if (
|
||||
!is_edge_point &&
|
||||
(localpos[0] < -10 ||
|
||||
localpos[0] > this.size[0] + 10 ||
|
||||
localpos[1] < -10 ||
|
||||
localpos[1] > this.size[1] + 10)
|
||||
) {
|
||||
points.splice(s, 1)
|
||||
this.selected = -1
|
||||
return
|
||||
}
|
||||
// not edges
|
||||
if (!is_edge_point) point[0] = clamp(x, 0, 1)
|
||||
else point[0] = s == 0 ? 0 : 1
|
||||
point[1] = 1.0 - clamp(y, 0, 1)
|
||||
points.sort(function (a, b) {
|
||||
return a[0] - b[0]
|
||||
})
|
||||
this.selected = points.indexOf(point)
|
||||
this.must_update = true
|
||||
}
|
||||
}
|
||||
|
||||
// Former params: localpos, graphcanvas
|
||||
onMouseUp(): boolean {
|
||||
this.selected = -1
|
||||
return false
|
||||
}
|
||||
|
||||
getCloserPoint(pos: Point, max_dist: number): number {
|
||||
const points = this.points
|
||||
if (!points) return -1
|
||||
|
||||
max_dist = max_dist || 30
|
||||
if (this.size == null)
|
||||
throw new Error('CurveEditor.size was null or undefined.')
|
||||
const w = this.size[0] - this.margin * 2
|
||||
const h = this.size[1] - this.margin * 2
|
||||
const num = points.length
|
||||
const p2: Point = [0, 0]
|
||||
let min_dist = 1_000_000
|
||||
let closest = -1
|
||||
|
||||
for (let i = 0; i < num; ++i) {
|
||||
const p = points[i]
|
||||
p2[0] = p[0] * w
|
||||
p2[1] = (1.0 - p[1]) * h
|
||||
const dist = distance(pos, p2)
|
||||
if (dist > min_dist || dist > max_dist) continue
|
||||
|
||||
closest = i
|
||||
min_dist = dist
|
||||
}
|
||||
return closest
|
||||
}
|
||||
}
|
||||
316
src/lib/litegraph/src/DragAndScale.ts
Normal file
316
src/lib/litegraph/src/DragAndScale.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import type { Point, ReadOnlyRect, Rect } from './interfaces'
|
||||
import { EaseFunction, Rectangle } from './litegraph'
|
||||
|
||||
export interface DragAndScaleState {
|
||||
/**
|
||||
* The offset from the top-left of the current canvas viewport to `[0, 0]` in graph space.
|
||||
* Or said another way, the inverse offset of the viewport.
|
||||
*/
|
||||
offset: [number, number]
|
||||
/** The scale of the graph. */
|
||||
scale: number
|
||||
}
|
||||
|
||||
export type AnimationOptions = {
|
||||
/** Duration of the animation in milliseconds. */
|
||||
duration?: number
|
||||
/** Relative target zoom level. 1 means the view is fit exactly on the bounding box. */
|
||||
zoom?: number
|
||||
/** The animation easing function (curve) */
|
||||
easing?: EaseFunction
|
||||
}
|
||||
|
||||
export class DragAndScale {
|
||||
/**
|
||||
* The state of this DragAndScale instance.
|
||||
*
|
||||
* Implemented as a POCO that can be proxied without side-effects.
|
||||
*/
|
||||
state: DragAndScaleState
|
||||
lastState: DragAndScaleState = {
|
||||
offset: [0, 0],
|
||||
scale: 0
|
||||
}
|
||||
|
||||
/** Maximum scale (zoom in) */
|
||||
max_scale: number
|
||||
/** Minimum scale (zoom out) */
|
||||
min_scale: number
|
||||
enabled: boolean
|
||||
last_mouse: Point
|
||||
element: HTMLCanvasElement
|
||||
visible_area: Rectangle
|
||||
dragging?: boolean
|
||||
viewport?: Rect
|
||||
|
||||
onredraw?(das: DragAndScale): void
|
||||
onChanged?(scale: number, offset: Point): void
|
||||
|
||||
get offset(): [number, number] {
|
||||
return this.state.offset
|
||||
}
|
||||
|
||||
set offset(value: Point) {
|
||||
this.state.offset[0] = value[0]
|
||||
this.state.offset[1] = value[1]
|
||||
}
|
||||
|
||||
get scale(): number {
|
||||
return this.state.scale
|
||||
}
|
||||
|
||||
set scale(value: number) {
|
||||
this.state.scale = value
|
||||
}
|
||||
|
||||
constructor(element: HTMLCanvasElement) {
|
||||
this.state = {
|
||||
offset: [0, 0],
|
||||
scale: 1
|
||||
}
|
||||
this.max_scale = 10
|
||||
this.min_scale = 0.1
|
||||
this.enabled = true
|
||||
this.last_mouse = [0, 0]
|
||||
this.visible_area = new Rectangle()
|
||||
|
||||
this.element = element
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the current state has changed from the previous state.
|
||||
* @returns `true` if the current state has changed from the previous state, otherwise `false`.
|
||||
*/
|
||||
#stateHasChanged(): boolean {
|
||||
const current = this.state
|
||||
const previous = this.lastState
|
||||
|
||||
return (
|
||||
current.scale !== previous.scale ||
|
||||
current.offset[0] !== previous.offset[0] ||
|
||||
current.offset[1] !== previous.offset[1]
|
||||
)
|
||||
}
|
||||
|
||||
computeVisibleArea(viewport: Rect | undefined): void {
|
||||
const { scale, offset, visible_area } = this
|
||||
|
||||
if (this.#stateHasChanged()) {
|
||||
this.onChanged?.(scale, offset)
|
||||
copyState(this.state, this.lastState)
|
||||
}
|
||||
|
||||
if (!this.element) {
|
||||
visible_area[0] = visible_area[1] = visible_area[2] = visible_area[3] = 0
|
||||
return
|
||||
}
|
||||
let { width, height } = this.element
|
||||
let startx = -offset[0]
|
||||
let starty = -offset[1]
|
||||
if (viewport) {
|
||||
startx += viewport[0] / scale
|
||||
starty += viewport[1] / scale
|
||||
width = viewport[2]
|
||||
height = viewport[3]
|
||||
}
|
||||
const endx = startx + width / scale
|
||||
const endy = starty + height / scale
|
||||
visible_area[0] = startx
|
||||
visible_area[1] = starty
|
||||
visible_area.resizeBottomRight(endx, endy)
|
||||
}
|
||||
|
||||
toCanvasContext(ctx: CanvasRenderingContext2D): void {
|
||||
ctx.scale(this.scale, this.scale)
|
||||
ctx.translate(this.offset[0], this.offset[1])
|
||||
}
|
||||
|
||||
convertOffsetToCanvas(pos: Point): Point {
|
||||
return [
|
||||
(pos[0] + this.offset[0]) * this.scale,
|
||||
(pos[1] + this.offset[1]) * this.scale
|
||||
]
|
||||
}
|
||||
|
||||
convertCanvasToOffset(pos: Point, out?: Point): Point {
|
||||
out = out || [0, 0]
|
||||
out[0] = pos[0] / this.scale - this.offset[0]
|
||||
out[1] = pos[1] / this.scale - this.offset[1]
|
||||
return out
|
||||
}
|
||||
|
||||
/** @deprecated Has not been kept up to date */
|
||||
mouseDrag(x: number, y: number): void {
|
||||
this.offset[0] += x / this.scale
|
||||
this.offset[1] += y / this.scale
|
||||
|
||||
this.onredraw?.(this)
|
||||
}
|
||||
|
||||
changeScale(
|
||||
value: number,
|
||||
zooming_center?: Point,
|
||||
roundToScaleOne = true
|
||||
): void {
|
||||
if (value < this.min_scale) {
|
||||
value = this.min_scale
|
||||
} else if (value > this.max_scale) {
|
||||
value = this.max_scale
|
||||
}
|
||||
if (value == this.scale) return
|
||||
|
||||
const rect = this.element.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
|
||||
zooming_center = zooming_center ?? [rect.width * 0.5, rect.height * 0.5]
|
||||
|
||||
const normalizedCenter: Point = [
|
||||
zooming_center[0] - rect.x,
|
||||
zooming_center[1] - rect.y
|
||||
]
|
||||
const center = this.convertCanvasToOffset(normalizedCenter)
|
||||
this.scale = value
|
||||
if (roundToScaleOne && Math.abs(this.scale - 1) < 0.01) this.scale = 1
|
||||
const new_center = this.convertCanvasToOffset(normalizedCenter)
|
||||
const delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]
|
||||
|
||||
this.offset[0] += delta_offset[0]
|
||||
this.offset[1] += delta_offset[1]
|
||||
|
||||
this.onredraw?.(this)
|
||||
}
|
||||
|
||||
changeDeltaScale(value: number, zooming_center?: Point): void {
|
||||
this.changeScale(this.scale * value, zooming_center)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fits the view to the specified bounds.
|
||||
* @param bounds The bounds to fit the view to, defined by a rectangle.
|
||||
*/
|
||||
fitToBounds(
|
||||
bounds: ReadOnlyRect,
|
||||
{ zoom = 0.75 }: { zoom?: number } = {}
|
||||
): void {
|
||||
const cw = this.element.width / window.devicePixelRatio
|
||||
const ch = this.element.height / window.devicePixelRatio
|
||||
let targetScale = this.scale
|
||||
|
||||
if (zoom > 0) {
|
||||
const targetScaleX = (zoom * cw) / Math.max(bounds[2], 300)
|
||||
const targetScaleY = (zoom * ch) / Math.max(bounds[3], 300)
|
||||
|
||||
// Choose the smaller scale to ensure the node fits into the viewport
|
||||
// Ensure we don't go over the max scale
|
||||
targetScale = Math.min(targetScaleX, targetScaleY, this.max_scale)
|
||||
}
|
||||
|
||||
const scaledWidth = cw / targetScale
|
||||
const scaledHeight = ch / targetScale
|
||||
|
||||
// Calculate the target position to center the bounds in the viewport
|
||||
const targetX = -bounds[0] - bounds[2] * 0.5 + scaledWidth * 0.5
|
||||
const targetY = -bounds[1] - bounds[3] * 0.5 + scaledHeight * 0.5
|
||||
|
||||
// Apply the changes immediately
|
||||
this.offset[0] = targetX
|
||||
this.offset[1] = targetY
|
||||
this.scale = targetScale
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an animation to fit the view around the specified selection of nodes.
|
||||
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
||||
*/
|
||||
animateToBounds(
|
||||
bounds: ReadOnlyRect,
|
||||
setDirty: () => void,
|
||||
{
|
||||
duration = 350,
|
||||
zoom = 0.75,
|
||||
easing = EaseFunction.EASE_IN_OUT_QUAD
|
||||
}: AnimationOptions = {}
|
||||
) {
|
||||
if (!(duration > 0)) throw new RangeError('Duration must be greater than 0')
|
||||
|
||||
const easeFunctions = {
|
||||
linear: (t: number) => t,
|
||||
easeInQuad: (t: number) => t * t,
|
||||
easeOutQuad: (t: number) => t * (2 - t),
|
||||
easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
||||
}
|
||||
const easeFunction = easeFunctions[easing] ?? easeFunctions.linear
|
||||
|
||||
const startTimestamp = performance.now()
|
||||
const cw = this.element.width / window.devicePixelRatio
|
||||
const ch = this.element.height / window.devicePixelRatio
|
||||
const startX = this.offset[0]
|
||||
const startY = this.offset[1]
|
||||
const startX2 = startX - cw / this.scale
|
||||
const startY2 = startY - ch / this.scale
|
||||
const startScale = this.scale
|
||||
let targetScale = startScale
|
||||
|
||||
if (zoom > 0) {
|
||||
const targetScaleX = (zoom * cw) / Math.max(bounds[2], 300)
|
||||
const targetScaleY = (zoom * ch) / Math.max(bounds[3], 300)
|
||||
|
||||
// Choose the smaller scale to ensure the node fits into the viewport
|
||||
// Ensure we don't go over the max scale
|
||||
targetScale = Math.min(targetScaleX, targetScaleY, this.max_scale)
|
||||
}
|
||||
const scaledWidth = cw / targetScale
|
||||
const scaledHeight = ch / targetScale
|
||||
|
||||
const targetX = -bounds[0] - bounds[2] * 0.5 + scaledWidth * 0.5
|
||||
const targetY = -bounds[1] - bounds[3] * 0.5 + scaledHeight * 0.5
|
||||
const targetX2 = targetX - scaledWidth
|
||||
const targetY2 = targetY - scaledHeight
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
const elapsed = timestamp - startTimestamp
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeFunction(progress)
|
||||
|
||||
const currentX = startX + (targetX - startX) * easedProgress
|
||||
const currentY = startY + (targetY - startY) * easedProgress
|
||||
this.offset[0] = currentX
|
||||
this.offset[1] = currentY
|
||||
|
||||
if (zoom > 0) {
|
||||
const currentX2 = startX2 + (targetX2 - startX2) * easedProgress
|
||||
const currentY2 = startY2 + (targetY2 - startY2) * easedProgress
|
||||
const currentWidth = Math.abs(currentX2 - currentX)
|
||||
const currentHeight = Math.abs(currentY2 - currentY)
|
||||
|
||||
this.scale = Math.min(cw / currentWidth, ch / currentHeight)
|
||||
}
|
||||
|
||||
setDirty()
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
} else {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
}
|
||||
let animationId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.scale = 1
|
||||
this.offset[0] = 0
|
||||
this.offset[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the values of one state into another, preserving references.
|
||||
* @param from The state to copy values from.
|
||||
* @param to The state to copy values into.
|
||||
*/
|
||||
function copyState(from: DragAndScaleState, to: DragAndScaleState): void {
|
||||
to.scale = from.scale
|
||||
to.offset[0] = from.offset[0]
|
||||
to.offset[1] = from.offset[1]
|
||||
}
|
||||
2333
src/lib/litegraph/src/LGraph.ts
Normal file
2333
src/lib/litegraph/src/LGraph.ts
Normal file
File diff suppressed because it is too large
Load Diff
122
src/lib/litegraph/src/LGraphBadge.ts
Normal file
122
src/lib/litegraph/src/LGraphBadge.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { LGraphIcon, type LGraphIconOptions } from './LGraphIcon'
|
||||
|
||||
export enum BadgePosition {
|
||||
TopLeft = 'top-left',
|
||||
TopRight = 'top-right'
|
||||
}
|
||||
|
||||
export interface LGraphBadgeOptions {
|
||||
text: string
|
||||
fgColor?: string
|
||||
bgColor?: string
|
||||
fontSize?: number
|
||||
padding?: number
|
||||
height?: number
|
||||
cornerRadius?: number
|
||||
iconOptions?: LGraphIconOptions
|
||||
xOffset?: number
|
||||
yOffset?: number
|
||||
}
|
||||
|
||||
export class LGraphBadge {
|
||||
text: string
|
||||
fgColor: string
|
||||
bgColor: string
|
||||
fontSize: number
|
||||
padding: number
|
||||
height: number
|
||||
cornerRadius: number
|
||||
icon?: LGraphIcon
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
|
||||
constructor({
|
||||
text,
|
||||
fgColor = 'white',
|
||||
bgColor = '#0F1F0F',
|
||||
fontSize = 12,
|
||||
padding = 6,
|
||||
height = 20,
|
||||
cornerRadius = 5,
|
||||
iconOptions,
|
||||
xOffset = 0,
|
||||
yOffset = 0
|
||||
}: LGraphBadgeOptions) {
|
||||
this.text = text
|
||||
this.fgColor = fgColor
|
||||
this.bgColor = bgColor
|
||||
this.fontSize = fontSize
|
||||
this.padding = padding
|
||||
this.height = height
|
||||
this.cornerRadius = cornerRadius
|
||||
if (iconOptions) {
|
||||
this.icon = new LGraphIcon(iconOptions)
|
||||
}
|
||||
this.xOffset = xOffset
|
||||
this.yOffset = yOffset
|
||||
}
|
||||
|
||||
get visible() {
|
||||
return (this.text?.length ?? 0) > 0 || !!this.icon
|
||||
}
|
||||
|
||||
getWidth(ctx: CanvasRenderingContext2D) {
|
||||
if (!this.visible) return 0
|
||||
const { font } = ctx
|
||||
let iconWidth = 0
|
||||
if (this.icon) {
|
||||
ctx.font = `${this.icon.fontSize}px '${this.icon.fontFamily}'`
|
||||
iconWidth = ctx.measureText(this.icon.unicode).width + this.padding
|
||||
}
|
||||
ctx.font = `${this.fontSize}px sans-serif`
|
||||
const textWidth = this.text ? ctx.measureText(this.text).width : 0
|
||||
ctx.font = font
|
||||
return iconWidth + textWidth + this.padding * 2
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D, x: number, y: number): void {
|
||||
if (!this.visible) return
|
||||
|
||||
x += this.xOffset
|
||||
y += this.yOffset
|
||||
|
||||
const { font, fillStyle, textBaseline, textAlign } = ctx
|
||||
|
||||
ctx.font = `${this.fontSize}px sans-serif`
|
||||
const badgeWidth = this.getWidth(ctx)
|
||||
const badgeX = 0
|
||||
|
||||
// Draw badge background
|
||||
ctx.fillStyle = this.bgColor
|
||||
ctx.beginPath()
|
||||
if (ctx.roundRect) {
|
||||
ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius)
|
||||
} else {
|
||||
// Fallback for browsers that don't support roundRect
|
||||
ctx.rect(x + badgeX, y, badgeWidth, this.height)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
let drawX = x + badgeX + this.padding
|
||||
const centerY = y + this.height / 2
|
||||
|
||||
// Draw icon if present
|
||||
if (this.icon) {
|
||||
this.icon.draw(ctx, drawX, centerY)
|
||||
drawX += this.icon.fontSize + this.padding / 2 + 4
|
||||
}
|
||||
|
||||
// Draw badge text
|
||||
if (this.text) {
|
||||
ctx.fillStyle = this.fgColor
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(this.text, drawX, centerY + 1)
|
||||
}
|
||||
|
||||
ctx.font = font
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.textBaseline = textBaseline
|
||||
ctx.textAlign = textAlign
|
||||
}
|
||||
}
|
||||
89
src/lib/litegraph/src/LGraphButton.ts
Normal file
89
src/lib/litegraph/src/LGraphButton.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { LGraphBadge, type LGraphBadgeOptions } from './LGraphBadge'
|
||||
import { Rectangle } from './infrastructure/Rectangle'
|
||||
|
||||
export interface LGraphButtonOptions extends LGraphBadgeOptions {
|
||||
name?: string // To identify the button
|
||||
}
|
||||
|
||||
export class LGraphButton extends LGraphBadge {
|
||||
name?: string
|
||||
_last_area: Rectangle = new Rectangle()
|
||||
|
||||
constructor(options: LGraphButtonOptions) {
|
||||
super(options)
|
||||
this.name = options.name
|
||||
}
|
||||
|
||||
override getWidth(ctx: CanvasRenderingContext2D): number {
|
||||
if (!this.visible) return 0
|
||||
|
||||
const { font } = ctx
|
||||
ctx.font = `${this.fontSize}px 'PrimeIcons'`
|
||||
|
||||
// For icon buttons, just measure the text width without padding
|
||||
const textWidth = this.text ? ctx.measureText(this.text).width : 0
|
||||
|
||||
ctx.font = font
|
||||
return textWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* Draws the button and updates its last rendered area for hit detection.
|
||||
* @param ctx The canvas rendering context.
|
||||
* @param x The x-coordinate to draw the button at.
|
||||
* @param y The y-coordinate to draw the button at.
|
||||
*/
|
||||
override draw(ctx: CanvasRenderingContext2D, x: number, y: number): void {
|
||||
if (!this.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
const width = this.getWidth(ctx)
|
||||
|
||||
// Update the hit area
|
||||
this._last_area[0] = x + this.xOffset
|
||||
this._last_area[1] = y + this.yOffset
|
||||
this._last_area[2] = width
|
||||
this._last_area[3] = this.height
|
||||
|
||||
// Custom drawing for buttons - no background, just icon/text
|
||||
const adjustedX = x + this.xOffset
|
||||
const adjustedY = y + this.yOffset
|
||||
|
||||
const { font, fillStyle, textBaseline, textAlign } = ctx
|
||||
|
||||
// Use the same color as the title text (usually white)
|
||||
const titleTextColor = ctx.fillStyle || 'white'
|
||||
|
||||
// Draw as icon-only without background
|
||||
ctx.font = `${this.fontSize}px 'PrimeIcons'`
|
||||
ctx.fillStyle = titleTextColor
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
|
||||
const centerX = adjustedX + width / 2
|
||||
const centerY = adjustedY + this.height / 2
|
||||
|
||||
if (this.text) {
|
||||
ctx.fillText(this.text, centerX, centerY)
|
||||
}
|
||||
|
||||
// Restore context
|
||||
ctx.font = font
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.textBaseline = textBaseline
|
||||
ctx.textAlign = textAlign
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a point is inside the button's last rendered area.
|
||||
* @param x The x-coordinate of the point.
|
||||
* @param y The y-coordinate of the point.
|
||||
* @returns `true` if the point is inside the button, otherwise `false`.
|
||||
*/
|
||||
isPointInside(x: number, y: number): boolean {
|
||||
return this._last_area.containsPoint([x, y])
|
||||
}
|
||||
}
|
||||
8448
src/lib/litegraph/src/LGraphCanvas.ts
Normal file
8448
src/lib/litegraph/src/LGraphCanvas.ts
Normal file
File diff suppressed because it is too large
Load Diff
357
src/lib/litegraph/src/LGraphGroup.ts
Normal file
357
src/lib/litegraph/src/LGraphGroup.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
|
||||
|
||||
import type { LGraph } from './LGraph'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import { strokeShape } from './draw'
|
||||
import type {
|
||||
ColorOption,
|
||||
IColorable,
|
||||
IContextMenuValue,
|
||||
IPinnable,
|
||||
Point,
|
||||
Positionable,
|
||||
Size
|
||||
} from './interfaces'
|
||||
import { LiteGraph } from './litegraph'
|
||||
import {
|
||||
containsCentre,
|
||||
containsRect,
|
||||
createBounds,
|
||||
isInRectangle,
|
||||
isPointInRect,
|
||||
snapPoint
|
||||
} from './measure'
|
||||
import type { ISerialisedGroup } from './types/serialisation'
|
||||
|
||||
export interface IGraphGroupFlags extends Record<string, unknown> {
|
||||
pinned?: true
|
||||
}
|
||||
|
||||
export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
static minWidth = 140
|
||||
static minHeight = 80
|
||||
static resizeLength = 10
|
||||
static padding = 4
|
||||
static defaultColour = '#335'
|
||||
|
||||
id: number
|
||||
color?: string
|
||||
title: string
|
||||
font?: string
|
||||
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
||||
_bounding: Float32Array = new Float32Array([
|
||||
10,
|
||||
10,
|
||||
LGraphGroup.minWidth,
|
||||
LGraphGroup.minHeight
|
||||
])
|
||||
|
||||
_pos: Point = this._bounding.subarray(0, 2)
|
||||
_size: Size = this._bounding.subarray(2, 4)
|
||||
/** @deprecated See {@link _children} */
|
||||
_nodes: LGraphNode[] = []
|
||||
_children: Set<Positionable> = new Set()
|
||||
graph?: LGraph
|
||||
flags: IGraphGroupFlags = {}
|
||||
selected?: boolean
|
||||
|
||||
constructor(title?: string, id?: number) {
|
||||
// TODO: Object instantiation pattern requires too much boilerplate and null checking. ID should be passed in via constructor.
|
||||
this.id = id ?? -1
|
||||
this.title = title || 'Group'
|
||||
|
||||
const { pale_blue } = LGraphCanvas.node_colors
|
||||
this.color = pale_blue ? pale_blue.groupcolor : '#AAA'
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link IColorable.setColorOption} */
|
||||
setColorOption(colorOption: ColorOption | null): void {
|
||||
if (colorOption == null) {
|
||||
delete this.color
|
||||
} else {
|
||||
this.color = colorOption.groupcolor
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link IColorable.getColorOption} */
|
||||
getColorOption(): ColorOption | null {
|
||||
return (
|
||||
Object.values(LGraphCanvas.node_colors).find(
|
||||
(colorOption) => colorOption.groupcolor === this.color
|
||||
) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
/** Position of the group, as x,y co-ordinates in graph space */
|
||||
get pos() {
|
||||
return this._pos
|
||||
}
|
||||
|
||||
set pos(v) {
|
||||
if (!v || v.length < 2) return
|
||||
|
||||
this._pos[0] = v[0]
|
||||
this._pos[1] = v[1]
|
||||
}
|
||||
|
||||
/** Size of the group, as width,height in graph units */
|
||||
get size() {
|
||||
return this._size
|
||||
}
|
||||
|
||||
set size(v) {
|
||||
if (!v || v.length < 2) return
|
||||
|
||||
this._size[0] = Math.max(LGraphGroup.minWidth, v[0])
|
||||
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
||||
}
|
||||
|
||||
get boundingRect() {
|
||||
return this._bounding
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this._nodes
|
||||
}
|
||||
|
||||
get titleHeight() {
|
||||
return this.font_size * 1.4
|
||||
}
|
||||
|
||||
get children(): ReadonlySet<Positionable> {
|
||||
return this._children
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
return !!this.flags.pinned
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the group being accidentally moved or resized by mouse interaction.
|
||||
* Toggles pinned state if no value is provided.
|
||||
*/
|
||||
pin(value?: boolean): void {
|
||||
const newState = value === undefined ? !this.pinned : value
|
||||
|
||||
if (newState) this.flags.pinned = true
|
||||
else delete this.flags.pinned
|
||||
}
|
||||
|
||||
unpin(): void {
|
||||
this.pin(false)
|
||||
}
|
||||
|
||||
configure(o: ISerialisedGroup): void {
|
||||
this.id = o.id
|
||||
this.title = o.title
|
||||
this._bounding.set(o.bounding)
|
||||
this.color = o.color
|
||||
this.flags = o.flags || this.flags
|
||||
if (o.font_size) this.font_size = o.font_size
|
||||
}
|
||||
|
||||
serialize(): ISerialisedGroup {
|
||||
const b = this._bounding
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
bounding: [...b],
|
||||
color: this.color,
|
||||
font_size: this.font_size,
|
||||
flags: this.flags
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the group on the canvas
|
||||
* @param graphCanvas
|
||||
* @param ctx
|
||||
*/
|
||||
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
|
||||
const { padding, resizeLength, defaultColour } = LGraphGroup
|
||||
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
||||
|
||||
const [x, y] = this._pos
|
||||
const [width, height] = this._size
|
||||
const color = this.color || defaultColour
|
||||
|
||||
// Titlebar
|
||||
ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha
|
||||
ctx.fillStyle = color
|
||||
ctx.strokeStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.rect(x + 0.5, y + 0.5, width, font_size * 1.4)
|
||||
ctx.fill()
|
||||
|
||||
// Group background, border
|
||||
ctx.fillStyle = color
|
||||
ctx.strokeStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.rect(x + 0.5, y + 0.5, width, height)
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = graphCanvas.editor_alpha
|
||||
ctx.stroke()
|
||||
|
||||
// Resize marker
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + width, y + height)
|
||||
ctx.lineTo(x + width - resizeLength, y + height)
|
||||
ctx.lineTo(x + width, y + height - resizeLength)
|
||||
ctx.fill()
|
||||
|
||||
// Title
|
||||
ctx.font = `${font_size}px ${LiteGraph.GROUP_FONT}`
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(
|
||||
this.title + (this.pinned ? '📌' : ''),
|
||||
x + padding,
|
||||
y + font_size
|
||||
)
|
||||
|
||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||
strokeShape(ctx, this._bounding, {
|
||||
title_height: this.titleHeight,
|
||||
padding
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resize(width: number, height: number): boolean {
|
||||
if (this.pinned) return false
|
||||
|
||||
this._size[0] = Math.max(LGraphGroup.minWidth, width)
|
||||
this._size[1] = Math.max(LGraphGroup.minHeight, height)
|
||||
return true
|
||||
}
|
||||
|
||||
move(deltaX: number, deltaY: number, skipChildren: boolean = false): void {
|
||||
if (this.pinned) return
|
||||
|
||||
this._pos[0] += deltaX
|
||||
this._pos[1] += deltaY
|
||||
if (skipChildren === true) return
|
||||
|
||||
for (const item of this._children) {
|
||||
item.move(deltaX, deltaY)
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
snapToGrid(snapTo: number): boolean {
|
||||
return this.pinned ? false : snapPoint(this.pos, snapTo)
|
||||
}
|
||||
|
||||
recomputeInsideNodes(): void {
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
const { nodes, reroutes, groups } = this.graph
|
||||
const children = this._children
|
||||
this._nodes.length = 0
|
||||
children.clear()
|
||||
|
||||
// Move nodes we overlap the centre point of
|
||||
for (const node of nodes) {
|
||||
if (containsCentre(this._bounding, node.boundingRect)) {
|
||||
this._nodes.push(node)
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
// Move reroutes we overlap the centre point of
|
||||
for (const reroute of reroutes.values()) {
|
||||
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
|
||||
}
|
||||
|
||||
// Move groups we wholly contain
|
||||
for (const group of groups) {
|
||||
if (containsRect(this._bounding, group._bounding)) children.add(group)
|
||||
}
|
||||
|
||||
groups.sort((a, b) => {
|
||||
if (a === this) {
|
||||
return children.has(b) ? -1 : 0
|
||||
} else if (b === this) {
|
||||
return children.has(a) ? 1 : 0
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes and moves the group to neatly fit all given {@link objects}.
|
||||
* @param objects All objects that should be inside the group
|
||||
* @param padding Value in graph units to add to all sides of the group. Default: 10
|
||||
*/
|
||||
resizeTo(objects: Iterable<Positionable>, padding: number = 10): void {
|
||||
const boundingBox = createBounds(objects, padding)
|
||||
if (boundingBox === null) return
|
||||
|
||||
this.pos[0] = boundingBox[0]
|
||||
this.pos[1] = boundingBox[1] - this.titleHeight
|
||||
this.size[0] = boundingBox[2]
|
||||
this.size[1] = boundingBox[3] + this.titleHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes to the group and adjust the group's position and size accordingly
|
||||
* @param nodes The nodes to add to the group
|
||||
* @param padding The padding around the group
|
||||
*/
|
||||
addNodes(nodes: LGraphNode[], padding: number = 10): void {
|
||||
if (!this._nodes && nodes.length === 0) return
|
||||
this.resizeTo([...this.children, ...this._nodes, ...nodes], padding)
|
||||
}
|
||||
|
||||
getMenuOptions(): (
|
||||
| IContextMenuValue<string>
|
||||
| IContextMenuValue<string | null>
|
||||
| null
|
||||
)[] {
|
||||
return [
|
||||
{
|
||||
content: this.pinned ? 'Unpin' : 'Pin',
|
||||
callback: () => {
|
||||
if (this.pinned) this.unpin()
|
||||
else this.pin()
|
||||
this.setDirtyCanvas(false, true)
|
||||
}
|
||||
},
|
||||
null,
|
||||
{ content: 'Title', callback: LGraphCanvas.onShowPropertyEditor },
|
||||
{
|
||||
content: 'Color',
|
||||
has_submenu: true,
|
||||
callback: LGraphCanvas.onMenuNodeColors
|
||||
},
|
||||
{
|
||||
content: 'Font size',
|
||||
property: 'font_size',
|
||||
type: 'Number',
|
||||
callback: LGraphCanvas.onShowPropertyEditor
|
||||
},
|
||||
null,
|
||||
{ content: 'Remove', callback: LGraphCanvas.onMenuNodeRemove }
|
||||
]
|
||||
}
|
||||
|
||||
isPointInTitlebar(x: number, y: number): boolean {
|
||||
const b = this.boundingRect
|
||||
return isInRectangle(x, y, b[0], b[1], b[2], this.titleHeight)
|
||||
}
|
||||
|
||||
isInResize(x: number, y: number): boolean {
|
||||
const b = this.boundingRect
|
||||
const right = b[0] + b[2]
|
||||
const bottom = b[1] + b[3]
|
||||
|
||||
return (
|
||||
x < right &&
|
||||
y < bottom &&
|
||||
x - right + (y - bottom) > -LGraphGroup.resizeLength
|
||||
)
|
||||
}
|
||||
|
||||
isPointInside = LGraphNode.prototype.isPointInside
|
||||
setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas
|
||||
}
|
||||
68
src/lib/litegraph/src/LGraphIcon.ts
Normal file
68
src/lib/litegraph/src/LGraphIcon.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export interface LGraphIconOptions {
|
||||
unicode: string
|
||||
fontFamily?: string
|
||||
color?: string
|
||||
bgColor?: string
|
||||
fontSize?: number
|
||||
circlePadding?: number
|
||||
xOffset?: number
|
||||
yOffset?: number
|
||||
}
|
||||
|
||||
export class LGraphIcon {
|
||||
unicode: string
|
||||
fontFamily: string
|
||||
color: string
|
||||
bgColor?: string
|
||||
fontSize: number
|
||||
circlePadding: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
|
||||
constructor({
|
||||
unicode,
|
||||
fontFamily = 'PrimeIcons',
|
||||
color = '#e6c200',
|
||||
bgColor,
|
||||
fontSize = 16,
|
||||
circlePadding = 2,
|
||||
xOffset = 0,
|
||||
yOffset = 0
|
||||
}: LGraphIconOptions) {
|
||||
this.unicode = unicode
|
||||
this.fontFamily = fontFamily
|
||||
this.color = color
|
||||
this.bgColor = bgColor
|
||||
this.fontSize = fontSize
|
||||
this.circlePadding = circlePadding
|
||||
this.xOffset = xOffset
|
||||
this.yOffset = yOffset
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D, x: number, y: number) {
|
||||
x += this.xOffset
|
||||
y += this.yOffset
|
||||
|
||||
const { font, textBaseline, textAlign, fillStyle } = ctx
|
||||
|
||||
ctx.font = `${this.fontSize}px '${this.fontFamily}'`
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
const iconRadius = this.fontSize / 2 + this.circlePadding
|
||||
// Draw icon background circle if bgColor is set
|
||||
if (this.bgColor) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
|
||||
ctx.fillStyle = this.bgColor
|
||||
ctx.fill()
|
||||
}
|
||||
// Draw icon
|
||||
ctx.fillStyle = this.color
|
||||
ctx.fillText(this.unicode, x + iconRadius, y)
|
||||
|
||||
ctx.font = font
|
||||
ctx.textBaseline = textBaseline
|
||||
ctx.textAlign = textAlign
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
}
|
||||
4078
src/lib/litegraph/src/LGraphNode.ts
Normal file
4078
src/lib/litegraph/src/LGraphNode.ts
Normal file
File diff suppressed because it is too large
Load Diff
493
src/lib/litegraph/src/LLink.ts
Normal file
493
src/lib/litegraph/src/LLink.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { Reroute, RerouteId } from './Reroute'
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
ReadonlyLinkNetwork
|
||||
} from './interfaces'
|
||||
import { Subgraph } from './litegraph'
|
||||
import type {
|
||||
Serialisable,
|
||||
SerialisableLLink,
|
||||
SubgraphIO
|
||||
} from './types/serialisation'
|
||||
|
||||
export type LinkId = number
|
||||
|
||||
export type SerialisedLLinkArray = [
|
||||
id: LinkId,
|
||||
origin_id: NodeId,
|
||||
origin_slot: number,
|
||||
target_id: NodeId,
|
||||
target_slot: number,
|
||||
type: ISlotType
|
||||
]
|
||||
|
||||
// Resolved connection union; eliminates subgraph in/out as a possibility
|
||||
export type ResolvedConnection = BaseResolvedConnection &
|
||||
(
|
||||
| (ResolvedSubgraphInput & ResolvedNormalOutput)
|
||||
| (ResolvedNormalInput & ResolvedSubgraphOutput)
|
||||
| (ResolvedNormalInput & ResolvedNormalOutput)
|
||||
)
|
||||
|
||||
interface BaseResolvedConnection {
|
||||
link: LLink
|
||||
/** The node on the input side of the link (owns {@link input}) */
|
||||
inputNode?: LGraphNode
|
||||
/** The input the link is connected to (mutually exclusive with {@link subgraphOutput}) */
|
||||
input?: INodeInputSlot
|
||||
/** The node on the output side of the link (owns {@link output}) */
|
||||
outputNode?: LGraphNode
|
||||
/** The output the link is connected to (mutually exclusive with {@link subgraphInput}) */
|
||||
output?: INodeOutputSlot
|
||||
/** The subgraph output the link is connected to (mutually exclusive with {@link input}) */
|
||||
subgraphOutput?: SubgraphIO
|
||||
/** The subgraph input the link is connected to (mutually exclusive with {@link output}) */
|
||||
subgraphInput?: SubgraphIO
|
||||
}
|
||||
|
||||
interface ResolvedNormalInput {
|
||||
inputNode: LGraphNode | undefined
|
||||
input: INodeInputSlot | undefined
|
||||
subgraphOutput?: undefined
|
||||
}
|
||||
|
||||
interface ResolvedNormalOutput {
|
||||
outputNode: LGraphNode | undefined
|
||||
output: INodeOutputSlot | undefined
|
||||
subgraphInput?: undefined
|
||||
}
|
||||
|
||||
interface ResolvedSubgraphInput {
|
||||
inputNode?: undefined
|
||||
/** The actual input slot the link is connected to (mutually exclusive with {@link subgraphOutput}) */
|
||||
input?: undefined
|
||||
subgraphOutput: SubgraphIO
|
||||
}
|
||||
|
||||
interface ResolvedSubgraphOutput {
|
||||
outputNode?: undefined
|
||||
output?: undefined
|
||||
subgraphInput: SubgraphIO
|
||||
}
|
||||
|
||||
type BasicReadonlyNetwork = Pick<
|
||||
ReadonlyLinkNetwork,
|
||||
'getNodeById' | 'links' | 'getLink' | 'inputNode' | 'outputNode'
|
||||
>
|
||||
|
||||
// this is the class in charge of storing link information
|
||||
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
static _drawDebug = false
|
||||
|
||||
/** Link ID */
|
||||
id: LinkId
|
||||
parentId?: RerouteId
|
||||
type: ISlotType
|
||||
/** Output node ID */
|
||||
origin_id: NodeId
|
||||
/** Output slot index */
|
||||
origin_slot: number
|
||||
/** Input node ID */
|
||||
target_id: NodeId
|
||||
/** Input slot index */
|
||||
target_slot: number
|
||||
|
||||
data?: number | string | boolean | { toToolTip?(): string }
|
||||
_data?: unknown
|
||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||
_pos: Float32Array
|
||||
/** @todo Clean up - never implemented in comfy. */
|
||||
_last_time?: number
|
||||
/** The last canvas 2D path that was used to render this link */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
|
||||
/** @inheritdoc */
|
||||
_dragging?: boolean
|
||||
|
||||
#color?: CanvasColour | null
|
||||
/** Custom colour for this link only */
|
||||
public get color(): CanvasColour | null | undefined {
|
||||
return this.#color
|
||||
}
|
||||
|
||||
public set color(value: CanvasColour) {
|
||||
this.#color = value === '' ? null : value
|
||||
}
|
||||
|
||||
public get isFloatingOutput(): boolean {
|
||||
return this.origin_id === -1 && this.origin_slot === -1
|
||||
}
|
||||
|
||||
public get isFloatingInput(): boolean {
|
||||
return this.target_id === -1 && this.target_slot === -1
|
||||
}
|
||||
|
||||
public get isFloating(): boolean {
|
||||
return this.isFloatingOutput || this.isFloatingInput
|
||||
}
|
||||
|
||||
/** `true` if this link is connected to a subgraph input node (the actual origin is in a different graph). */
|
||||
get originIsIoNode(): boolean {
|
||||
return this.origin_id === SUBGRAPH_INPUT_ID
|
||||
}
|
||||
|
||||
/** `true` if this link is connected to a subgraph output node (the actual target is in a different graph). */
|
||||
get targetIsIoNode(): boolean {
|
||||
return this.target_id === SUBGRAPH_OUTPUT_ID
|
||||
}
|
||||
|
||||
constructor(
|
||||
id: LinkId,
|
||||
type: ISlotType,
|
||||
origin_id: NodeId,
|
||||
origin_slot: number,
|
||||
target_id: NodeId,
|
||||
target_slot: number,
|
||||
parentId?: RerouteId
|
||||
) {
|
||||
this.id = id
|
||||
this.type = type
|
||||
this.origin_id = origin_id
|
||||
this.origin_slot = origin_slot
|
||||
this.target_id = target_id
|
||||
this.target_slot = target_slot
|
||||
this.parentId = parentId
|
||||
|
||||
this._data = null
|
||||
// center
|
||||
this._pos = new Float32Array(2)
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link LLink.create} */
|
||||
static createFromArray(data: SerialisedLLinkArray): LLink {
|
||||
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
|
||||
}
|
||||
|
||||
/**
|
||||
* LLink static factory: creates a new LLink from the provided data.
|
||||
* @param data Serialised LLink data to create the link from
|
||||
* @returns A new LLink
|
||||
*/
|
||||
static create(data: SerialisableLLink): LLink {
|
||||
return new LLink(
|
||||
data.id,
|
||||
data.type,
|
||||
data.origin_id,
|
||||
data.origin_slot,
|
||||
data.target_id,
|
||||
data.target_slot,
|
||||
data.parentId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will not be included.
|
||||
* @returns An ordered array of all reroutes from the node output to
|
||||
* this reroute or the reroute before it. Otherwise, an empty array.
|
||||
*/
|
||||
static getReroutes(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment
|
||||
): Reroute[] {
|
||||
if (!linkSegment.parentId) return []
|
||||
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
|
||||
}
|
||||
|
||||
static getFirstReroute(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment
|
||||
): Reroute | undefined {
|
||||
return LLink.getReroutes(network, linkSegment).at(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the reroute in the chain after the provided reroute ID.
|
||||
* @param network The network this link belongs to
|
||||
* @param linkSegment The starting point of the search (input side).
|
||||
* Typically the LLink object itself, but can be any link segment.
|
||||
* @param rerouteId The matching reroute will have this set as its {@link parentId}.
|
||||
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
||||
*/
|
||||
static findNextReroute(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment,
|
||||
rerouteId: RerouteId
|
||||
): Reroute | null | undefined {
|
||||
if (!linkSegment.parentId) return
|
||||
return network.reroutes
|
||||
.get(linkSegment.parentId)
|
||||
?.findNextReroute(rerouteId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the origin node of a link.
|
||||
* @param network The network to search
|
||||
* @param linkId The ID of the link to get the origin node of
|
||||
* @returns The origin node of the link, or `undefined` if the link is not found or the origin node is not found
|
||||
*/
|
||||
static getOriginNode(
|
||||
network: BasicReadonlyNetwork,
|
||||
linkId: LinkId
|
||||
): LGraphNode | undefined {
|
||||
const id = network.links.get(linkId)?.origin_id
|
||||
return network.getNodeById(id) ?? undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target node of a link.
|
||||
* @param network The network to search
|
||||
* @param linkId The ID of the link to get the target node of
|
||||
* @returns The target node of the link, or `undefined` if the link is not found or the target node is not found
|
||||
*/
|
||||
static getTargetNode(
|
||||
network: BasicReadonlyNetwork,
|
||||
linkId: LinkId
|
||||
): LGraphNode | undefined {
|
||||
const id = network.links.get(linkId)?.target_id
|
||||
return network.getNodeById(id) ?? undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a link ID to the link, node, and slot objects.
|
||||
* @param linkId The {@link id} of the link to resolve
|
||||
* @param network The link network to search
|
||||
* @returns An object containing the input and output nodes, as well as the input and output slots.
|
||||
* @remarks This method is heavier than others; it will always resolve all objects.
|
||||
* Whilst the performance difference should in most cases be negligible,
|
||||
* it is recommended to use simpler methods where appropriate.
|
||||
*/
|
||||
static resolve(
|
||||
linkId: LinkId | null | undefined,
|
||||
network: BasicReadonlyNetwork
|
||||
): ResolvedConnection | undefined {
|
||||
return network.getLink(linkId)?.resolve(network)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a list of link IDs to the link, node, and slot objects.
|
||||
* Discards invalid link IDs.
|
||||
* @param linkIds An iterable of link {@link id}s to resolve
|
||||
* @param network The link network to search
|
||||
* @returns An array of resolved connections. If a link is not found, it is not included in the array.
|
||||
* @see {@link LLink.resolve}
|
||||
*/
|
||||
static resolveMany(
|
||||
linkIds: Iterable<LinkId>,
|
||||
network: BasicReadonlyNetwork
|
||||
): ResolvedConnection[] {
|
||||
const resolved: ResolvedConnection[] = []
|
||||
for (const id of linkIds) {
|
||||
const r = network.getLink(id)?.resolve(network)
|
||||
if (r) resolved.push(r)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the primitive ID values stored in the link to the referenced objects.
|
||||
* @param network The link network to search
|
||||
* @returns An object containing the input and output nodes, as well as the input and output slots.
|
||||
* @remarks This method is heavier than others; it will always resolve all objects.
|
||||
* Whilst the performance difference should in most cases be negligible,
|
||||
* it is recommended to use simpler methods where appropriate.
|
||||
*/
|
||||
resolve(network: BasicReadonlyNetwork): ResolvedConnection {
|
||||
const inputNode =
|
||||
this.target_id === -1
|
||||
? undefined
|
||||
: network.getNodeById(this.target_id) ?? undefined
|
||||
const input = inputNode?.inputs[this.target_slot]
|
||||
const subgraphInput = this.originIsIoNode
|
||||
? network.inputNode?.slots[this.origin_slot]
|
||||
: undefined
|
||||
if (subgraphInput) {
|
||||
return { inputNode, input, subgraphInput, link: this }
|
||||
}
|
||||
|
||||
const outputNode =
|
||||
this.origin_id === -1
|
||||
? undefined
|
||||
: network.getNodeById(this.origin_id) ?? undefined
|
||||
const output = outputNode?.outputs[this.origin_slot]
|
||||
const subgraphOutput = this.targetIsIoNode
|
||||
? network.outputNode?.slots[this.target_slot]
|
||||
: undefined
|
||||
if (subgraphOutput) {
|
||||
return {
|
||||
outputNode,
|
||||
output,
|
||||
subgraphInput: undefined,
|
||||
subgraphOutput,
|
||||
link: this
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
inputNode,
|
||||
outputNode,
|
||||
input,
|
||||
output,
|
||||
subgraphInput,
|
||||
subgraphOutput,
|
||||
link: this
|
||||
}
|
||||
}
|
||||
|
||||
configure(o: LLink | SerialisedLLinkArray) {
|
||||
if (Array.isArray(o)) {
|
||||
this.id = o[0]
|
||||
this.origin_id = o[1]
|
||||
this.origin_slot = o[2]
|
||||
this.target_id = o[3]
|
||||
this.target_slot = o[4]
|
||||
this.type = o[5]
|
||||
} else {
|
||||
this.id = o.id
|
||||
this.type = o.type
|
||||
this.origin_id = o.origin_id
|
||||
this.origin_slot = o.origin_slot
|
||||
this.target_id = o.target_id
|
||||
this.target_slot = o.target_slot
|
||||
this.parentId = o.parentId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified node id and output index are this link's origin (output side).
|
||||
* @param nodeId ID of the node to check
|
||||
* @param outputIndex The array index of the node output
|
||||
* @returns `true` if the origin matches, otherwise `false`.
|
||||
*/
|
||||
hasOrigin(nodeId: NodeId, outputIndex: number): boolean {
|
||||
return this.origin_id === nodeId && this.origin_slot === outputIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified node id and input index are this link's target (input side).
|
||||
* @param nodeId ID of the node to check
|
||||
* @param inputIndex The array index of the node input
|
||||
* @returns `true` if the target matches, otherwise `false`.
|
||||
*/
|
||||
hasTarget(nodeId: NodeId, inputIndex: number): boolean {
|
||||
return this.target_id === nodeId && this.target_slot === inputIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a floating link from this link.
|
||||
* @param slotType The side of the link that is still connected
|
||||
* @param parentId The parent reroute ID of the link
|
||||
* @returns A new LLink that is floating
|
||||
*/
|
||||
toFloating(slotType: 'input' | 'output', parentId: RerouteId): LLink {
|
||||
const exported = this.asSerialisable()
|
||||
exported.id = -1
|
||||
exported.parentId = parentId
|
||||
|
||||
if (slotType === 'input') {
|
||||
exported.origin_id = -1
|
||||
exported.origin_slot = -1
|
||||
} else {
|
||||
exported.target_id = -1
|
||||
exported.target_slot = -1
|
||||
}
|
||||
|
||||
return LLink.create(exported)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used
|
||||
* @param network The container (LGraph) where reroutes should be updated
|
||||
* @param keepReroutes If `undefined`, reroutes will be automatically removed if no links remain.
|
||||
* If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively.
|
||||
*/
|
||||
disconnect(network: LinkNetwork, keepReroutes?: 'input' | 'output'): void {
|
||||
const reroutes = LLink.getReroutes(network, this)
|
||||
|
||||
const lastReroute = reroutes.at(-1)
|
||||
|
||||
// When floating from output, 1-to-1 ratio of floating link to final reroute (tree-like)
|
||||
const outputFloating =
|
||||
keepReroutes === 'output' &&
|
||||
lastReroute?.linkIds.size === 1 &&
|
||||
lastReroute.floatingLinkIds.size === 0
|
||||
|
||||
// When floating from inputs, the final (input side) reroute may have many floating links
|
||||
if (outputFloating || (keepReroutes === 'input' && lastReroute)) {
|
||||
const newLink = LLink.create(this)
|
||||
newLink.id = -1
|
||||
|
||||
if (keepReroutes === 'input') {
|
||||
newLink.origin_id = -1
|
||||
newLink.origin_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: 'input' }
|
||||
} else {
|
||||
newLink.target_id = -1
|
||||
newLink.target_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: 'output' }
|
||||
}
|
||||
|
||||
network.addFloatingLink(newLink)
|
||||
}
|
||||
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.delete(this.id)
|
||||
if (!keepReroutes && !reroute.totalLinks) {
|
||||
network.reroutes.delete(reroute.id)
|
||||
}
|
||||
}
|
||||
network.links.delete(this.id)
|
||||
|
||||
if (this.originIsIoNode && network instanceof Subgraph) {
|
||||
const subgraphInput = network.inputs.at(this.origin_slot)
|
||||
if (!subgraphInput)
|
||||
throw new Error('Invalid link - subgraph input not found')
|
||||
|
||||
subgraphInput.events.dispatch('input-disconnected', {
|
||||
input: subgraphInput
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
|
||||
* @returns An array representing this LLink
|
||||
*/
|
||||
serialize(): SerialisedLLinkArray {
|
||||
return [
|
||||
this.id,
|
||||
this.origin_id,
|
||||
this.origin_slot,
|
||||
this.target_id,
|
||||
this.target_slot,
|
||||
this.type
|
||||
]
|
||||
}
|
||||
|
||||
asSerialisable(): SerialisableLLink {
|
||||
const copy: SerialisableLLink = {
|
||||
id: this.id,
|
||||
origin_id: this.origin_id,
|
||||
origin_slot: this.origin_slot,
|
||||
target_id: this.target_id,
|
||||
target_slot: this.target_slot,
|
||||
type: this.type
|
||||
}
|
||||
if (this.parentId) copy.parentId = this.parentId
|
||||
return copy
|
||||
}
|
||||
}
|
||||
992
src/lib/litegraph/src/LiteGraphGlobal.ts
Normal file
992
src/lib/litegraph/src/LiteGraphGlobal.ts
Normal file
@@ -0,0 +1,992 @@
|
||||
import { ContextMenu } from './ContextMenu'
|
||||
import { CurveEditor } from './CurveEditor'
|
||||
import { DragAndScale } from './DragAndScale'
|
||||
import { LGraph } from './LGraph'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import { LLink } from './LLink'
|
||||
import { Reroute } from './Reroute'
|
||||
import { InputIndicators } from './canvas/InputIndicators'
|
||||
import { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw'
|
||||
import { Rectangle } from './infrastructure/Rectangle'
|
||||
import type { Dictionary, ISlotType, Rect, WhenNullish } from './interfaces'
|
||||
import { distance, isInsideRectangle, overlapBounding } from './measure'
|
||||
import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase'
|
||||
import { SubgraphSlot } from './subgraph/SubgraphSlotBase'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LinkDirection,
|
||||
LinkRenderType,
|
||||
NodeSlotType,
|
||||
RenderShape,
|
||||
TitleMode
|
||||
} from './types/globalEnums'
|
||||
import { createUuidv4 } from './utils/uuid'
|
||||
|
||||
/**
|
||||
* The Global Scope. It contains all the registered node classes.
|
||||
*/
|
||||
export class LiteGraphGlobal {
|
||||
// Enums
|
||||
SlotShape = SlotShape
|
||||
SlotDirection = SlotDirection
|
||||
SlotType = SlotType
|
||||
LabelPosition = LabelPosition
|
||||
|
||||
/** Used in serialised graphs at one point. */
|
||||
VERSION = 0.4 as const
|
||||
|
||||
CANVAS_GRID_SIZE = 10
|
||||
|
||||
NODE_TITLE_HEIGHT = 30
|
||||
NODE_TITLE_TEXT_Y = 20
|
||||
NODE_SLOT_HEIGHT = 20
|
||||
NODE_WIDGET_HEIGHT = 20
|
||||
NODE_WIDTH = 140
|
||||
NODE_MIN_WIDTH = 50
|
||||
NODE_COLLAPSED_RADIUS = 10
|
||||
NODE_COLLAPSED_WIDTH = 80
|
||||
NODE_TITLE_COLOR = '#999'
|
||||
NODE_SELECTED_TITLE_COLOR = '#FFF'
|
||||
NODE_TEXT_SIZE = 14
|
||||
NODE_TEXT_COLOR = '#AAA'
|
||||
NODE_TEXT_HIGHLIGHT_COLOR = '#EEE'
|
||||
NODE_SUBTEXT_SIZE = 12
|
||||
NODE_DEFAULT_COLOR = '#333'
|
||||
NODE_DEFAULT_BGCOLOR = '#353535'
|
||||
NODE_DEFAULT_BOXCOLOR = '#666'
|
||||
NODE_DEFAULT_SHAPE = RenderShape.ROUND
|
||||
NODE_BOX_OUTLINE_COLOR = '#FFF'
|
||||
NODE_ERROR_COLOUR = '#E00'
|
||||
NODE_FONT = 'Arial'
|
||||
|
||||
DEFAULT_FONT = 'Arial'
|
||||
DEFAULT_SHADOW_COLOR = 'rgba(0,0,0,0.5)'
|
||||
|
||||
DEFAULT_GROUP_FONT = 24
|
||||
DEFAULT_GROUP_FONT_SIZE?: any
|
||||
GROUP_FONT = 'Arial'
|
||||
|
||||
WIDGET_BGCOLOR = '#222'
|
||||
WIDGET_OUTLINE_COLOR = '#666'
|
||||
WIDGET_ADVANCED_OUTLINE_COLOR = 'rgba(56, 139, 253, 0.8)'
|
||||
WIDGET_TEXT_COLOR = '#DDD'
|
||||
WIDGET_SECONDARY_TEXT_COLOR = '#999'
|
||||
WIDGET_DISABLED_TEXT_COLOR = '#666'
|
||||
|
||||
LINK_COLOR = '#9A9'
|
||||
EVENT_LINK_COLOR = '#A86'
|
||||
CONNECTING_LINK_COLOR = '#AFA'
|
||||
|
||||
/** avoid infinite loops */
|
||||
MAX_NUMBER_OF_NODES = 10_000
|
||||
/** default node position */
|
||||
DEFAULT_POSITION = [100, 100]
|
||||
/** ,"circle" */
|
||||
VALID_SHAPES = ['default', 'box', 'round', 'card'] satisfies (
|
||||
| 'default'
|
||||
| Lowercase<keyof typeof RenderShape>
|
||||
)[]
|
||||
ROUND_RADIUS = 8
|
||||
|
||||
// shapes are used for nodes but also for slots
|
||||
BOX_SHAPE = RenderShape.BOX
|
||||
ROUND_SHAPE = RenderShape.ROUND
|
||||
CIRCLE_SHAPE = RenderShape.CIRCLE
|
||||
CARD_SHAPE = RenderShape.CARD
|
||||
ARROW_SHAPE = RenderShape.ARROW
|
||||
/** intended for slot arrays */
|
||||
GRID_SHAPE = RenderShape.GRID
|
||||
|
||||
// enums
|
||||
INPUT = NodeSlotType.INPUT
|
||||
OUTPUT = NodeSlotType.OUTPUT
|
||||
|
||||
// TODO: -1 can lead to ambiguity in JS; these should be updated to a more explicit constant or Symbol.
|
||||
/** for outputs */
|
||||
EVENT = -1 as const
|
||||
/** for inputs */
|
||||
ACTION = -1 as const
|
||||
|
||||
/** helper, will add "On Request" and more in the future */
|
||||
NODE_MODES = ['Always', 'On Event', 'Never', 'On Trigger']
|
||||
/** use with node_box_coloured_by_mode */
|
||||
NODE_MODES_COLORS = ['#666', '#422', '#333', '#224', '#626']
|
||||
ALWAYS = LGraphEventMode.ALWAYS
|
||||
ON_EVENT = LGraphEventMode.ON_EVENT
|
||||
NEVER = LGraphEventMode.NEVER
|
||||
ON_TRIGGER = LGraphEventMode.ON_TRIGGER
|
||||
|
||||
UP = LinkDirection.UP
|
||||
DOWN = LinkDirection.DOWN
|
||||
LEFT = LinkDirection.LEFT
|
||||
RIGHT = LinkDirection.RIGHT
|
||||
CENTER = LinkDirection.CENTER
|
||||
|
||||
/** helper */
|
||||
LINK_RENDER_MODES = ['Straight', 'Linear', 'Spline']
|
||||
HIDDEN_LINK = LinkRenderType.HIDDEN_LINK
|
||||
STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK
|
||||
LINEAR_LINK = LinkRenderType.LINEAR_LINK
|
||||
SPLINE_LINK = LinkRenderType.SPLINE_LINK
|
||||
|
||||
NORMAL_TITLE = TitleMode.NORMAL_TITLE
|
||||
NO_TITLE = TitleMode.NO_TITLE
|
||||
TRANSPARENT_TITLE = TitleMode.TRANSPARENT_TITLE
|
||||
AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE
|
||||
|
||||
/** arrange nodes vertically */
|
||||
VERTICAL_LAYOUT = 'vertical'
|
||||
|
||||
/** used to redirect calls */
|
||||
proxy = null
|
||||
node_images_path = ''
|
||||
|
||||
debug = false
|
||||
catch_exceptions = true
|
||||
throw_errors = true
|
||||
/** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */
|
||||
allow_scripts = false
|
||||
/** nodetypes by string */
|
||||
registered_node_types: Record<string, typeof LGraphNode> = {}
|
||||
/** @deprecated used for dropping files in the canvas. It appears the code that enables this was removed, but the object remains and is references by built-in drag drop. */
|
||||
node_types_by_file_extension: Record<string, { type: string }> = {}
|
||||
/** node types by classname */
|
||||
Nodes: Record<string, typeof LGraphNode> = {}
|
||||
/** used to store vars between graphs */
|
||||
Globals = {}
|
||||
|
||||
/** @deprecated Unused and will be deleted. */
|
||||
searchbox_extras: Dictionary<unknown> = {}
|
||||
|
||||
/** [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback */
|
||||
node_box_coloured_when_on = false
|
||||
/** [true!] nodebox based on node mode, visual feedback */
|
||||
node_box_coloured_by_mode = false
|
||||
|
||||
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
|
||||
dialog_close_on_mouse_leave = false
|
||||
dialog_close_on_mouse_leave_delay = 500
|
||||
|
||||
/** [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys */
|
||||
shift_click_do_break_link_from = false
|
||||
/** [false!]prefer false, way too easy to break links */
|
||||
click_do_break_link_to = false
|
||||
/** [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! */
|
||||
ctrl_alt_click_do_break_link = true
|
||||
/** [true!] snaps links when dragging connections over valid targets */
|
||||
snaps_for_comfy = true
|
||||
/** [true!] renders a partial border to highlight when a dragged link is snapped to a node */
|
||||
snap_highlights_node = true
|
||||
|
||||
/**
|
||||
* If `true`, items always snap to the grid - modifier keys are ignored.
|
||||
* When {@link snapToGrid} is falsy, a value of `1` is used.
|
||||
* Default: `false`
|
||||
*/
|
||||
alwaysSnapToGrid?: boolean
|
||||
|
||||
/**
|
||||
* When set to a positive number, when nodes are moved their positions will
|
||||
* be rounded to the nearest multiple of this value. Half up.
|
||||
* Default: `undefined`
|
||||
* @todo Not implemented - see {@link LiteGraph.CANVAS_GRID_SIZE}
|
||||
*/
|
||||
snapToGrid?: number
|
||||
|
||||
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
|
||||
search_hide_on_mouse_leave = true
|
||||
/**
|
||||
* [true!] enable filtering slots type in the search widget
|
||||
* !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
|
||||
*/
|
||||
search_filter_enabled = false
|
||||
/** [true!] opens the results list when opening the search widget */
|
||||
search_show_all_on_open = true
|
||||
|
||||
/**
|
||||
* [if want false, use true, run, get vars values to be statically set, than disable]
|
||||
* nodes types and nodeclass association with node types need to be calculated,
|
||||
* if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
|
||||
*/
|
||||
auto_load_slot_types = false
|
||||
|
||||
// set these values if not using auto_load_slot_types
|
||||
/** slot types for nodeclass */
|
||||
registered_slot_in_types: Record<string, { nodes: string[] }> = {}
|
||||
/** slot types for nodeclass */
|
||||
registered_slot_out_types: Record<string, { nodes: string[] }> = {}
|
||||
/** slot types IN */
|
||||
slot_types_in: string[] = []
|
||||
/** slot types OUT */
|
||||
slot_types_out: string[] = []
|
||||
/**
|
||||
* specify for each IN slot type a(/many) default node(s), use single string, array, or object
|
||||
* (with node, title, parameters, ..) like for search
|
||||
*/
|
||||
slot_types_default_in: Record<string, string[]> = {}
|
||||
/**
|
||||
* specify for each OUT slot type a(/many) default node(s), use single string, array, or object
|
||||
* (with node, title, parameters, ..) like for search
|
||||
*/
|
||||
slot_types_default_out: Record<string, string[]> = {}
|
||||
|
||||
/** [true!] very handy, ALT click to clone and drag the new node */
|
||||
alt_drag_do_clone_nodes = false
|
||||
|
||||
/**
|
||||
* [true!] will create and connect event slots when using action/events connections,
|
||||
* !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
|
||||
*/
|
||||
do_add_triggers_slots = false
|
||||
|
||||
/** [false!] being events, it is strongly reccomended to use them sequentially, one by one */
|
||||
allow_multi_output_for_events = true
|
||||
|
||||
/** [true!] allows to create and connect a ndoe clicking with the third button (wheel) */
|
||||
middle_click_slot_add_default_node = false
|
||||
|
||||
/** [true!] dragging a link to empty space will open a menu, add from list, search or defaults */
|
||||
release_link_on_empty_shows_menu = false
|
||||
|
||||
/** "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) */
|
||||
pointerevents_method = 'pointer'
|
||||
|
||||
/**
|
||||
* [true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected
|
||||
* with the inputs of the newly pasted nodes
|
||||
*/
|
||||
ctrl_shift_v_paste_connect_unselected_outputs = true
|
||||
|
||||
// if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
|
||||
// use this if you must have node IDs that are unique across all graphs and subgraphs.
|
||||
use_uuids = false
|
||||
|
||||
// Whether to highlight the bounding box of selected groups
|
||||
highlight_selected_group = true
|
||||
|
||||
/** Whether to scale context with the graph when zooming in. Zooming out never makes context menus smaller. */
|
||||
context_menu_scaling = false
|
||||
|
||||
/**
|
||||
* Debugging flag. Repeats deprecation warnings every time they are reported.
|
||||
* May impact performance.
|
||||
*/
|
||||
alwaysRepeatWarnings: boolean = false
|
||||
|
||||
/**
|
||||
* Array of callbacks to execute when Litegraph first reports a deprecated API being used.
|
||||
* @see alwaysRepeatWarnings By default, will not repeat identical messages.
|
||||
*/
|
||||
onDeprecationWarning: ((message: string, source?: object) => void)[] = [
|
||||
console.warn
|
||||
]
|
||||
|
||||
/**
|
||||
* If `true`, mouse wheel events will be interpreted as trackpad gestures.
|
||||
* Tested on MacBook M4 Pro.
|
||||
* @default false
|
||||
* @see macGesturesRequireMac
|
||||
*/
|
||||
macTrackpadGestures: boolean = false
|
||||
|
||||
/**
|
||||
* If both this setting and {@link macTrackpadGestures} are `true`, trackpad gestures will
|
||||
* only be enabled when the browser user agent includes "Mac".
|
||||
* @default true
|
||||
* @see macTrackpadGestures
|
||||
*/
|
||||
macGesturesRequireMac: boolean = true
|
||||
|
||||
/**
|
||||
* "standard": change the dragging on left mouse button click to select, enable middle-click or spacebar+left-click dragging
|
||||
* "legacy": Enable dragging on left-click (original behavior)
|
||||
* @default "legacy"
|
||||
*/
|
||||
canvasNavigationMode: 'standard' | 'legacy' = 'legacy'
|
||||
|
||||
/**
|
||||
* If `true`, widget labels and values will both be truncated (proportionally to size),
|
||||
* until they fit within the widget.
|
||||
*
|
||||
* Otherwise, the label will be truncated completely before the value is truncated.
|
||||
* @default false
|
||||
*/
|
||||
truncateWidgetTextEvenly: boolean = false
|
||||
|
||||
/**
|
||||
* If `true`, widget values will be completely truncated when shrinking a widget,
|
||||
* before truncating widget labels. {@link truncateWidgetTextEvenly} must be `false`.
|
||||
* @default false
|
||||
*/
|
||||
truncateWidgetValuesFirst: boolean = false
|
||||
|
||||
/**
|
||||
* If `true`, the current viewport scale & offset of the first attached canvas will be included with the graph when exporting.
|
||||
* @default true
|
||||
*/
|
||||
saveViewportWithGraph: boolean = true
|
||||
|
||||
// TODO: Remove legacy accessors
|
||||
LGraph = LGraph
|
||||
LLink = LLink
|
||||
LGraphNode = LGraphNode
|
||||
LGraphGroup = LGraphGroup
|
||||
DragAndScale = DragAndScale
|
||||
LGraphCanvas = LGraphCanvas
|
||||
ContextMenu = ContextMenu
|
||||
CurveEditor = CurveEditor
|
||||
Reroute = Reroute
|
||||
|
||||
constructor() {
|
||||
Object.defineProperty(this, 'Classes', { writable: false })
|
||||
}
|
||||
|
||||
Classes = {
|
||||
get SubgraphSlot() {
|
||||
return SubgraphSlot
|
||||
},
|
||||
get SubgraphIONodeBase() {
|
||||
return SubgraphIONodeBase
|
||||
},
|
||||
|
||||
// Rich drawing
|
||||
get Rectangle() {
|
||||
return Rectangle
|
||||
},
|
||||
|
||||
// Debug / helpers
|
||||
get InputIndicators() {
|
||||
return InputIndicators
|
||||
}
|
||||
}
|
||||
|
||||
onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void
|
||||
onNodeTypeReplaced?(
|
||||
type: string,
|
||||
base_class: typeof LGraphNode,
|
||||
prev: unknown
|
||||
): void
|
||||
|
||||
/**
|
||||
* Register a node class so it can be listed when the user wants to create a new one
|
||||
* @param type name of the node and path
|
||||
* @param base_class class containing the structure of a node
|
||||
*/
|
||||
registerNodeType(type: string, base_class: typeof LGraphNode): void {
|
||||
if (!base_class.prototype)
|
||||
throw 'Cannot register a simple object, it must be a class with a prototype'
|
||||
base_class.type = type
|
||||
|
||||
if (this.debug) console.log('Node registered:', type)
|
||||
|
||||
const classname = base_class.name
|
||||
|
||||
const pos = type.lastIndexOf('/')
|
||||
base_class.category = type.substring(0, pos)
|
||||
|
||||
base_class.title ||= classname
|
||||
|
||||
// extend class
|
||||
for (const i in LGraphNode.prototype) {
|
||||
// @ts-expect-error #576 This functionality is deprecated and should be removed.
|
||||
base_class.prototype[i] ||= LGraphNode.prototype[i]
|
||||
}
|
||||
|
||||
const prev = this.registered_node_types[type]
|
||||
if (prev && this.debug) {
|
||||
console.log('replacing node type:', type)
|
||||
}
|
||||
|
||||
this.registered_node_types[type] = base_class
|
||||
if (base_class.constructor.name) this.Nodes[classname] = base_class
|
||||
|
||||
this.onNodeTypeRegistered?.(type, base_class)
|
||||
if (prev) this.onNodeTypeReplaced?.(type, base_class, prev)
|
||||
|
||||
// warnings
|
||||
if (base_class.prototype.onPropertyChange)
|
||||
console.warn(
|
||||
`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`
|
||||
)
|
||||
|
||||
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
|
||||
if (this.auto_load_slot_types) new base_class(base_class.title || 'tmpnode')
|
||||
}
|
||||
|
||||
/**
|
||||
* removes a node type from the system
|
||||
* @param type name of the node or the node constructor itself
|
||||
*/
|
||||
unregisterNodeType(type: string | typeof LGraphNode): void {
|
||||
const base_class =
|
||||
typeof type === 'string' ? this.registered_node_types[type] : type
|
||||
if (!base_class) throw `node type not found: ${String(type)}`
|
||||
|
||||
delete this.registered_node_types[String(base_class.type)]
|
||||
|
||||
const name = base_class.constructor.name
|
||||
if (name) delete this.Nodes[name]
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a slot type and his node
|
||||
* @param type name of the node or the node constructor itself
|
||||
* @param slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
|
||||
*/
|
||||
registerNodeAndSlotType(
|
||||
type: LGraphNode,
|
||||
slot_type: ISlotType,
|
||||
out?: boolean
|
||||
): void {
|
||||
out ||= false
|
||||
const base_class =
|
||||
typeof type === 'string' &&
|
||||
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
|
||||
this.registered_node_types[type] !== 'anonymous'
|
||||
? this.registered_node_types[type]
|
||||
: type
|
||||
|
||||
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
|
||||
const class_type = base_class.constructor.type
|
||||
|
||||
let allTypes = []
|
||||
if (typeof slot_type === 'string') {
|
||||
allTypes = slot_type.split(',')
|
||||
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
|
||||
allTypes = ['_event_']
|
||||
} else {
|
||||
allTypes = ['*']
|
||||
}
|
||||
|
||||
for (let slotType of allTypes) {
|
||||
if (slotType === '') slotType = '*'
|
||||
|
||||
const register = out
|
||||
? this.registered_slot_out_types
|
||||
: this.registered_slot_in_types
|
||||
register[slotType] ??= { nodes: [] }
|
||||
|
||||
const { nodes } = register[slotType]
|
||||
if (!nodes.includes(class_type)) nodes.push(class_type)
|
||||
|
||||
// check if is a new type
|
||||
const types = out ? this.slot_types_out : this.slot_types_in
|
||||
const type = slotType.toLowerCase()
|
||||
|
||||
if (!types.includes(type)) {
|
||||
types.push(type)
|
||||
types.sort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all previously registered node's types
|
||||
*/
|
||||
clearRegisteredTypes(): void {
|
||||
this.registered_node_types = {}
|
||||
this.node_types_by_file_extension = {}
|
||||
this.Nodes = {}
|
||||
this.searchbox_extras = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a node of a given type with a name. The node is not attached to any graph yet.
|
||||
* @param type full name of the node class. p.e. "math/sin"
|
||||
* @param title a name to distinguish from other nodes
|
||||
* @param options to set options
|
||||
*/
|
||||
createNode(
|
||||
type: string,
|
||||
title?: string,
|
||||
options?: Dictionary<unknown>
|
||||
): LGraphNode | null {
|
||||
const base_class = this.registered_node_types[type]
|
||||
if (!base_class) {
|
||||
if (this.debug) console.log(`GraphNode type "${type}" not registered.`)
|
||||
return null
|
||||
}
|
||||
|
||||
title = title || base_class.title || type
|
||||
|
||||
let node = null
|
||||
|
||||
if (this.catch_exceptions) {
|
||||
try {
|
||||
node = new base_class(title)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
node = new base_class(title)
|
||||
}
|
||||
|
||||
node.type = type
|
||||
|
||||
if (!node.title && title) node.title = title
|
||||
node.properties ||= {}
|
||||
node.properties_info ||= []
|
||||
node.flags ||= {}
|
||||
// call onresize?
|
||||
node.size ||= node.computeSize()
|
||||
node.pos ||= [this.DEFAULT_POSITION[0], this.DEFAULT_POSITION[1]]
|
||||
node.mode ||= LGraphEventMode.ALWAYS
|
||||
|
||||
// extra options
|
||||
if (options) {
|
||||
for (const i in options) {
|
||||
// @ts-expect-error #577 Requires interface
|
||||
node[i] = options[i]
|
||||
}
|
||||
}
|
||||
|
||||
// callback
|
||||
node.onNodeCreated?.()
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a registered node type with a given name
|
||||
* @param type full name of the node class. p.e. "math/sin"
|
||||
* @returns the node class
|
||||
*/
|
||||
getNodeType(type: string): typeof LGraphNode {
|
||||
return this.registered_node_types[type]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of node types matching one category
|
||||
* @param category category name
|
||||
* @returns array with all the node classes
|
||||
*/
|
||||
getNodeTypesInCategory(category: string, filter?: string) {
|
||||
const r = []
|
||||
for (const i in this.registered_node_types) {
|
||||
const type = this.registered_node_types[i]
|
||||
if (type.filter != filter) continue
|
||||
|
||||
if (category == '') {
|
||||
if (type.category == null) r.push(type)
|
||||
} else if (type.category == category) {
|
||||
r.push(type)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list with all the node type categories
|
||||
* @param filter only nodes with ctor.filter equal can be shown
|
||||
* @returns array with all the names of the categories
|
||||
*/
|
||||
getNodeTypesCategories(filter?: string): string[] {
|
||||
const categories: Dictionary<number> = { '': 1 }
|
||||
for (const i in this.registered_node_types) {
|
||||
const type = this.registered_node_types[i]
|
||||
if (type.category && !type.skip_list) {
|
||||
if (type.filter != filter) continue
|
||||
|
||||
categories[type.category] = 1
|
||||
}
|
||||
}
|
||||
const result = []
|
||||
for (const i in categories) {
|
||||
result.push(i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// debug purposes: reloads all the js scripts that matches a wildcard
|
||||
reloadNodes(folder_wildcard: string): void {
|
||||
const tmp = document.getElementsByTagName('script')
|
||||
// weird, this array changes by its own, so we use a copy
|
||||
const script_files = []
|
||||
for (const element of tmp) {
|
||||
script_files.push(element)
|
||||
}
|
||||
|
||||
const docHeadObj = document.getElementsByTagName('head')[0]
|
||||
folder_wildcard = document.location.href + folder_wildcard
|
||||
|
||||
for (const script_file of script_files) {
|
||||
const src = script_file.src
|
||||
if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard)
|
||||
continue
|
||||
|
||||
try {
|
||||
if (this.debug) console.log('Reloading:', src)
|
||||
const dynamicScript = document.createElement('script')
|
||||
dynamicScript.type = 'text/javascript'
|
||||
dynamicScript.src = src
|
||||
docHeadObj.append(dynamicScript)
|
||||
script_file.remove()
|
||||
} catch (error) {
|
||||
if (this.throw_errors) throw error
|
||||
if (this.debug) console.log('Error while reloading', src)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.debug) console.log('Nodes reloaded')
|
||||
}
|
||||
|
||||
// separated just to improve if it doesn't work
|
||||
/** @deprecated Prefer {@link structuredClone} */
|
||||
cloneObject<T extends object | undefined | null>(
|
||||
obj: T,
|
||||
target?: T
|
||||
): WhenNullish<T, null> {
|
||||
if (obj == null) return null as WhenNullish<T, null>
|
||||
|
||||
const r = JSON.parse(JSON.stringify(obj))
|
||||
if (!target) return r
|
||||
|
||||
for (const i in r) {
|
||||
// @ts-expect-error deprecated
|
||||
target[i] = r[i]
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
/** @see {@link createUuidv4} @inheritdoc */
|
||||
uuidv4 = createUuidv4
|
||||
|
||||
/**
|
||||
* Returns if the types of two slots are compatible (taking into account wildcards, etc)
|
||||
* @param type_a output
|
||||
* @param type_b input
|
||||
* @returns true if they can be connected
|
||||
*/
|
||||
isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean {
|
||||
if (type_a == '' || type_a === '*') type_a = 0
|
||||
if (type_b == '' || type_b === '*') type_b = 0
|
||||
// If generic in/output, matching types (valid for triggers), or event/action types
|
||||
if (
|
||||
!type_a ||
|
||||
!type_b ||
|
||||
type_a == type_b ||
|
||||
(type_a == this.EVENT && type_b == this.ACTION)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Enforce string type to handle toLowerCase call (-1 number not ok)
|
||||
type_a = String(type_a)
|
||||
type_b = String(type_b)
|
||||
type_a = type_a.toLowerCase()
|
||||
type_b = type_b.toLowerCase()
|
||||
|
||||
// For nodes supporting multiple connection types
|
||||
if (!type_a.includes(',') && !type_b.includes(',')) return type_a == type_b
|
||||
|
||||
// Check all permutations to see if one is valid
|
||||
const supported_types_a = type_a.split(',')
|
||||
const supported_types_b = type_b.split(',')
|
||||
for (const a of supported_types_a) {
|
||||
for (const b of supported_types_b) {
|
||||
if (this.isValidConnection(a, b)) return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// used to create nodes from wrapping functions
|
||||
getParameterNames(func: (...args: any) => any): string[] {
|
||||
return String(func)
|
||||
.replaceAll(/\/\/.*$/gm, '') // strip single-line comments
|
||||
.replaceAll(/\s+/g, '') // strip white space
|
||||
.replaceAll(/\/\*[^*/]*\*\//g, '') // strip multi-line comments /**/
|
||||
.split('){', 1)[0]
|
||||
.replace(/^[^(]*\(/, '') // extract the parameters
|
||||
.replaceAll(/=[^,]+/g, '') // strip any ES6 defaults
|
||||
.split(',')
|
||||
.filter(Boolean) // split & filter [""]
|
||||
}
|
||||
|
||||
/* helper for interaction: pointer, touch, mouse Listeners
|
||||
used by LGraphCanvas DragAndScale ContextMenu */
|
||||
pointerListenerAdd(
|
||||
oDOM: Node,
|
||||
sEvIn: string,
|
||||
fCall: (e: Event) => boolean | void,
|
||||
capture = false
|
||||
): void {
|
||||
if (
|
||||
!oDOM ||
|
||||
!oDOM.addEventListener ||
|
||||
!sEvIn ||
|
||||
typeof fCall !== 'function'
|
||||
)
|
||||
return
|
||||
|
||||
let sMethod = this.pointerevents_method
|
||||
let sEvent = sEvIn
|
||||
|
||||
// UNDER CONSTRUCTION
|
||||
// convert pointerevents to touch event when not available
|
||||
if (sMethod == 'pointer' && !window.PointerEvent) {
|
||||
console.warn("sMethod=='pointer' && !window.PointerEvent")
|
||||
console.log(
|
||||
`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`
|
||||
)
|
||||
switch (sEvent) {
|
||||
case 'down': {
|
||||
sMethod = 'touch'
|
||||
sEvent = 'start'
|
||||
break
|
||||
}
|
||||
case 'move': {
|
||||
sMethod = 'touch'
|
||||
// sEvent = "move";
|
||||
break
|
||||
}
|
||||
case 'up': {
|
||||
sMethod = 'touch'
|
||||
sEvent = 'end'
|
||||
break
|
||||
}
|
||||
case 'cancel': {
|
||||
sMethod = 'touch'
|
||||
// sEvent = "cancel";
|
||||
break
|
||||
}
|
||||
case 'enter': {
|
||||
console.log('debug: Should I send a move event?') // ???
|
||||
break
|
||||
}
|
||||
// case "over": case "out": not used at now
|
||||
default: {
|
||||
console.warn(
|
||||
`PointerEvent not available in this browser ? The event ${sEvent} would not be called`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (sEvent) {
|
||||
// both pointer and move events
|
||||
case 'down':
|
||||
case 'up':
|
||||
case 'move':
|
||||
case 'over':
|
||||
case 'out':
|
||||
// @ts-expect-error - intentional fallthrough
|
||||
case 'enter': {
|
||||
oDOM.addEventListener(sMethod + sEvent, fCall, capture)
|
||||
}
|
||||
// only pointerevents
|
||||
// falls through
|
||||
case 'leave':
|
||||
case 'cancel':
|
||||
case 'gotpointercapture':
|
||||
// @ts-expect-error - intentional fallthrough
|
||||
case 'lostpointercapture': {
|
||||
if (sMethod != 'mouse') {
|
||||
return oDOM.addEventListener(sMethod + sEvent, fCall, capture)
|
||||
}
|
||||
}
|
||||
// not "pointer" || "mouse"
|
||||
// falls through
|
||||
default:
|
||||
return oDOM.addEventListener(sEvent, fCall, capture)
|
||||
}
|
||||
}
|
||||
|
||||
pointerListenerRemove(
|
||||
oDOM: Node,
|
||||
sEvent: string,
|
||||
fCall: (e: Event) => boolean | void,
|
||||
capture = false
|
||||
): void {
|
||||
if (
|
||||
!oDOM ||
|
||||
!oDOM.removeEventListener ||
|
||||
!sEvent ||
|
||||
typeof fCall !== 'function'
|
||||
)
|
||||
return
|
||||
|
||||
switch (sEvent) {
|
||||
// both pointer and move events
|
||||
case 'down':
|
||||
case 'up':
|
||||
case 'move':
|
||||
case 'over':
|
||||
case 'out':
|
||||
// @ts-expect-error - intentional fallthrough
|
||||
case 'enter': {
|
||||
if (
|
||||
this.pointerevents_method == 'pointer' ||
|
||||
this.pointerevents_method == 'mouse'
|
||||
) {
|
||||
oDOM.removeEventListener(
|
||||
this.pointerevents_method + sEvent,
|
||||
fCall,
|
||||
capture
|
||||
)
|
||||
}
|
||||
}
|
||||
// only pointerevents
|
||||
// falls through
|
||||
case 'leave':
|
||||
case 'cancel':
|
||||
case 'gotpointercapture':
|
||||
// @ts-expect-error - intentional fallthrough
|
||||
case 'lostpointercapture': {
|
||||
if (this.pointerevents_method == 'pointer') {
|
||||
return oDOM.removeEventListener(
|
||||
this.pointerevents_method + sEvent,
|
||||
fCall,
|
||||
capture
|
||||
)
|
||||
}
|
||||
}
|
||||
// not "pointer" || "mouse"
|
||||
// falls through
|
||||
default:
|
||||
return oDOM.removeEventListener(sEvent, fCall, capture)
|
||||
}
|
||||
}
|
||||
|
||||
getTime(): number {
|
||||
return performance.now()
|
||||
}
|
||||
|
||||
distance = distance
|
||||
|
||||
colorToString(c: [number, number, number, number]): string {
|
||||
return `rgba(${Math.round(c[0] * 255).toFixed()},${Math.round(
|
||||
c[1] * 255
|
||||
).toFixed()},${Math.round(c[2] * 255).toFixed()},${
|
||||
c.length == 4 ? c[3].toFixed(2) : '1.0'
|
||||
})`
|
||||
}
|
||||
|
||||
isInsideRectangle = isInsideRectangle
|
||||
|
||||
// [minx,miny,maxx,maxy]
|
||||
growBounding(bounding: Rect, x: number, y: number): void {
|
||||
if (x < bounding[0]) {
|
||||
bounding[0] = x
|
||||
} else if (x > bounding[2]) {
|
||||
bounding[2] = x
|
||||
}
|
||||
|
||||
if (y < bounding[1]) {
|
||||
bounding[1] = y
|
||||
} else if (y > bounding[3]) {
|
||||
bounding[3] = y
|
||||
}
|
||||
}
|
||||
|
||||
overlapBounding = overlapBounding
|
||||
|
||||
// point inside bounding box
|
||||
isInsideBounding(p: number[], bb: number[][]): boolean {
|
||||
if (
|
||||
p[0] < bb[0][0] ||
|
||||
p[1] < bb[0][1] ||
|
||||
p[0] > bb[1][0] ||
|
||||
p[1] > bb[1][1]
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Convert a hex value to its decimal value - the inputted hex must be in the
|
||||
// format of a hex triplet - the kind we use for HTML colours. The function
|
||||
// will return an array with three values.
|
||||
hex2num(hex: string): number[] {
|
||||
if (hex.charAt(0) == '#') {
|
||||
hex = hex.slice(1)
|
||||
// Remove the '#' char - if there is one.
|
||||
}
|
||||
hex = hex.toUpperCase()
|
||||
const hex_alphabets = '0123456789ABCDEF'
|
||||
const value = new Array(3)
|
||||
let k = 0
|
||||
let int1, int2
|
||||
for (let i = 0; i < 6; i += 2) {
|
||||
int1 = hex_alphabets.indexOf(hex.charAt(i))
|
||||
int2 = hex_alphabets.indexOf(hex.charAt(i + 1))
|
||||
value[k] = int1 * 16 + int2
|
||||
k++
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Give a array with three values as the argument and the function will return
|
||||
// the corresponding hex triplet.
|
||||
num2hex(triplet: number[]): string {
|
||||
const hex_alphabets = '0123456789ABCDEF'
|
||||
let hex = '#'
|
||||
let int1, int2
|
||||
for (let i = 0; i < 3; i++) {
|
||||
int1 = triplet[i] / 16
|
||||
int2 = triplet[i] % 16
|
||||
|
||||
hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2)
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
closeAllContextMenus(ref_window: Window = window): void {
|
||||
const elements = [
|
||||
...ref_window.document.querySelectorAll('.litecontextmenu')
|
||||
]
|
||||
if (!elements.length) return
|
||||
|
||||
for (const element of elements) {
|
||||
if ('close' in element && typeof element.close === 'function') {
|
||||
element.close()
|
||||
} else {
|
||||
element.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extendClass(target: any, origin: any): void {
|
||||
for (const i in origin) {
|
||||
// copy class properties
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (target.hasOwnProperty(i)) continue
|
||||
target[i] = origin[i]
|
||||
}
|
||||
|
||||
if (origin.prototype) {
|
||||
// copy prototype properties
|
||||
for (const i in origin.prototype) {
|
||||
// only enumerable
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (!origin.prototype.hasOwnProperty(i)) continue
|
||||
|
||||
// avoid overwriting existing ones
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (target.prototype.hasOwnProperty(i)) continue
|
||||
|
||||
// copy getters
|
||||
if (origin.prototype.__lookupGetter__(i)) {
|
||||
target.prototype.__defineGetter__(
|
||||
i,
|
||||
origin.prototype.__lookupGetter__(i)
|
||||
)
|
||||
} else {
|
||||
target.prototype[i] = origin.prototype[i]
|
||||
}
|
||||
|
||||
// and setters
|
||||
if (origin.prototype.__lookupSetter__(i)) {
|
||||
target.prototype.__defineSetter__(
|
||||
i,
|
||||
origin.prototype.__lookupSetter__(i)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user