From a8d6650827f175cf4803fa430cce8672a0175b31 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Wed, 3 Sep 2025 14:43:06 +0200 Subject: [PATCH 01/33] initial setup, wip --- packages/tdev/nand-game/index.ts | 37 +++++++ packages/tdev/nand-game/model.ts | 139 ++++++++++++++++++++++++++ packages/tdev/nand-game/package.json | 18 ++++ packages/tdev/nand-game/tsconfig.json | 3 + src/api/document.ts | 12 ++- src/stores/DocumentStore.ts | 3 + 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 packages/tdev/nand-game/index.ts create mode 100644 packages/tdev/nand-game/model.ts create mode 100644 packages/tdev/nand-game/package.json create mode 100644 packages/tdev/nand-game/tsconfig.json diff --git a/packages/tdev/nand-game/index.ts b/packages/tdev/nand-game/index.ts new file mode 100644 index 000000000..ea45bd1a9 --- /dev/null +++ b/packages/tdev/nand-game/index.ts @@ -0,0 +1,37 @@ +import type { PluginConfig } from '@docusaurus/types'; +import path from 'path'; + +export const excalidrawPluginConfig: PluginConfig = () => { + return { + name: 'excalidraw-config', + configureWebpack(config, isServer, { currentBundler }) { + const cwd = process.cwd(); + return { + module: { + rules: [ + { + test: /\.excalidraw$/, + type: 'json' + }, + { + test: /\.excalidrawlib$/, + type: 'json' + } + ] + }, + resolve: { + fallback: { + 'roughjs/bin/math': path.resolve(cwd, './node_modules/roughjs/bin/math.js'), + 'roughjs/bin/rough': path.resolve(cwd, './node_modules/roughjs/bin/rough.js'), + 'roughjs/bin/generator': path.resolve(cwd, './node_modules/roughjs/bin/generator.js') + } + }, + plugins: [ + new currentBundler.instance.DefinePlugin({ + 'process.env.IS_PREACT': JSON.stringify('false') + }) + ] + }; + } + }; +}; diff --git a/packages/tdev/nand-game/model.ts b/packages/tdev/nand-game/model.ts new file mode 100644 index 000000000..991a657f7 --- /dev/null +++ b/packages/tdev/nand-game/model.ts @@ -0,0 +1,139 @@ +import { action, computed, observable } from 'mobx'; +import iDocument, { Source } from '@tdev-models/iDocument'; +import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { TypeMeta } from '@tdev-models/DocumentRoot'; +import type { exportToBlob } from '@excalidraw/excalidraw'; +import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; +import type { BinaryFiles } from '@excalidraw/excalidraw/types'; +type ExportToBlobArgs = Parameters[0]; +type ExportToBlobReturn = ReturnType; +type ExportToBlob = (args: ExportToBlobArgs) => Promise; + +export interface MetaInit { + readonly?: boolean; + defaultFiles?: BinaryFiles; + defaultElements?: readonly ExcalidrawElement[]; + defaultImage?: string; +} + +export class ModelMeta extends TypeMeta { + readonly type = DocumentType.Excalidoc; + readonly defaultElements: readonly ExcalidrawElement[]; + readonly defaultFiles: BinaryFiles; + readonly defaultImage: string; + + constructor(props: Partial) { + super(DocumentType.Excalidoc, props.readonly ? Access.RO_User : undefined); + this.defaultElements = props.defaultElements || []; + this.defaultFiles = props.defaultFiles || {}; + this.defaultImage = props.defaultImage || ''; + } + + get defaultData(): TypeDataMapping[DocumentType.Excalidoc] { + return { + elements: this.defaultElements, + files: this.defaultFiles, + image: this.defaultImage + }; + } +} + +const blobToBase64 = (blob: Blob): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); +}; + +class Node extends iDocument { + @observable.ref accessor elements: readonly ExcalidrawElement[]; + @observable.ref accessor files: BinaryFiles; + @observable.ref accessor image: string; + constructor(props: DocumentProps, store: DocumentStore) { + super(props, store); + this.elements = props.data.elements || []; + this.files = props.data.files || {}; + this.image = props.data.image || ''; + } + + @action + setData( + data: TypeDataMapping[DocumentType.Excalidoc], + from: Source, + updatedAt?: Date, + lib?: { exportToBlob: ExportToBlob } | null + ): void { + if (from === Source.LOCAL) { + /** + * Assumption: + * - local changes are commited only when the scene version is updated! + * - only non-deleted elements are commited + */ + const updatedElements = data.elements; + const presentFileIds = new Set( + updatedElements.map((element) => (element.type === 'image' ? element.fileId : null)) + ); + const updatedFiles: BinaryFiles = { ...data.files }; + Object.entries(data.files).forEach(([fileId, file]) => { + if (!presentFileIds.has(file.id)) { + delete updatedFiles[fileId]; + } + }); + this.elements = updatedElements; + this.files = updatedFiles; + this.save(async () => { + if (!lib) { + return; + } + return lib + .exportToBlob({ + elements: updatedElements, + files: updatedFiles, + mimeType: 'image/webp', + quality: 0.92 + }) + .then((blob) => { + return blobToBase64(blob); + }) + .then( + action((base64String) => { + if (typeof base64String === 'string') { + this.image = base64String; + } + }) + ) + .catch((e) => { + console.warn('Failed to export excalidraw to blob', e); + }); + }); + } else { + this.elements = data.elements; + this.files = data.files; + this.image = data.image; + } + if (updatedAt) { + this.updatedAt = new Date(updatedAt); + } + } + + get data(): TypeDataMapping[DocumentType.Excalidoc] { + return { + elements: this.elements, + files: this.files, + image: this.image + }; + } + + @computed + get meta(): ModelMeta { + if (this.root?.type === DocumentType.Excalidoc) { + return this.root.meta as ModelMeta; + } + return new ModelMeta({}); + } +} + +export default Node; diff --git a/packages/tdev/nand-game/package.json b/packages/tdev/nand-game/package.json new file mode 100644 index 000000000..d3dff84d4 --- /dev/null +++ b/packages/tdev/nand-game/package.json @@ -0,0 +1,18 @@ +{ + "name": "@tdev/nand-game", + "version": "1.0.0", + "main": "index.ts", + "types": "index.ts", + "dependencies": { + "reactflow": "^11.11.4" + }, + "devDependencies": { + "vitest": "*", + "@docusaurus/module-type-aliases": "*", + "@docusaurus/core": "*", + "@types/node": "*" + }, + "peerDependencies": { + "@tdev/core": "1.0.0" + } +} \ No newline at end of file diff --git a/packages/tdev/nand-game/tsconfig.json b/packages/tdev/nand-game/tsconfig.json new file mode 100644 index 000000000..ea56794f8 --- /dev/null +++ b/packages/tdev/nand-game/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.json" +} diff --git a/src/api/document.ts b/src/api/document.ts index fe269258e..75c73296a 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -21,6 +21,7 @@ import type { BinaryFiles } from '@excalidraw/excalidraw/types'; import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; import Excalidoc from '@tdev/excalidoc/model'; import ProgressState from '@tdev-models/documents/ProgressState'; +import Node from '@tdev/nand-game/model'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -51,7 +52,8 @@ export enum DocumentType { TextMessage = 'text_message', DynamicDocumentRoot = 'dynamic_document_root', DynamicDocumentRoots = 'dynamic_document_roots', - NetpbmGraphic = 'netpbm_graphic' + NetpbmGraphic = 'netpbm_graphic', + Node = 'node' } /** @@ -106,6 +108,11 @@ export interface ExcaliData { image: string; } +export interface NodeData { + inputs: string[]; + outputs: string[]; +} + export type StateType = | 'checked' | 'question' @@ -174,6 +181,7 @@ export interface TypeDataMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootData; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRootsData; [DocumentType.NetpbmGraphic]: NetpbmGraphicData; + [DocumentType.Node]: NodeData; // Add more mappings as needed } @@ -195,6 +203,8 @@ export interface TypeModelMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootModel; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRoots; [DocumentType.NetpbmGraphic]: NetpbmGraphic; + [DocumentType.Node]: Node; + /** * Add more mappings as needed * TODO: implement the mapping in DocumentRoot.ts diff --git a/src/stores/DocumentStore.ts b/src/stores/DocumentStore.ts index 96bf3e455..73a309183 100644 --- a/src/stores/DocumentStore.ts +++ b/src/stores/DocumentStore.ts @@ -37,6 +37,7 @@ import { DynamicDocumentRootModel } from '@tdev-models/documents/DynamicDocument import NetpbmGraphic from '@tdev-models/documents/NetpbmGraphic'; import Excalidoc from '@tdev/excalidoc/model'; import ProgressState from '@tdev-models/documents/ProgressState'; +import Node from '@tdev/nand-game/model'; const IsNotUniqueError = (error: any) => { try { @@ -91,6 +92,8 @@ export function CreateDocumentModel(data: DocumentProps, store: Do return new NetpbmGraphic(data as DocumentProps, store); case DocumentType.ProgressState: return new ProgressState(data as DocumentProps, store); + case DocumentType.Node: + return new Node(data as DocumentProps, store); } } class DocumentStore extends iStore<`delete-${string}`> { From dc005ab42e583dc82d2fe1c38429daf7e2fba8ab Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 8 Sep 2025 11:10:45 +0200 Subject: [PATCH 02/33] setup flow nodes --- packages/tdev/nand-game/model.ts | 139 ---------- packages/tdev/nand-game/models/Flow.ts | 51 ++++ packages/tdev/nand-game/models/FlowNode.ts | 68 +++++ src/api/document.ts | 20 +- src/stores/DocumentStore.ts | 9 +- yarn.lock | 293 ++++++++++++++++++++- 6 files changed, 426 insertions(+), 154 deletions(-) delete mode 100644 packages/tdev/nand-game/model.ts create mode 100644 packages/tdev/nand-game/models/Flow.ts create mode 100644 packages/tdev/nand-game/models/FlowNode.ts diff --git a/packages/tdev/nand-game/model.ts b/packages/tdev/nand-game/model.ts deleted file mode 100644 index 991a657f7..000000000 --- a/packages/tdev/nand-game/model.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { action, computed, observable } from 'mobx'; -import iDocument, { Source } from '@tdev-models/iDocument'; -import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; -import DocumentStore from '@tdev-stores/DocumentStore'; -import { TypeMeta } from '@tdev-models/DocumentRoot'; -import type { exportToBlob } from '@excalidraw/excalidraw'; -import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; -import type { BinaryFiles } from '@excalidraw/excalidraw/types'; -type ExportToBlobArgs = Parameters[0]; -type ExportToBlobReturn = ReturnType; -type ExportToBlob = (args: ExportToBlobArgs) => Promise; - -export interface MetaInit { - readonly?: boolean; - defaultFiles?: BinaryFiles; - defaultElements?: readonly ExcalidrawElement[]; - defaultImage?: string; -} - -export class ModelMeta extends TypeMeta { - readonly type = DocumentType.Excalidoc; - readonly defaultElements: readonly ExcalidrawElement[]; - readonly defaultFiles: BinaryFiles; - readonly defaultImage: string; - - constructor(props: Partial) { - super(DocumentType.Excalidoc, props.readonly ? Access.RO_User : undefined); - this.defaultElements = props.defaultElements || []; - this.defaultFiles = props.defaultFiles || {}; - this.defaultImage = props.defaultImage || ''; - } - - get defaultData(): TypeDataMapping[DocumentType.Excalidoc] { - return { - elements: this.defaultElements, - files: this.defaultFiles, - image: this.defaultImage - }; - } -} - -const blobToBase64 = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.onerror = () => reject(reader.error); - reader.readAsDataURL(blob); - }); -}; - -class Node extends iDocument { - @observable.ref accessor elements: readonly ExcalidrawElement[]; - @observable.ref accessor files: BinaryFiles; - @observable.ref accessor image: string; - constructor(props: DocumentProps, store: DocumentStore) { - super(props, store); - this.elements = props.data.elements || []; - this.files = props.data.files || {}; - this.image = props.data.image || ''; - } - - @action - setData( - data: TypeDataMapping[DocumentType.Excalidoc], - from: Source, - updatedAt?: Date, - lib?: { exportToBlob: ExportToBlob } | null - ): void { - if (from === Source.LOCAL) { - /** - * Assumption: - * - local changes are commited only when the scene version is updated! - * - only non-deleted elements are commited - */ - const updatedElements = data.elements; - const presentFileIds = new Set( - updatedElements.map((element) => (element.type === 'image' ? element.fileId : null)) - ); - const updatedFiles: BinaryFiles = { ...data.files }; - Object.entries(data.files).forEach(([fileId, file]) => { - if (!presentFileIds.has(file.id)) { - delete updatedFiles[fileId]; - } - }); - this.elements = updatedElements; - this.files = updatedFiles; - this.save(async () => { - if (!lib) { - return; - } - return lib - .exportToBlob({ - elements: updatedElements, - files: updatedFiles, - mimeType: 'image/webp', - quality: 0.92 - }) - .then((blob) => { - return blobToBase64(blob); - }) - .then( - action((base64String) => { - if (typeof base64String === 'string') { - this.image = base64String; - } - }) - ) - .catch((e) => { - console.warn('Failed to export excalidraw to blob', e); - }); - }); - } else { - this.elements = data.elements; - this.files = data.files; - this.image = data.image; - } - if (updatedAt) { - this.updatedAt = new Date(updatedAt); - } - } - - get data(): TypeDataMapping[DocumentType.Excalidoc] { - return { - elements: this.elements, - files: this.files, - image: this.image - }; - } - - @computed - get meta(): ModelMeta { - if (this.root?.type === DocumentType.Excalidoc) { - return this.root.meta as ModelMeta; - } - return new ModelMeta({}); - } -} - -export default Node; diff --git a/packages/tdev/nand-game/models/Flow.ts b/packages/tdev/nand-game/models/Flow.ts new file mode 100644 index 000000000..676403f85 --- /dev/null +++ b/packages/tdev/nand-game/models/Flow.ts @@ -0,0 +1,51 @@ +import { action, computed, observable } from 'mobx'; +import iDocument, { Source } from '@tdev-models/iDocument'; +import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { TypeMeta } from '@tdev-models/DocumentRoot'; + +export interface MetaInit { + readonly?: boolean; +} + +export class ModelMeta extends TypeMeta { + readonly type = DocumentType.Flow; + + constructor(props: Partial) { + super(DocumentType.Flow, props.readonly ? Access.RO_User : undefined); + } + + get defaultData(): TypeDataMapping[DocumentType.Flow] { + return {}; + } +} + +class Flow extends iDocument { + constructor(props: DocumentProps, store: DocumentStore) { + super(props, store); + } + + @action + setData(data: TypeDataMapping[DocumentType.Flow], from: Source, updatedAt?: Date): void { + if (from === Source.LOCAL) { + // this.save(); + } + if (updatedAt) { + this.updatedAt = new Date(updatedAt); + } + } + + get data(): TypeDataMapping[DocumentType.Flow] { + return {}; + } + + @computed + get meta(): ModelMeta { + if (this.root?.type === DocumentType.Flow) { + return this.root.meta as ModelMeta; + } + return new ModelMeta({}); + } +} + +export default Flow; diff --git a/packages/tdev/nand-game/models/FlowNode.ts b/packages/tdev/nand-game/models/FlowNode.ts new file mode 100644 index 000000000..95a344e3f --- /dev/null +++ b/packages/tdev/nand-game/models/FlowNode.ts @@ -0,0 +1,68 @@ +import { action, computed, observable } from 'mobx'; +import iDocument, { Source } from '@tdev-models/iDocument'; +import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { TypeMeta } from '@tdev-models/DocumentRoot'; + +export interface MetaInit { + readonly?: boolean; +} + +export class ModelMeta extends TypeMeta { + readonly type = DocumentType.FlowNode; + + constructor(props: Partial) { + super(DocumentType.FlowNode, props.readonly ? Access.RO_User : undefined); + } + + get defaultData(): TypeDataMapping[DocumentType.FlowNode] { + return { + inputs: [], + outputs: [] + }; + } +} + +class FlowNode extends iDocument { + @observable.ref accessor inputs: string[]; + @observable.ref accessor outputs: string[]; + constructor(props: DocumentProps, store: DocumentStore) { + super(props, store); + this.inputs = props.data.inputs || []; + this.outputs = props.data.outputs || []; + } + + @action + setData(data: TypeDataMapping[DocumentType.FlowNode], from: Source, updatedAt?: Date): void { + this.inputs = [...data.inputs]; + this.outputs = [...data.outputs]; + if (from === Source.LOCAL) { + /** + * Assumption: + * - local changes are commited only when the scene version is updated! + * - only non-deleted elements are commited + */ + this.save(); + } + if (updatedAt) { + this.updatedAt = new Date(updatedAt); + } + } + + get data(): TypeDataMapping[DocumentType.FlowNode] { + return { + inputs: this.inputs, + outputs: this.outputs + }; + } + + @computed + get meta(): ModelMeta { + if (this.root?.type === DocumentType.FlowNode) { + return this.root.meta as ModelMeta; + } + return new ModelMeta({}); + } +} + +export default FlowNode; diff --git a/src/api/document.ts b/src/api/document.ts index 75c73296a..d7f94d9fb 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -21,7 +21,8 @@ import type { BinaryFiles } from '@excalidraw/excalidraw/types'; import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; import Excalidoc from '@tdev/excalidoc/model'; import ProgressState from '@tdev-models/documents/ProgressState'; -import Node from '@tdev/nand-game/model'; +import FlowNode from '@tdev/nand-game/models/FlowNode'; +import Flow from '@tdev/nand-game/models/Flow'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -53,7 +54,8 @@ export enum DocumentType { DynamicDocumentRoot = 'dynamic_document_root', DynamicDocumentRoots = 'dynamic_document_roots', NetpbmGraphic = 'netpbm_graphic', - Node = 'node' + Flow = 'react_flow', + FlowNode = 'flow_node' } /** @@ -108,7 +110,9 @@ export interface ExcaliData { image: string; } -export interface NodeData { +export interface FlowData {} + +export interface FlowNodeData { inputs: string[]; outputs: string[]; } @@ -181,7 +185,8 @@ export interface TypeDataMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootData; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRootsData; [DocumentType.NetpbmGraphic]: NetpbmGraphicData; - [DocumentType.Node]: NodeData; + [DocumentType.Flow]: FlowData; + [DocumentType.FlowNode]: FlowNodeData; // Add more mappings as needed } @@ -203,7 +208,8 @@ export interface TypeModelMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootModel; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRoots; [DocumentType.NetpbmGraphic]: NetpbmGraphic; - [DocumentType.Node]: Node; + [DocumentType.Flow]: Flow; + [DocumentType.FlowNode]: FlowNode; /** * Add more mappings as needed @@ -230,7 +236,9 @@ export type DocumentTypes = | DynamicDocumentRootModel | DynamicDocumentRoots | NetpbmGraphic - | ProgressState; + | ProgressState + | Flow + | FlowNode; export interface Document { id: string; diff --git a/src/stores/DocumentStore.ts b/src/stores/DocumentStore.ts index 73a309183..19368effa 100644 --- a/src/stores/DocumentStore.ts +++ b/src/stores/DocumentStore.ts @@ -37,7 +37,8 @@ import { DynamicDocumentRootModel } from '@tdev-models/documents/DynamicDocument import NetpbmGraphic from '@tdev-models/documents/NetpbmGraphic'; import Excalidoc from '@tdev/excalidoc/model'; import ProgressState from '@tdev-models/documents/ProgressState'; -import Node from '@tdev/nand-game/model'; +import FlowNode from '@tdev/nand-game/models/FlowNode'; +import Flow from '@tdev/nand-game/models/Flow'; const IsNotUniqueError = (error: any) => { try { @@ -92,8 +93,10 @@ export function CreateDocumentModel(data: DocumentProps, store: Do return new NetpbmGraphic(data as DocumentProps, store); case DocumentType.ProgressState: return new ProgressState(data as DocumentProps, store); - case DocumentType.Node: - return new Node(data as DocumentProps, store); + case DocumentType.Flow: + return new Flow(data as DocumentProps, store); + case DocumentType.FlowNode: + return new FlowNode(data as DocumentProps, store); } } class DocumentStore extends iStore<`delete-${string}`> { diff --git a/yarn.lock b/yarn.lock index 8f311ebdf..fabc90f84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4590,6 +4590,72 @@ resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== +"@reactflow/background@11.3.14": + version "11.3.14" + resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.14.tgz#778ca30174f3de77fc321459ab3789e66e71a699" + integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA== + dependencies: + "@reactflow/core" "11.11.4" + classcat "^5.0.3" + zustand "^4.4.1" + +"@reactflow/controls@11.2.14": + version "11.2.14" + resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.14.tgz#508ed2c40d23341b3b0919dd11e76fd49cf850c7" + integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw== + dependencies: + "@reactflow/core" "11.11.4" + classcat "^5.0.3" + zustand "^4.4.1" + +"@reactflow/core@11.11.4": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.4.tgz#89bd86d1862aa1416f3f49926cede7e8c2aab6a7" + integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q== + dependencies: + "@types/d3" "^7.4.0" + "@types/d3-drag" "^3.0.1" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.4.1" + +"@reactflow/minimap@11.7.14": + version "11.7.14" + resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.14.tgz#298d7a63cb1da06b2518c99744f716560c88ca73" + integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ== + dependencies: + "@reactflow/core" "11.11.4" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.4.1" + +"@reactflow/node-resizer@2.2.14": + version "2.2.14" + resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz#1810c0ce51aeb936f179466a6660d1e02c7a77a8" + integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA== + dependencies: + "@reactflow/core" "11.11.4" + classcat "^5.0.4" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + zustand "^4.4.1" + +"@reactflow/node-toolbar@1.3.14": + version "1.3.14" + resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz#c6ffc76f82acacdce654f2160dc9852162d6e7c9" + integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ== + dependencies: + "@reactflow/core" "11.11.4" + classcat "^5.0.3" + zustand "^4.4.1" + "@rollup/rollup-android-arm-eabi@4.49.0": version "4.49.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48" @@ -5424,23 +5490,216 @@ dependencies: "@types/node" "*" -"@types/d3-scale-chromatic@^3.0.0": +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2" + integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== + +"@types/d3-drag@*", "@types/d3-drag@^3.0.1": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*", "@types/d3-scale-chromatic@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== -"@types/d3-scale@^4.0.3": +"@types/d3-scale@*", "@types/d3-scale@^4.0.3": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" +"@types/d3-selection@*", "@types/d3-selection@^3.0.3": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + "@types/d3-time@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.0": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -5540,6 +5799,11 @@ "@types/jsonfile" "*" "@types/node" "*" +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/gtag.js@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" @@ -6931,6 +7195,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +classcat@^5.0.3, classcat@^5.0.4: + version "5.0.5" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" + integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== + classnames@^2.2.5, classnames@^2.3.2: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" @@ -7589,7 +7858,7 @@ d3-delaunay@6: resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== -"d3-drag@2 - 3", d3-drag@3: +"d3-drag@2 - 3", d3-drag@3, d3-drag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== @@ -7703,7 +7972,7 @@ d3-scale@4: d3-time "2.1.1 - 3" d3-time-format "2 - 4" -"d3-selection@2 - 3", d3-selection@3: +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== @@ -7752,7 +8021,7 @@ d3-shape@^1.2.0: d3-interpolate "1 - 3" d3-timer "1 - 3" -d3-zoom@3: +d3-zoom@3, d3-zoom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== @@ -13226,6 +13495,18 @@ react@^19.1.0: resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== +reactflow@^11.11.4: + version "11.11.4" + resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653" + integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og== + dependencies: + "@reactflow/background" "11.3.14" + "@reactflow/controls" "11.2.14" + "@reactflow/core" "11.11.4" + "@reactflow/minimap" "11.7.14" + "@reactflow/node-resizer" "2.2.14" + "@reactflow/node-toolbar" "1.3.14" + reactjs-popup@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/reactjs-popup/-/reactjs-popup-2.0.6.tgz#ff0e340e31d03785ae62f6b6cf62de1ef48c9605" @@ -15403,7 +15684,7 @@ zip-stream@^4.1.0: compress-commons "^4.1.2" readable-stream "^3.6.0" -zustand@^4.3.2: +zustand@^4.3.2, zustand@^4.4.1: version "4.5.7" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== From f8ff84094bc384f34eb734db837071dced71371f Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 8 Sep 2025 12:34:23 +0200 Subject: [PATCH 03/33] UPDATE INPUT STYLES --- src/components/shared/SelectInput/index.tsx | 47 ++++++++++++++----- .../shared/SelectInput/styles.module.scss | 19 ++++---- src/components/shared/TextInput/index.tsx | 4 ++ .../shared/TextInput/styles.module.scss | 6 +-- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/components/shared/SelectInput/index.tsx b/src/components/shared/SelectInput/index.tsx index cdfed749b..949c59351 100644 --- a/src/components/shared/SelectInput/index.tsx +++ b/src/components/shared/SelectInput/index.tsx @@ -3,23 +3,47 @@ import styles from './styles.module.scss'; import { observer } from 'mobx-react-lite'; import clsx from 'clsx'; -interface Props { +interface Option { + value: string; + label?: string; + disabled?: boolean; +} + +interface BaseProps { defaultValue?: string; placeholder?: string; - /** - * provide the labels in the same order as the options - * to display them in the dropdown - */ - labels?: string[]; onChange: (text: string) => void; - options: string[]; value: string; disabled?: boolean; className?: string; } +interface SimpleProps extends BaseProps { + labels?: string[]; + options: string[]; +} + +interface CustomizableProps extends BaseProps { + labels?: never[]; + options: Option[]; +} + +type Props = SimpleProps | CustomizableProps; + const SelectInput = observer((props: Props) => { - const { options, onChange, value } = props; + const { onChange, value } = props; + const options = React.useMemo(() => { + if (props.options.length === 0) { + return []; + } + if (typeof props.options[0] === 'string') { + return (props.options as string[]).map((o, idx) => ({ + value: o as string, + label: props.labels?.[idx] ?? o + })); + } + return props.options as Option[]; + }, [props.options, props.labels]); return ( diff --git a/src/components/shared/SelectInput/styles.module.scss b/src/components/shared/SelectInput/styles.module.scss index 58159cec1..f0b2129be 100644 --- a/src/components/shared/SelectInput/styles.module.scss +++ b/src/components/shared/SelectInput/styles.module.scss @@ -1,20 +1,23 @@ .dropdown { - appearance: none; border: 1px solid var(--ifm-color-gray-500); - border-radius: 4px; - padding: var(--ifm-button-padding-vertical); - font-size: var(--ifm-font-size-base); - color: #333; + border-radius: var(--ifm-button-border-radius); + padding: 3.5px; + font-size: calc(0.9 * var(--ifm-font-size-base)); + color: var(--ifm-font-color-base); cursor: pointer; width: auto; - min-width: 0; - max-width: 300px; + max-width: 90vw; transition: all 0.3s ease; + z-index: var(--ifm-z-index-dropdown); + box-shadow: var(--ifm-global-shadow-ld); + transition: + border-color 0.3s, + box-shadow 0.3s; &:focus { border-color: var(--ifm-link-hover-color); outline: none; - box-shadow: 0 0 3px var(--ifm-link-hover-color); + box-shadow: 0 0 2px var(--ifm-link-hover-color); } option { diff --git a/src/components/shared/TextInput/index.tsx b/src/components/shared/TextInput/index.tsx index f3ba58e79..df37df041 100644 --- a/src/components/shared/TextInput/index.tsx +++ b/src/components/shared/TextInput/index.tsx @@ -9,6 +9,7 @@ interface Props { onChange: (text: string) => void; onEnter?: () => void; onEscape?: () => void; + onSave?: () => void; className?: string; labelClassName?: string; value?: string; @@ -56,6 +57,9 @@ const TextInput = observer((props: Props) => { if (e.key === 'Escape') { props.onEscape?.(); } + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + props.onSave?.(); + } }} autoFocus={!props.noAutoFocus} autoComplete="off" diff --git a/src/components/shared/TextInput/styles.module.scss b/src/components/shared/TextInput/styles.module.scss index cf9dd796c..4c23f810b 100644 --- a/src/components/shared/TextInput/styles.module.scss +++ b/src/components/shared/TextInput/styles.module.scss @@ -1,17 +1,17 @@ .textInput { width: 100%; - padding: var(--ifm-button-padding-vertical); + padding: 3px; border: 1px solid var(--ifm-color-gray-500); border-radius: var(--ifm-global-radius); font-size: var(--ifm-font-size-base); - box-shadow: var(--ifm-global-shadow-md); + box-shadow: var(--ifm-global-shadow-ld); transition: border-color 0.3s, box-shadow 0.3s; &:focus { border-color: var(--ifm-link-hover-color); outline: none; - box-shadow: 0 0 8px var(--ifm-link-hover-color); + box-shadow: 0 0 2px var(--ifm-link-hover-color); } } .label { From 584833176f5de33e9ebcf3e622bae1d4e8cac0fd Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 8 Sep 2025 12:43:37 +0200 Subject: [PATCH 04/33] update dynamic docs --- src/components/PermissionsPanel/index.tsx | 4 +- .../AddDynamicDocumentRoot/index.tsx | 9 +- .../DynamicDocumentRoot/index.tsx | 85 ++++++++++--------- .../RoomTypeSelector/index.tsx | 51 ++++++----- .../shared/SelectInput/styles.module.scss | 2 +- .../shared/TextInput/styles.module.scss | 2 +- src/hooks/useFirstMainDocument.ts | 2 +- 7 files changed, 86 insertions(+), 69 deletions(-) diff --git a/src/components/PermissionsPanel/index.tsx b/src/components/PermissionsPanel/index.tsx index 6d91fc1f3..7a49437ff 100644 --- a/src/components/PermissionsPanel/index.tsx +++ b/src/components/PermissionsPanel/index.tsx @@ -44,7 +44,9 @@ const MissingPermissionsBadge = ({ available, total }: { available: number; tota const PermissionsPanel = observer((props: Props) => { const { documentRootId, documentRootIds, position } = props; - const docRootIds = documentRootIds || [documentRootId]; + const docRootIds = React.useMemo(() => { + return documentRootIds || [documentRootId]; + }, [documentRootId, documentRootIds]); const [isOpen, setIsOpen] = React.useState(false); const userStore = useStore('userStore'); const documentRootStore = useStore('documentRootStore'); diff --git a/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx b/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx index 8c4f5362b..09cf8fb4d 100644 --- a/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx @@ -16,10 +16,15 @@ const AddDynamicDocumentRoot = observer((props: Props) => { const { dynamicDocumentRoots } = props; const userStore = useStore('userStore'); const user = userStore.current; + const permissionStore = useStore('permissionStore'); + React.useEffect(() => { + if (dynamicDocumentRoots.root) { + permissionStore.loadPermissions(dynamicDocumentRoots.root); + } + }, [dynamicDocumentRoots]); if (!user || !user.hasElevatedAccess) { return null; } - return (
+ + + ); +}); +export default Circuit; diff --git a/packages/tdev/circuit/components/styles.module.scss b/packages/tdev/circuit/components/styles.module.scss new file mode 100644 index 000000000..1d6814ad3 --- /dev/null +++ b/packages/tdev/circuit/components/styles.module.scss @@ -0,0 +1,29 @@ +.wrapper { + display: flex; + flex-direction: row; + justify-content: center; + margin: 0 1em; + + .rooms { + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 1em; + margin: 1em 0; + max-width: 60em; + + .name { + display: flex; + justify-content: space-between; + align-items: center; + } + } +} + +.alert { + display: flex; + justify-content: start; + gap: 1em; + align-items: center; + margin: 1em; +} diff --git a/packages/tdev/circuit/index.ts b/packages/tdev/circuit/index.ts new file mode 100644 index 000000000..0d8444d85 --- /dev/null +++ b/packages/tdev/circuit/index.ts @@ -0,0 +1,4 @@ +import type { PluginConfig } from '@docusaurus/types'; +import { DocumentType } from '@tdev-api/document'; +import type DocumentRoot from '@tdev-models/DocumentRoot'; +import path from 'path'; diff --git a/packages/tdev/circuit/models/CircuitRoom.ts b/packages/tdev/circuit/models/CircuitRoom.ts new file mode 100644 index 000000000..a20f6f0e8 --- /dev/null +++ b/packages/tdev/circuit/models/CircuitRoom.ts @@ -0,0 +1,64 @@ +import DynamicRoom from '@tdev-models/documents/DynamicRooms'; +import DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; +import { DocumentType, RoomType } from '@tdev-api/document'; +import { action, computed, observable } from 'mobx'; +import FlowNode from './FlowNode'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { Node, NodeChange, OnNodesChange } from '@xyflow/react'; +import { Source } from '@tdev-models/iDocument'; + +class CircuitRoom extends DynamicRoom { + constructor(dynamicRoot: DynamicDocumentRoot, documentStore: DocumentStore) { + super(dynamicRoot, documentStore); + } + + @computed + get flowNodes() { + return this.documents.filter((doc) => doc.type === DocumentType.FlowNode); + } + + @computed + get nodes() { + return this.flowNodes.map((fn) => fn.nodeData); + } + + @action + onNodesChange(changes: NodeChange[]) { + changes.forEach((change) => { + if (!('id' in change)) { + return; + } + + const node = this.flowNodes.find((doc) => doc.id === change.id); + if (node) { + switch (change.type) { + case 'dimensions': + break; + case 'position': + node.setData({ position: change.position }, Source.LOCAL, new Date()); + break; + case 'select': + node.setData({ selected: change.selected }, Source.LOCAL, new Date()); + break; + } + } + }); + } + + @action + addFlowNode() { + this.documentStore.create({ + documentRootId: this.dynamicRoot.rootDocumentId, + type: DocumentType.FlowNode, + data: { + data: {}, + position: { + x: 100, + y: 100 + } + } + }); + } +} + +export default CircuitRoom; diff --git a/packages/tdev/nand-game/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts similarity index 64% rename from packages/tdev/nand-game/models/FlowNode.ts rename to packages/tdev/circuit/models/FlowNode.ts index 95a344e3f..b727fbff4 100644 --- a/packages/tdev/nand-game/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -1,8 +1,16 @@ import { action, computed, observable } from 'mobx'; import iDocument, { Source } from '@tdev-models/iDocument'; -import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; +import { + DocumentType, + Document as DocumentProps, + TypeDataMapping, + Access, + FlowNodeData +} from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import { TypeMeta } from '@tdev-models/DocumentRoot'; +import { Node } from '@xyflow/react'; +import { merge, toMerged } from 'es-toolkit'; export interface MetaInit { readonly?: boolean; @@ -17,25 +25,25 @@ export class ModelMeta extends TypeMeta { get defaultData(): TypeDataMapping[DocumentType.FlowNode] { return { - inputs: [], - outputs: [] + data: {}, + position: { + x: 0, + y: 0 + } }; } } class FlowNode extends iDocument { - @observable.ref accessor inputs: string[]; - @observable.ref accessor outputs: string[]; + @observable.ref accessor flowData: FlowNodeData; constructor(props: DocumentProps, store: DocumentStore) { - super(props, store); - this.inputs = props.data.inputs || []; - this.outputs = props.data.outputs || []; + super(props, store, 5); + this.flowData = props.data; } @action - setData(data: TypeDataMapping[DocumentType.FlowNode], from: Source, updatedAt?: Date): void { - this.inputs = [...data.inputs]; - this.outputs = [...data.outputs]; + setData(data: Partial, from: Source, updatedAt?: Date): void { + this.flowData = toMerged(this.flowData, data); if (from === Source.LOCAL) { /** * Assumption: @@ -50,10 +58,12 @@ class FlowNode extends iDocument { } get data(): TypeDataMapping[DocumentType.FlowNode] { - return { - inputs: this.inputs, - outputs: this.outputs - }; + return { ...this.flowData }; + } + + @computed + get nodeData() { + return { ...this.flowData, data: { ...this.flowData.data, label: this.id }, id: this.id }; } @computed diff --git a/packages/tdev/nand-game/package.json b/packages/tdev/circuit/package.json similarity index 70% rename from packages/tdev/nand-game/package.json rename to packages/tdev/circuit/package.json index d3dff84d4..503fc5179 100644 --- a/packages/tdev/nand-game/package.json +++ b/packages/tdev/circuit/package.json @@ -1,18 +1,18 @@ { - "name": "@tdev/nand-game", + "name": "@tdev/circuit", "version": "1.0.0", "main": "index.ts", "types": "index.ts", "dependencies": { - "reactflow": "^11.11.4" + "@xyflow/react": "^12.8.4" }, "devDependencies": { - "vitest": "*", - "@docusaurus/module-type-aliases": "*", "@docusaurus/core": "*", - "@types/node": "*" + "@docusaurus/module-type-aliases": "*", + "@types/node": "*", + "vitest": "*" }, "peerDependencies": { "@tdev/core": "1.0.0" } -} \ No newline at end of file +} diff --git a/packages/tdev/nand-game/tsconfig.json b/packages/tdev/circuit/tsconfig.json similarity index 100% rename from packages/tdev/nand-game/tsconfig.json rename to packages/tdev/circuit/tsconfig.json diff --git a/packages/tdev/nand-game/index.ts b/packages/tdev/nand-game/index.ts deleted file mode 100644 index ea45bd1a9..000000000 --- a/packages/tdev/nand-game/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PluginConfig } from '@docusaurus/types'; -import path from 'path'; - -export const excalidrawPluginConfig: PluginConfig = () => { - return { - name: 'excalidraw-config', - configureWebpack(config, isServer, { currentBundler }) { - const cwd = process.cwd(); - return { - module: { - rules: [ - { - test: /\.excalidraw$/, - type: 'json' - }, - { - test: /\.excalidrawlib$/, - type: 'json' - } - ] - }, - resolve: { - fallback: { - 'roughjs/bin/math': path.resolve(cwd, './node_modules/roughjs/bin/math.js'), - 'roughjs/bin/rough': path.resolve(cwd, './node_modules/roughjs/bin/rough.js'), - 'roughjs/bin/generator': path.resolve(cwd, './node_modules/roughjs/bin/generator.js') - } - }, - plugins: [ - new currentBundler.instance.DefinePlugin({ - 'process.env.IS_PREACT': JSON.stringify('false') - }) - ] - }; - } - }; -}; diff --git a/packages/tdev/nand-game/models/Flow.ts b/packages/tdev/nand-game/models/Flow.ts deleted file mode 100644 index 676403f85..000000000 --- a/packages/tdev/nand-game/models/Flow.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { action, computed, observable } from 'mobx'; -import iDocument, { Source } from '@tdev-models/iDocument'; -import { DocumentType, Document as DocumentProps, TypeDataMapping, Access } from '@tdev-api/document'; -import DocumentStore from '@tdev-stores/DocumentStore'; -import { TypeMeta } from '@tdev-models/DocumentRoot'; - -export interface MetaInit { - readonly?: boolean; -} - -export class ModelMeta extends TypeMeta { - readonly type = DocumentType.Flow; - - constructor(props: Partial) { - super(DocumentType.Flow, props.readonly ? Access.RO_User : undefined); - } - - get defaultData(): TypeDataMapping[DocumentType.Flow] { - return {}; - } -} - -class Flow extends iDocument { - constructor(props: DocumentProps, store: DocumentStore) { - super(props, store); - } - - @action - setData(data: TypeDataMapping[DocumentType.Flow], from: Source, updatedAt?: Date): void { - if (from === Source.LOCAL) { - // this.save(); - } - if (updatedAt) { - this.updatedAt = new Date(updatedAt); - } - } - - get data(): TypeDataMapping[DocumentType.Flow] { - return {}; - } - - @computed - get meta(): ModelMeta { - if (this.root?.type === DocumentType.Flow) { - return this.root.meta as ModelMeta; - } - return new ModelMeta({}); - } -} - -export default Flow; diff --git a/src/api/document.ts b/src/api/document.ts index d7f94d9fb..a91bb5b59 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -15,14 +15,15 @@ import { Color } from '@tdev-components/shared/Colors'; import CmsText from '@tdev-models/documents/CmsText'; import TextMessage from '@tdev-models/documents/TextMessage'; import DynamicDocumentRoots from '@tdev-models/documents/DynamicDocumentRoots'; -import { DynamicDocumentRootModel } from '@tdev-models/documents/DynamicDocumentRoot'; -import NetpbmGraphic from '@tdev-models/documents/NetpbmGraphic'; +import type { DynamicDocumentRootModel } from '@tdev-models/documents/DynamicDocumentRoot'; +import type NetpbmGraphic from '@tdev-models/documents/NetpbmGraphic'; import type { BinaryFiles } from '@excalidraw/excalidraw/types'; import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; -import Excalidoc from '@tdev/excalidoc/model'; -import ProgressState from '@tdev-models/documents/ProgressState'; -import FlowNode from '@tdev/nand-game/models/FlowNode'; -import Flow from '@tdev/nand-game/models/Flow'; +import type Excalidoc from '@tdev/excalidoc/model'; +import type ProgressState from '@tdev-models/documents/ProgressState'; +import FlowNode from '@tdev/circuit/models/FlowNode'; +import { Node } from '@xyflow/react'; +import CircuitRoom from '@tdev/circuit/models/CircuitRoom'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -54,7 +55,6 @@ export enum DocumentType { DynamicDocumentRoot = 'dynamic_document_root', DynamicDocumentRoots = 'dynamic_document_roots', NetpbmGraphic = 'netpbm_graphic', - Flow = 'react_flow', FlowNode = 'flow_node' } @@ -110,12 +110,7 @@ export interface ExcaliData { image: string; } -export interface FlowData {} - -export interface FlowNodeData { - inputs: string[]; - outputs: string[]; -} +export type FlowNodeData = Omit; export type StateType = | 'checked' @@ -151,15 +146,16 @@ export interface DynamicDocumentRootData { } export enum RoomType { - Messages = 'text_messages' + Messages = 'text_messages', + Circuit = 'circuit' } export interface DynamicDocumentRoot { id: string; name: string; - type: RoomType; } export interface DynamicDocumentRootsData { + roomType: RoomType; documentRoots: DynamicDocumentRoot[]; } @@ -185,7 +181,6 @@ export interface TypeDataMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootData; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRootsData; [DocumentType.NetpbmGraphic]: NetpbmGraphicData; - [DocumentType.Flow]: FlowData; [DocumentType.FlowNode]: FlowNodeData; // Add more mappings as needed } @@ -208,7 +203,6 @@ export interface TypeModelMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootModel; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRoots; [DocumentType.NetpbmGraphic]: NetpbmGraphic; - [DocumentType.Flow]: Flow; [DocumentType.FlowNode]: FlowNode; /** @@ -237,7 +231,6 @@ export type DocumentTypes = | DynamicDocumentRoots | NetpbmGraphic | ProgressState - | Flow | FlowNode; export interface Document { diff --git a/src/components/Rooms/index.tsx b/src/components/Rooms/index.tsx index ed426cd99..51e257ee2 100644 --- a/src/components/Rooms/index.tsx +++ b/src/components/Rooms/index.tsx @@ -19,6 +19,8 @@ import PermissionsPanel from '@tdev-components/PermissionsPanel'; import { NoneAccess } from '@tdev-models/helpers/accessPolicy'; import NoAccess from '@tdev-components/shared/NoAccess'; import TextMessages from './TextMessages'; +import Circuit from '@tdev/circuit/components/Circuit'; +import { default as DynamiDocumentRootMeta } from '@tdev-models/documents/DynamicDocumentRoot'; import RoomTypeSelector from '@tdev-components/documents/DynamicDocumentRoots/RoomTypeSelector'; const NoRoom = () => { @@ -30,11 +32,11 @@ const NoRoom = () => { ); }; -const NoType = ({ dynamicRoot }: { dynamicRoot: DynamicDocumentRootMeta }) => { +const NoType = ({ dynamicRoot }: { dynamicRoot: DynamicDocumentRootMeta }) => { return (
- Unbekannter Raum-Typ "{dynamicRoot.props?.type}" + Unbekannter Raum-Typ "{dynamicRoot.roomType}"
@@ -67,14 +69,19 @@ const PATHNAME_PATTERN = '/rooms/:parentRootId/:documentRootId?' as const; interface Props { roomProps: DynamicDocumentRoot; parentDocumentId: string; + roomType: RoomType; } const RoomComponent = observer((props: Props): React.ReactNode => { const documentStore = useStore('documentStore'); - const drStore = useStore('documentRootStore'); - const { roomProps } = props; - const [dynamicRoot] = React.useState( - new DynamicDocumentRootMeta({}, roomProps.id, props.parentDocumentId, documentStore) - ); + const { roomProps, roomType } = props; + const dynamicRoot = React.useMemo(() => { + return new DynamicDocumentRootMeta( + { roomType: roomType }, + roomProps.id, + props.parentDocumentId, + documentStore + ); + }, [roomProps.id, roomType]); const documentRoot = useDocumentRoot(roomProps.id, dynamicRoot, false, {}, true); if (!documentRoot || documentRoot.type !== DocumentType.DynamicDocumentRoot) { @@ -89,9 +96,11 @@ const RoomComponent = observer((props: Props): React.ReactNode => { ); } - switch (roomProps.type) { + switch (roomType) { case RoomType.Messages: return ; + case RoomType.Circuit: + return } />; default: return ; } @@ -103,7 +112,7 @@ interface WithParentRootProps { const WithParentRoot = observer((props: WithParentRootProps): React.ReactNode => { const routeParams = matchPath(props.path, PATHNAME_PATTERN); const { parentRootId, documentRootId } = routeParams?.params || {}; - const [rootsMeta] = React.useState(new RootsMeta({})); + const [rootsMeta] = React.useState(new RootsMeta({ roomType: RoomType.Messages })); const dynDocRoots = useDocumentRoot(parentRootId, rootsMeta, false, {}, true); if (dynDocRoots.isDummy) { return ; @@ -115,15 +124,21 @@ const WithParentRoot = observer((props: WithParentRootProps): React.ReactNode => return ; } if (!documentRootId || !dynDocRoots.firstMainDocument?.id) { - return ; + return ; } const dynamicRoot = dynDocRoots.firstMainDocument.dynamicDocumentRoots.find( (ddr) => ddr.id === documentRootId ); if (!dynamicRoot) { - return ; + return ; } - return ; + return ( + + ); }); const Rooms = observer(() => { diff --git a/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx b/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx index 09cf8fb4d..17c3e76c9 100644 --- a/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/AddDynamicDocumentRoot/index.tsx @@ -5,10 +5,9 @@ import { useStore } from '@tdev-hooks/useStore'; import { v4 as uuidv4 } from 'uuid'; import Button from '@tdev-components/shared/Button'; import { mdiPlusCircleOutline } from '@mdi/js'; -import { RoomType } from '@tdev-api/document'; import { RWAccess } from '@tdev-models/helpers/accessPolicy'; -interface Props extends MetaInit { +interface Props extends Partial { dynamicDocumentRoots: DynamicDocumentRoots; } @@ -37,8 +36,7 @@ const AddDynamicDocumentRoot = observer((props: Props) => { const newId = uuidv4(); dynamicDocumentRoots.addDynamicDocumentRoot( newId, - `Neue Gruppe (${dynamicDocumentRoots.dynamicDocumentRoots.length + 1})`, - RoomType.Messages + `Neue Gruppe (${dynamicDocumentRoots.dynamicDocumentRoots.length + 1})` ); }} /> diff --git a/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx b/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx index da5ccc01d..ea0de065f 100644 --- a/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx @@ -17,19 +17,26 @@ import { import { useDocumentRoot } from '@tdev-hooks/useDocumentRoot'; import { default as DynamicDocumentRootMeta } from '@tdev-models/documents/DynamicDocumentRoot'; import { NoneAccess } from '@tdev-models/helpers/accessPolicy'; -import RoomTypeSelector, { RoomTypeLabel } from '../RoomTypeSelector'; +import { RoomTypeLabel } from '../RoomTypeSelector'; import TextInput from '@tdev-components/shared/TextInput'; import { Confirm } from '@tdev-components/shared/Button/Confirm'; +import { RoomType } from '@tdev-api/document'; interface Props extends MetaInit { id: string; dynamicRootsDocumentId: string; + roomType: RoomType; } const DynamicDocumentRoot = observer((props: Props) => { const documentStore = useStore('documentStore'); const [meta] = React.useState( - new DynamicDocumentRootMeta({}, props.id, props.dynamicRootsDocumentId, documentStore) + new DynamicDocumentRootMeta( + { roomType: props.roomType }, + props.id, + props.dynamicRootsDocumentId, + documentStore + ) ); const [edit, setEdit] = React.useState(false); const [title, setTitle] = React.useState(''); @@ -40,6 +47,11 @@ const DynamicDocumentRoot = observer((props: Props) => { permissionStore.loadPermissions(docRoot); } }, [docRoot]); + React.useEffect(() => { + if (props.roomType && meta.roomType !== props.roomType) { + meta.setRoomType(props.roomType); + } + }, [props.roomType, meta.roomType]); if (!docRoot || docRoot.isDummy) { return (
@@ -70,9 +82,7 @@ const DynamicDocumentRoot = observer((props: Props) => { ) : (
{meta.name}
)} - {edit ? ( - setEdit(false)} /> - ) : ( + {!edit && (
{meta.roomType ? RoomTypeLabel[meta.roomType] : '-'}
diff --git a/src/components/documents/DynamicDocumentRoots/RoomTypeSelector/index.tsx b/src/components/documents/DynamicDocumentRoots/RoomTypeSelector/index.tsx index ba9beef8c..3494b7597 100644 --- a/src/components/documents/DynamicDocumentRoots/RoomTypeSelector/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/RoomTypeSelector/index.tsx @@ -2,15 +2,18 @@ import React from 'react'; import clsx from 'clsx'; import styles from './styles.module.scss'; import { observer } from 'mobx-react-lite'; -import { RoomType } from '@tdev-api/document'; -import DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; +import { DocumentType, RoomType } from '@tdev-api/document'; +import type DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; import SelectInput from '@tdev-components/shared/SelectInput'; +import type DocumentRoot from '@tdev-models/DocumentRoot'; interface Props { - dynamicRoot: DynamicDocumentRoot; + dynamicRoot: DynamicDocumentRoot; onChange?: (type: RoomType) => void; } +export type MessageRoom = DocumentRoot; + export const RoomTypeLabel: { [key in RoomType]: string } = { [RoomType.Messages]: 'Textnachrichten', [RoomType.Circuit]: 'Schaltkreis' @@ -27,8 +30,8 @@ const RoomTypeSelector = observer((props: Props) => { const { dynamicRoot } = props; const options = React.useMemo(() => { const values = Object.values(RoomType); - if (dynamicRoot.props?.type && !ValidRoomType.has(dynamicRoot.props?.type || '')) { - values.push(dynamicRoot.props.type); + if (!ValidRoomType.has(dynamicRoot.roomType)) { + values.push(dynamicRoot.roomType); } return values.map((o) => { return { @@ -37,12 +40,13 @@ const RoomTypeSelector = observer((props: Props) => { disabled: !ValidRoomType.has(o) }; }); - }, [dynamicRoot.props?.type]); + }, [dynamicRoot.roomType]); return ( { dynamicRoot.setRoomType(value as RoomType); props?.onChange?.(value as RoomType); diff --git a/src/components/documents/DynamicDocumentRoots/index.tsx b/src/components/documents/DynamicDocumentRoots/index.tsx index eb51e9d6a..1802f7ac1 100644 --- a/src/components/documents/DynamicDocumentRoots/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/index.tsx @@ -3,15 +3,15 @@ import clsx from 'clsx'; import styles from './styles.module.scss'; import { observer } from 'mobx-react-lite'; import { useFirstRealMainDocument } from '@tdev-hooks/useFirstRealMainDocument'; -import Loader from '@tdev-components/Loader'; import { MetaInit, ModelMeta } from '@tdev-models/documents/DynamicDocumentRoots'; import PermissionsPanel from '@tdev-components/PermissionsPanel'; -import { Access } from '@tdev-api/document'; +import { Access, RoomType } from '@tdev-api/document'; import { useStore } from '@tdev-hooks/useStore'; import AddDynamicDocumentRoot from './AddDynamicDocumentRoot'; import DynamicDocumentRoot from './DynamicDocumentRoot'; -import NoAccess from '@tdev-components/shared/NoAccess'; import { NotCreated } from '@tdev-components/Rooms'; +import Badge from '@tdev-components/shared/Badge'; +import { RoomTypeLabel } from './RoomTypeSelector'; interface Props extends MetaInit { id: string; @@ -43,7 +43,9 @@ const DynamicDocumentRoots = observer((props: Props) => { return (
-

{props.name || 'Gruppe'}

+

+ {props.name || 'Gruppe'} {RoomTypeLabel[doc.roomType] ?? doc.roomType} +

@@ -51,7 +53,14 @@ const DynamicDocumentRoots = observer((props: Props) => {
{doc.dynamicDocumentRoots.map((root) => { - return ; + return ( + + ); })}
diff --git a/src/models/documents/DynamicDocumentRoot.ts b/src/models/documents/DynamicDocumentRoot.ts index 7e6ddd7c9..e13a52cb1 100644 --- a/src/models/documents/DynamicDocumentRoot.ts +++ b/src/models/documents/DynamicDocumentRoot.ts @@ -11,27 +11,56 @@ import { import DocumentStore from '@tdev-stores/DocumentStore'; import DocumentRoot, { TypeMeta } from '@tdev-models/DocumentRoot'; import DynamicDocumentRoots from './DynamicDocumentRoots'; +import DynamicRoom from './DynamicRooms'; +import CircuitRoom from '@tdev/circuit/models/CircuitRoom'; -export interface MetaInit { +export interface MetaInit { readonly?: boolean; + roomType: T; } -class DynamicDocumentRoot extends TypeMeta { +interface RoomMapping { + [RoomType.Circuit]: CircuitRoom; + [RoomType.Messages]: DynamicRoom; +} + +function CreateRoomModel( + docRoot: DynamicDocumentRoot, + documentStore: DocumentStore +): RoomMapping[T]; +function CreateRoomModel( + docRoot: DynamicDocumentRoot, + documentStore: DocumentStore +): RoomMapping[RoomType] { + switch (docRoot.roomType) { + case RoomType.Messages: + return new DynamicRoom(docRoot as DynamicDocumentRoot, documentStore); + case RoomType.Circuit: + return new CircuitRoom(docRoot as DynamicDocumentRoot, documentStore); + // Add more cases as needed + } +} + +class DynamicDocumentRoot extends TypeMeta { readonly type = DocumentType.DynamicDocumentRoot; readonly store: DocumentStore; readonly rootDocumentId: string; readonly parentDocumentId: string; + readonly roomType: T; + readonly room: RoomMapping[T]; constructor( - props: Partial, + props: MetaInit, rootDocumentId: string, parentDocumentId: string, documentStore: DocumentStore ) { super(DocumentType.DynamicDocumentRoot, props.readonly ? Access.RO_User : undefined); + this.roomType = props.roomType; this.store = documentStore; this.rootDocumentId = rootDocumentId; this.parentDocumentId = parentDocumentId; + this.room = CreateRoomModel(this, documentStore); } @computed @@ -58,14 +87,6 @@ class DynamicDocumentRoot extends TypeMeta { return title === undefined ? 'Dynamische Document Root' : title; } - @computed - get roomType(): RoomType | undefined { - if (!this.parentDocument) { - return undefined; - } - return this.props?.type; - } - @action destroy(): void { this.parentDocument?.removeDynamicDocumentRoot(this.rootDocumentId); @@ -85,7 +106,7 @@ class DynamicDocumentRoot extends TypeMeta { if (!this.parentDocument) { return; } - this.parentDocument.setRoomType(this.rootDocumentId, type); + this.parentDocument.setRoomType(type); this.parentDocument.saveNow(); } @@ -110,7 +131,7 @@ export class DynamicDocumentRootModel extends iDocument { throw new Error('Method not implemented.'); } } diff --git a/src/models/documents/DynamicDocumentRoots.ts b/src/models/documents/DynamicDocumentRoots.ts index 98d23a0a7..fc22238ba 100644 --- a/src/models/documents/DynamicDocumentRoots.ts +++ b/src/models/documents/DynamicDocumentRoots.ts @@ -14,17 +14,21 @@ import { default as DynamicDocRootMeta } from '@tdev-models/documents/DynamicDoc export interface MetaInit { readonly?: boolean; + roomType: RoomType; } export class ModelMeta extends TypeMeta { readonly type = DocumentType.DynamicDocumentRoots; + readonly roomType: RoomType; - constructor(props: Partial) { + constructor(props: MetaInit) { super(DocumentType.DynamicDocumentRoots, props.readonly ? Access.RO_User : undefined); + this.roomType = props.roomType; } get defaultData(): TypeDataMapping[DocumentType.DynamicDocumentRoots] { return { + roomType: this.roomType, documentRoots: [] }; } @@ -33,10 +37,12 @@ export class ModelMeta extends TypeMeta { class DynamicDocumentRoots extends iDocument { readonly type = DocumentType.DynamicDocumentRoots; dynamicDocumentRoots = observable.array([]); + @observable accessor roomType: RoomType; constructor(props: DocumentProps, store: DocumentStore) { super(props, store); this.dynamicDocumentRoots.replace(props.data.documentRoots); + this.roomType = props.data.roomType; } @action @@ -63,20 +69,24 @@ class DynamicDocumentRoots extends iDocument { ...current, name: name }, ...this.dynamicDocumentRoots.filter((dr) => dr.id !== id) ]; - this.setData({ documentRoots: renamedRoots }, Source.LOCAL, new Date()); + this.setData({ roomType: this.roomType, documentRoots: renamedRoots }, Source.LOCAL, new Date()); + } + + @computed + get canChangeType() { + return this.linkedDynamicDocumentRoots.every((dr) => dr.allDocuments.length === 0); } @action - setRoomType(id: string, roomType: RoomType) { - const current = this.dynamicDocumentRoots.find((dr) => dr.id === id); - if (!current) { + setRoomType(roomType: RoomType) { + if (!this.canChangeType) { return; } - const renamedRoots: DynamicDocumentRoot[] = [ - { ...current, type: roomType }, - ...this.dynamicDocumentRoots.filter((dr) => dr.id !== id) - ]; - this.setData({ documentRoots: renamedRoots }, Source.LOCAL, new Date()); + this.setData( + { roomType: roomType, documentRoots: this.dynamicDocumentRoots.slice() }, + Source.LOCAL, + new Date() + ); } containsDynamicDocumentRoot(id: string): boolean { @@ -84,20 +94,27 @@ class DynamicDocumentRoots extends iDocument } @action - addDynamicDocumentRoot(id: string, name: string, roomType: RoomType) { + addDynamicDocumentRoot(id: string, name: string) { this.store.root.documentRootStore - .create(id, new DynamicDocRootMeta({}, id, this.id, this.store.root.documentStore), { - access: Access.None_DocumentRoot, - sharedAccess: Access.RO_DocumentRoot - }) + .create( + id, + new DynamicDocRootMeta( + { roomType: this.roomType }, + id, + this.id, + this.store.root.documentStore + ), + { + access: Access.None_DocumentRoot, + sharedAccess: Access.RO_DocumentRoot + } + ) .then( action((dynRoot) => { this.setData( { - documentRoots: [ - ...this.dynamicDocumentRoots, - { id: id, name: name, type: roomType } - ] + roomType: this.roomType, + documentRoots: [...this.dynamicDocumentRoots, { id: id, name: name }] }, Source.LOCAL, new Date() @@ -127,17 +144,23 @@ class DynamicDocumentRoots extends iDocument const linkedRoot = this.linkedDynamicDocumentRoots.find((dr) => dr.id === id); /** first remove the doc root from the state, otherwise it will be recreated immediately... */ this.setData( - { documentRoots: this.dynamicDocumentRoots.filter((dr) => dr.id !== id) }, + { + roomType: this.roomType, + documentRoots: this.dynamicDocumentRoots.filter((dr) => dr.id !== id) + }, Source.LOCAL, new Date() ); - (this.saveNow() || Promise.resolve()).then(() => { + this.saveNow().then(() => { if (linkedRoot) { this.store.root.documentRootStore.destroy(linkedRoot).then((success) => { if (!success) { /** undo the removal */ this.setData( - { documentRoots: [...this.dynamicDocumentRoots, ddRoot] }, + { + roomType: this.roomType, + documentRoots: [...this.dynamicDocumentRoots, ddRoot] + }, Source.LOCAL, new Date() ); @@ -149,6 +172,7 @@ class DynamicDocumentRoots extends iDocument get data(): TypeDataMapping[DocumentType.DynamicDocumentRoots] { return { + roomType: this.roomType, documentRoots: this.dynamicDocumentRoots.slice() }; } @@ -165,7 +189,7 @@ class DynamicDocumentRoots extends iDocument if (this.root?.type === DocumentType.DynamicDocumentRoots) { return this.root.meta as ModelMeta; } - return new ModelMeta({}); + return new ModelMeta({ roomType: this.roomType }); } } diff --git a/src/models/documents/DynamicRooms.ts b/src/models/documents/DynamicRooms.ts new file mode 100644 index 000000000..2a4fbadbc --- /dev/null +++ b/src/models/documents/DynamicRooms.ts @@ -0,0 +1,20 @@ +import { DocumentTypes, RoomType } from '@tdev-api/document'; +import type DynamicDocumentRoot from './DynamicDocumentRoot'; +import { computed } from 'mobx'; +import DocumentStore from '@tdev-stores/DocumentStore'; + +class DynamicRoom { + readonly dynamicRoot: DynamicDocumentRoot; + readonly documentStore: DocumentStore; + constructor(dynamicRoot: DynamicDocumentRoot, documentStore: DocumentStore) { + this.dynamicRoot = dynamicRoot; + this.documentStore = documentStore; + } + + @computed + get documents() { + return this.documentStore.findByDocumentRoot(this.dynamicRoot.rootDocumentId); + } +} + +export default DynamicRoom; diff --git a/src/models/iDocument.ts b/src/models/iDocument.ts index f66ba9f36..086bfae5c 100644 --- a/src/models/iDocument.ts +++ b/src/models/iDocument.ts @@ -1,12 +1,12 @@ import { action, computed, IReactionDisposer, observable, reaction, set } from 'mobx'; import { Document as DocumentProps, TypeDataMapping, DocumentType } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; -import _ from 'es-toolkit/compat'; +import _, { type DebouncedFunc } from 'es-toolkit/compat'; import { ApiState } from '@tdev-stores/iStore'; import { NoneAccess, ROAccess, RWAccess } from './helpers/accessPolicy'; import type iSideEffect from './SideEffects/iSideEffect'; import { DUMMY_DOCUMENT_ID } from '@tdev-hooks/useFirstMainDocument'; -import { isDummyId, isTempId } from '@tdev-hooks/useDummyId'; +import { isTempId } from '@tdev-hooks/useDummyId'; /** * normally, save only once all 1000ms @@ -36,11 +36,7 @@ abstract class iDocument { * Time [s] : 0 1 2 3 4 5 6 7 * Edits : ||| | ||| || | | || |||| ||| || ||| ||||| */ - save = _.debounce(action(this._save), SAVE_DEBOUNCE_TIME, { - leading: false, - trailing: true, - maxWait: 5 * SAVE_DEBOUNCE_TIME - }); + save: DebouncedFunc<() => Promise>; @observable accessor state: ApiState = ApiState.IDLE; @@ -48,7 +44,11 @@ abstract class iDocument { @observable.ref accessor updatedAt: Date; readonly stateDisposer: IReactionDisposer; - constructor(props: DocumentProps, store: DocumentStore) { + constructor( + props: DocumentProps, + store: DocumentStore, + saveDebounceTime: number = SAVE_DEBOUNCE_TIME + ) { this.store = store; this.id = props.id; this.authorId = props.authorId; @@ -59,6 +59,11 @@ abstract class iDocument { this.createdAt = new Date(props.createdAt); this.updatedAt = new Date(props.updatedAt); + this.save = _.debounce(action(this._save), saveDebounceTime, { + leading: false, + trailing: true, + maxWait: 5 * saveDebounceTime + }); this.stateDisposer = reaction( () => this.state, (state) => { @@ -216,7 +221,7 @@ abstract class iDocument { @action saveNow() { this.save(); - return this.save.flush(); + return this.save.flush() ?? Promise.resolve(); } @action diff --git a/src/stores/DocumentStore.ts b/src/stores/DocumentStore.ts index 36c3d8c2b..25331980e 100644 --- a/src/stores/DocumentStore.ts +++ b/src/stores/DocumentStore.ts @@ -33,12 +33,11 @@ import Restricted from '@tdev-models/documents/Restricted'; import CmsText from '@tdev-models/documents/CmsText'; import TextMessage from '@tdev-models/documents/TextMessage'; import DynamicDocumentRoots from '@tdev-models/documents/DynamicDocumentRoots'; -import { DynamicDocumentRootModel } from '@tdev-models/documents/DynamicDocumentRoot'; import NetpbmGraphic from '@tdev-models/documents/NetpbmGraphic'; import Excalidoc from '@tdev/excalidoc/model'; import ProgressState from '@tdev-models/documents/ProgressState'; -import FlowNode from '@tdev/nand-game/models/FlowNode'; -import Flow from '@tdev/nand-game/models/Flow'; +import FlowNode from '@tdev/circuit/models/FlowNode'; +import CircuitRoom from '@tdev/circuit/models/CircuitRoom'; const IsNotUniqueError = (error: any) => { try { @@ -83,18 +82,13 @@ export function CreateDocumentModel(data: DocumentProps, store: Do case DocumentType.TextMessage: return new TextMessage(data as DocumentProps, store); case DocumentType.DynamicDocumentRoot: - return new DynamicDocumentRootModel( - data as DocumentProps, - store - ); + throw new Error(`Dynamic Document Root's can't be instantiated.`); case DocumentType.DynamicDocumentRoots: return new DynamicDocumentRoots(data as DocumentProps, store); case DocumentType.NetpbmGraphic: return new NetpbmGraphic(data as DocumentProps, store); case DocumentType.ProgressState: return new ProgressState(data as DocumentProps, store); - case DocumentType.Flow: - return new Flow(data as DocumentProps, store); case DocumentType.FlowNode: return new FlowNode(data as DocumentProps, store); } diff --git a/yarn.lock b/yarn.lock index fabc90f84..f69432c68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4590,72 +4590,6 @@ resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== -"@reactflow/background@11.3.14": - version "11.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.14.tgz#778ca30174f3de77fc321459ab3789e66e71a699" - integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA== - dependencies: - "@reactflow/core" "11.11.4" - classcat "^5.0.3" - zustand "^4.4.1" - -"@reactflow/controls@11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.14.tgz#508ed2c40d23341b3b0919dd11e76fd49cf850c7" - integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw== - dependencies: - "@reactflow/core" "11.11.4" - classcat "^5.0.3" - zustand "^4.4.1" - -"@reactflow/core@11.11.4": - version "11.11.4" - resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.4.tgz#89bd86d1862aa1416f3f49926cede7e8c2aab6a7" - integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q== - dependencies: - "@types/d3" "^7.4.0" - "@types/d3-drag" "^3.0.1" - "@types/d3-selection" "^3.0.3" - "@types/d3-zoom" "^3.0.1" - classcat "^5.0.3" - d3-drag "^3.0.0" - d3-selection "^3.0.0" - d3-zoom "^3.0.0" - zustand "^4.4.1" - -"@reactflow/minimap@11.7.14": - version "11.7.14" - resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.14.tgz#298d7a63cb1da06b2518c99744f716560c88ca73" - integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ== - dependencies: - "@reactflow/core" "11.11.4" - "@types/d3-selection" "^3.0.3" - "@types/d3-zoom" "^3.0.1" - classcat "^5.0.3" - d3-selection "^3.0.0" - d3-zoom "^3.0.0" - zustand "^4.4.1" - -"@reactflow/node-resizer@2.2.14": - version "2.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz#1810c0ce51aeb936f179466a6660d1e02c7a77a8" - integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA== - dependencies: - "@reactflow/core" "11.11.4" - classcat "^5.0.4" - d3-drag "^3.0.0" - d3-selection "^3.0.0" - zustand "^4.4.1" - -"@reactflow/node-toolbar@1.3.14": - version "1.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz#c6ffc76f82acacdce654f2160dc9852162d6e7c9" - integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ== - dependencies: - "@reactflow/core" "11.11.4" - classcat "^5.0.3" - zustand "^4.4.1" - "@rollup/rollup-android-arm-eabi@4.49.0": version "4.49.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48" @@ -5490,173 +5424,55 @@ dependencies: "@types/node" "*" -"@types/d3-array@*": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" - integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== - -"@types/d3-axis@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" - integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-brush@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" - integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-chord@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" - integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== - "@types/d3-color@*": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== -"@types/d3-contour@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" - integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== - dependencies: - "@types/d3-array" "*" - "@types/geojson" "*" - -"@types/d3-delaunay@*": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" - integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== - -"@types/d3-dispatch@*": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2" - integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== - -"@types/d3-drag@*", "@types/d3-drag@^3.0.1": +"@types/d3-drag@^3.0.7": version "3.0.7" resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== dependencies: "@types/d3-selection" "*" -"@types/d3-dsv@*": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" - integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== - -"@types/d3-ease@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" - integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== - -"@types/d3-fetch@*": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" - integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== - dependencies: - "@types/d3-dsv" "*" - -"@types/d3-force@*": - version "3.0.10" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" - integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== - -"@types/d3-format@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" - integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== - -"@types/d3-geo@*": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" - integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== - dependencies: - "@types/geojson" "*" - -"@types/d3-hierarchy@*": - version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" - integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== - -"@types/d3-interpolate@*": +"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== dependencies: "@types/d3-color" "*" -"@types/d3-path@*": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" - integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== - -"@types/d3-polygon@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" - integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== - -"@types/d3-quadtree@*": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" - integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== - -"@types/d3-random@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" - integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== - -"@types/d3-scale-chromatic@*", "@types/d3-scale-chromatic@^3.0.0": +"@types/d3-scale-chromatic@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== -"@types/d3-scale@*", "@types/d3-scale@^4.0.3": +"@types/d3-scale@^4.0.3": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" -"@types/d3-selection@*", "@types/d3-selection@^3.0.3": +"@types/d3-selection@*", "@types/d3-selection@^3.0.10": version "3.0.11" resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== -"@types/d3-shape@*": - version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" - integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== - dependencies: - "@types/d3-path" "*" - -"@types/d3-time-format@*": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" - integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== - "@types/d3-time@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== -"@types/d3-timer@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" - integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== - -"@types/d3-transition@*": +"@types/d3-transition@^3.0.8": version "3.0.9" resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== dependencies: "@types/d3-selection" "*" -"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": +"@types/d3-zoom@^3.0.8": version "3.0.8" resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== @@ -5664,42 +5480,6 @@ "@types/d3-interpolate" "*" "@types/d3-selection" "*" -"@types/d3@^7.4.0": - version "7.4.3" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" - integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== - dependencies: - "@types/d3-array" "*" - "@types/d3-axis" "*" - "@types/d3-brush" "*" - "@types/d3-chord" "*" - "@types/d3-color" "*" - "@types/d3-contour" "*" - "@types/d3-delaunay" "*" - "@types/d3-dispatch" "*" - "@types/d3-drag" "*" - "@types/d3-dsv" "*" - "@types/d3-ease" "*" - "@types/d3-fetch" "*" - "@types/d3-force" "*" - "@types/d3-format" "*" - "@types/d3-geo" "*" - "@types/d3-hierarchy" "*" - "@types/d3-interpolate" "*" - "@types/d3-path" "*" - "@types/d3-polygon" "*" - "@types/d3-quadtree" "*" - "@types/d3-random" "*" - "@types/d3-scale" "*" - "@types/d3-scale-chromatic" "*" - "@types/d3-selection" "*" - "@types/d3-shape" "*" - "@types/d3-time" "*" - "@types/d3-time-format" "*" - "@types/d3-timer" "*" - "@types/d3-transition" "*" - "@types/d3-zoom" "*" - "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -5799,11 +5579,6 @@ "@types/jsonfile" "*" "@types/node" "*" -"@types/geojson@*": - version "7946.0.16" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" - integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== - "@types/gtag.js@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" @@ -6382,6 +6157,30 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@xyflow/react@^12.8.4": + version "12.8.4" + resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.8.4.tgz#db0eabd9e356c25f5ebf427413a8c5dd46113394" + integrity sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w== + dependencies: + "@xyflow/system" "0.0.68" + classcat "^5.0.3" + zustand "^4.4.0" + +"@xyflow/system@0.0.68": + version "0.0.68" + resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.68.tgz#980b8185743147a454bfb24a98f6bddc55a703e6" + integrity sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA== + dependencies: + "@types/d3-drag" "^3.0.7" + "@types/d3-interpolate" "^3.0.4" + "@types/d3-selection" "^3.0.10" + "@types/d3-transition" "^3.0.8" + "@types/d3-zoom" "^3.0.8" + d3-drag "^3.0.0" + d3-interpolate "^3.0.1" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -7195,7 +6994,7 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -classcat@^5.0.3, classcat@^5.0.4: +classcat@^5.0.3: version "5.0.5" resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== @@ -7913,7 +7712,7 @@ d3-hierarchy@3: resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3, d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -13495,18 +13294,6 @@ react@^19.1.0: resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== -reactflow@^11.11.4: - version "11.11.4" - resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653" - integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og== - dependencies: - "@reactflow/background" "11.3.14" - "@reactflow/controls" "11.2.14" - "@reactflow/core" "11.11.4" - "@reactflow/minimap" "11.7.14" - "@reactflow/node-resizer" "2.2.14" - "@reactflow/node-toolbar" "1.3.14" - reactjs-popup@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/reactjs-popup/-/reactjs-popup-2.0.6.tgz#ff0e340e31d03785ae62f6b6cf62de1ef48c9605" @@ -15684,7 +15471,7 @@ zip-stream@^4.1.0: compress-commons "^4.1.2" readable-stream "^3.6.0" -zustand@^4.3.2, zustand@^4.4.1: +zustand@^4.3.2, zustand@^4.4.0: version "4.5.7" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== From da6210b5ff5d48e0152a43deaa80a47103cf59b7 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 8 Sep 2025 21:23:19 +0200 Subject: [PATCH 06/33] support streamed updates --- packages/tdev/circuit/components/Circuit.tsx | 90 ++++++++++++------- packages/tdev/circuit/models/CircuitRoom.ts | 65 +++++++++++--- packages/tdev/circuit/models/FlowEdge.ts | 79 ++++++++++++++++ packages/tdev/circuit/models/FlowNode.ts | 18 ++-- src/api/IoEventTypes.ts | 6 ++ src/api/document.ts | 13 ++- src/components/Rooms/index.tsx | 44 +++++++-- .../DynamicDocumentRoot/index.tsx | 4 +- src/stores/DocumentStore.ts | 9 +- src/stores/SocketDataStore.ts | 12 ++- 10 files changed, 272 insertions(+), 68 deletions(-) create mode 100644 packages/tdev/circuit/models/FlowEdge.ts diff --git a/packages/tdev/circuit/components/Circuit.tsx b/packages/tdev/circuit/components/Circuit.tsx index 8972ed970..8c44c0f2c 100644 --- a/packages/tdev/circuit/components/Circuit.tsx +++ b/packages/tdev/circuit/components/Circuit.tsx @@ -4,37 +4,33 @@ import { observer } from 'mobx-react-lite'; import styles from './styles.module.scss'; import React from 'react'; import PermissionsPanel from '@tdev-components/PermissionsPanel'; -import DocumentRoot from '@tdev-models/DocumentRoot'; -import { - ReactFlow, - MiniMap, - Controls, - Background, - useNodesState, - useEdgesState, - addEdge, +import { Background, ReactFlow, MiniMap, Controls, Panel } from '@xyflow/react'; +import type { OnConnect, - ControlButton, - Panel, Edge, - NodeChange + OnEdgesChange, + OnNodesChange, + Node, + OnReconnect, + FinalConnectionState, + HandleType } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import Icon from '@mdi/react'; import { mdiWizardHat } from '@mdi/js'; import Button from '@tdev-components/shared/Button'; -import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument'; import DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; import { RoomType } from '@tdev-api/document'; +import { useStore } from '@tdev-hooks/useStore'; -const initialNodes = [ - { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } } - // { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } } -]; +type OnReconnectEnd = ( + event: MouseEvent | TouchEvent, + edge: Edge, + handleType: HandleType, + connectionState: FinalConnectionState +) => void; -// const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; -const initialEdges: Edge[] = []; +type OnReconnectStart = (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void; interface Props { dynamicRoot: DynamicDocumentRoot; @@ -42,23 +38,48 @@ interface Props { const Circuit = observer((props: Props): React.ReactNode => { const { dynamicRoot } = props; - - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const onConnect = React.useCallback( - (params: OnConnect['arguments']) => setEdges((eds) => addEdge(params, eds)), - [setEdges] - ); - const onChange = React.useCallback( - (change: NodeChange[]) => { + const documentStore = useStore('documentStore'); + const edgeReconnectSuccessful = React.useRef(true); + const onChange = React.useCallback>( + (change) => { dynamicRoot.room.onNodesChange(change); }, [dynamicRoot.room] ); + const onChangeEdge = React.useCallback>( + (change) => { + dynamicRoot.room.onEdgeChange(change); + }, + [dynamicRoot.room] + ); + const onConnect = React.useCallback( + (connection) => { + dynamicRoot.room.onConnect(connection); + }, + [dynamicRoot.room] + ); + const onReconnectStart = React.useCallback(() => { + edgeReconnectSuccessful.current = false; + }, []); + + const onReconnect = React.useCallback((oldEdge, newConnection) => { + edgeReconnectSuccessful.current = true; + // setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); + }, []); + + const onReconnectEnd = React.useCallback((_, edge) => { + if (!edgeReconnectSuccessful.current) { + const doc = documentStore.find(edge.id); + if (doc) { + documentStore.apiDelete(doc); + } + } + + edgeReconnectSuccessful.current = true; + }, []); if (!dynamicRoot) { return null; } - console.log('rerender', dynamicRoot.room.nodes); return (
@@ -69,12 +90,13 @@ const Circuit = observer((props: Props): React.ReactNode => {
diff --git a/packages/tdev/circuit/models/CircuitRoom.ts b/packages/tdev/circuit/models/CircuitRoom.ts index a20f6f0e8..e0031f9e5 100644 --- a/packages/tdev/circuit/models/CircuitRoom.ts +++ b/packages/tdev/circuit/models/CircuitRoom.ts @@ -4,7 +4,18 @@ import { DocumentType, RoomType } from '@tdev-api/document'; import { action, computed, observable } from 'mobx'; import FlowNode from './FlowNode'; import DocumentStore from '@tdev-stores/DocumentStore'; -import { Node, NodeChange, OnNodesChange } from '@xyflow/react'; +import { + Node, + NodeChange, + addEdge, + applyNodeChanges, + applyEdgeChanges, + EdgeChange, + OnEdgesChange, + Edge, + OnNodesChange, + OnConnect +} from '@xyflow/react'; import { Source } from '@tdev-models/iDocument'; class CircuitRoom extends DynamicRoom { @@ -14,33 +25,61 @@ class CircuitRoom extends DynamicRoom { @computed get flowNodes() { + console.log('rerun flow nodes'); return this.documents.filter((doc) => doc.type === DocumentType.FlowNode); } + @computed + get flowEdges() { + return this.documents.filter((doc) => doc.type === DocumentType.FlowEdge); + } + @computed get nodes() { return this.flowNodes.map((fn) => fn.nodeData); } + @computed + get edges() { + return this.flowEdges.map((fn) => fn.edgeData); + } + @action - onNodesChange(changes: NodeChange[]) { + onNodesChange(changes: Parameters[0]) { changes.forEach((change) => { if (!('id' in change)) { return; } - const node = this.flowNodes.find((doc) => doc.id === change.id); if (node) { - switch (change.type) { - case 'dimensions': - break; - case 'position': - node.setData({ position: change.position }, Source.LOCAL, new Date()); - break; - case 'select': - node.setData({ selected: change.selected }, Source.LOCAL, new Date()); - break; - } + node.setData(applyNodeChanges([change], [node.nodeData])[0], Source.LOCAL, new Date()); + } + }); + } + + @action + onEdgeChange(changes: Parameters>[0]) { + changes.forEach((change) => { + if (!('id' in change)) { + return; + } + const edge = this.flowEdges.find((doc) => doc.id === change.id); + if (edge) { + edge.setData(applyEdgeChanges([change], [edge.edgeData])[0], Source.LOCAL, new Date()); + } + }); + } + + @action + onConnect(connection: Parameters[0]) { + this.documentStore.create({ + documentRootId: this.dynamicRoot.rootDocumentId, + type: DocumentType.FlowEdge, + data: { + source: connection.source, + target: connection.target, + sourceHandle: connection.sourceHandle, + targetHandle: connection.targetHandle } }); } diff --git a/packages/tdev/circuit/models/FlowEdge.ts b/packages/tdev/circuit/models/FlowEdge.ts new file mode 100644 index 000000000..26dee7288 --- /dev/null +++ b/packages/tdev/circuit/models/FlowEdge.ts @@ -0,0 +1,79 @@ +import { action, computed, observable } from 'mobx'; +import iDocument, { Source } from '@tdev-models/iDocument'; +import { + DocumentType, + Document as DocumentProps, + TypeDataMapping, + Access, + FlowEdgeData +} from '@tdev-api/document'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { TypeMeta } from '@tdev-models/DocumentRoot'; + +export interface MetaInit { + readonly?: boolean; +} + +export class ModelMeta extends TypeMeta { + readonly type = DocumentType.FlowEdge; + + constructor(props: Partial) { + super(DocumentType.FlowEdge, props.readonly ? Access.RO_User : undefined); + } + + get defaultData(): TypeDataMapping[DocumentType.FlowEdge] { + return { + source: '', + target: '' + }; + } +} + +class FlowEdge extends iDocument { + @observable.ref accessor flowData: FlowEdgeData; + constructor(props: DocumentProps, store: DocumentStore) { + super(props, store, 15); + this.flowData = props.data; + } + + @action + setData(data: TypeDataMapping[DocumentType.FlowEdge], from: Source, updatedAt?: Date): void { + delete data.type; + this.flowData = data as any; + if (from === Source.LOCAL) { + /** + * Assumption: + * - local changes are commited only when the scene version is updated! + * - only non-deleted elements are commited + */ + this.save(); + this.store.root.socketStore.streamUpdate(this.documentRootId, { + id: this.id, + data: data, + updatedAt: this.updatedAt.toISOString() + }); + } + if (updatedAt) { + this.updatedAt = new Date(updatedAt); + } + } + + get data(): TypeDataMapping[DocumentType.FlowEdge] { + return { ...this.flowData }; + } + + @computed + get edgeData() { + return { ...this.flowData, id: this.id }; + } + + @computed + get meta(): ModelMeta { + if (this.root?.type === DocumentType.FlowEdge) { + return this.root.meta as ModelMeta; + } + return new ModelMeta({}); + } +} + +export default FlowEdge; diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index b727fbff4..dfdd2b8f4 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -37,13 +37,17 @@ export class ModelMeta extends TypeMeta { class FlowNode extends iDocument { @observable.ref accessor flowData: FlowNodeData; constructor(props: DocumentProps, store: DocumentStore) { - super(props, store, 5); + super(props, store); this.flowData = props.data; } @action - setData(data: Partial, from: Source, updatedAt?: Date): void { - this.flowData = toMerged(this.flowData, data); + setData(data: TypeDataMapping[DocumentType.FlowNode], from: Source, updatedAt?: Date): void { + delete data.type; + this.flowData = data; + if (updatedAt) { + this.updatedAt = new Date(updatedAt); + } if (from === Source.LOCAL) { /** * Assumption: @@ -51,9 +55,11 @@ class FlowNode extends iDocument { * - only non-deleted elements are commited */ this.save(); - } - if (updatedAt) { - this.updatedAt = new Date(updatedAt); + this.store.root.socketStore.streamUpdate(this.documentRootId, { + id: this.id, + data: data, + updatedAt: this.updatedAt.toISOString() + }); } } diff --git a/src/api/IoEventTypes.ts b/src/api/IoEventTypes.ts index 2151ae713..f1591493c 100644 --- a/src/api/IoEventTypes.ts +++ b/src/api/IoEventTypes.ts @@ -51,6 +51,10 @@ export interface ChangedDocument { updatedAt: string; } +export interface StreamedDynamicDocument extends ChangedDocument { + roomId: string; +} + export interface ConnectedClients { rooms: [string, number][]; type: 'full' | 'update'; @@ -116,6 +120,7 @@ export interface Action { export enum IoClientEvent { JOIN_ROOM = 'JOIN_ROOM', LEAVE_ROOM = 'LEAVE_ROOM', + STREAM_UPDATE = 'STREAM_UPDATE', ACTION = 'ACTION' } @@ -132,6 +137,7 @@ export interface ClientToServerEvents { [IoClientEvent.JOIN_ROOM]: (roomId: string, callback: (joined: boolean) => void) => void; [IoClientEvent.LEAVE_ROOM]: (roomId: string, callback: (left: boolean) => void) => void; [IoClientEvent.ACTION]: (action: Action, callback: (ok: boolean) => void) => void; + [IoClientEvent.STREAM_UPDATE]: (payload: StreamedDynamicDocument) => void; } export const RecordStoreMap: { [key in RecordType]: keyof typeof rootStore } = { diff --git a/src/api/document.ts b/src/api/document.ts index a91bb5b59..243bf3b05 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -22,8 +22,8 @@ import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'; import type Excalidoc from '@tdev/excalidoc/model'; import type ProgressState from '@tdev-models/documents/ProgressState'; import FlowNode from '@tdev/circuit/models/FlowNode'; -import { Node } from '@xyflow/react'; -import CircuitRoom from '@tdev/circuit/models/CircuitRoom'; +import { Edge, Node } from '@xyflow/react'; +import FlowEdge from '@tdev/circuit/models/FlowEdge'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -55,7 +55,8 @@ export enum DocumentType { DynamicDocumentRoot = 'dynamic_document_root', DynamicDocumentRoots = 'dynamic_document_roots', NetpbmGraphic = 'netpbm_graphic', - FlowNode = 'flow_node' + FlowNode = 'flow_node', + FlowEdge = 'flow_edge' } /** @@ -111,6 +112,7 @@ export interface ExcaliData { } export type FlowNodeData = Omit; +export type FlowEdgeData = Omit; export type StateType = | 'checked' @@ -182,6 +184,7 @@ export interface TypeDataMapping { [DocumentType.DynamicDocumentRoots]: DynamicDocumentRootsData; [DocumentType.NetpbmGraphic]: NetpbmGraphicData; [DocumentType.FlowNode]: FlowNodeData; + [DocumentType.FlowEdge]: FlowEdgeData; // Add more mappings as needed } @@ -204,6 +207,7 @@ export interface TypeModelMapping { [DocumentType.DynamicDocumentRoots]: DynamicDocumentRoots; [DocumentType.NetpbmGraphic]: NetpbmGraphic; [DocumentType.FlowNode]: FlowNode; + [DocumentType.FlowEdge]: FlowEdge; /** * Add more mappings as needed @@ -231,7 +235,8 @@ export type DocumentTypes = | DynamicDocumentRoots | NetpbmGraphic | ProgressState - | FlowNode; + | FlowNode + | FlowEdge; export interface Document { id: string; diff --git a/src/components/Rooms/index.tsx b/src/components/Rooms/index.tsx index 51e257ee2..0070c1f5e 100644 --- a/src/components/Rooms/index.tsx +++ b/src/components/Rooms/index.tsx @@ -22,6 +22,7 @@ import TextMessages from './TextMessages'; import Circuit from '@tdev/circuit/components/Circuit'; import { default as DynamiDocumentRootMeta } from '@tdev-models/documents/DynamicDocumentRoot'; import RoomTypeSelector from '@tdev-components/documents/DynamicDocumentRoots/RoomTypeSelector'; +import type DocumentRoot from '@tdev-models/DocumentRoot'; const NoRoom = () => { return ( @@ -66,11 +67,38 @@ const NoUser = () => { type PathParams = { parentRootId: string; documentRootId: string }; const PATHNAME_PATTERN = '/rooms/:parentRootId/:documentRootId?' as const; +interface RoomSwitcherProps { + roomType: RoomType; + dynamicRoot: DynamicDocumentRootMeta; + documentRoot: DocumentRoot; + roomProps: DynamicDocumentRoot; +} + +const RoomSwitcher = observer((props: RoomSwitcherProps) => { + const { roomType, documentRoot, dynamicRoot, roomProps } = props; + const socketStore = useStore('socketStore'); + React.useEffect(() => { + socketStore.joinRoom(documentRoot.id); + return () => { + socketStore.leaveRoom(documentRoot.id); + }; + }, [documentRoot, socketStore.socket?.id]); + switch (roomType) { + case RoomType.Messages: + return ; + case RoomType.Circuit: + return } />; + default: + return ; + } +}); + interface Props { roomProps: DynamicDocumentRoot; parentDocumentId: string; roomType: RoomType; } + const RoomComponent = observer((props: Props): React.ReactNode => { const documentStore = useStore('documentStore'); const { roomProps, roomType } = props; @@ -96,14 +124,14 @@ const RoomComponent = observer((props: Props): React.ReactNode => { ); } - switch (roomType) { - case RoomType.Messages: - return ; - case RoomType.Circuit: - return } />; - default: - return ; - } + return ( + + ); }); interface WithParentRootProps { diff --git a/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx b/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx index ea0de065f..ff4cc87c4 100644 --- a/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx +++ b/src/components/documents/DynamicDocumentRoots/DynamicDocumentRoot/index.tsx @@ -21,6 +21,7 @@ import { RoomTypeLabel } from '../RoomTypeSelector'; import TextInput from '@tdev-components/shared/TextInput'; import { Confirm } from '@tdev-components/shared/Button/Confirm'; import { RoomType } from '@tdev-api/document'; +import useBaseUrl from '@docusaurus/useBaseUrl'; interface Props extends MetaInit { id: string; @@ -41,6 +42,7 @@ const DynamicDocumentRoot = observer((props: Props) => { const [edit, setEdit] = React.useState(false); const [title, setTitle] = React.useState(''); const docRoot = useDocumentRoot(props.id, meta, false, {}, true); + const roomUrl = useBaseUrl(`/rooms/${meta.parentRoot?.id}/${docRoot.id}`); const permissionStore = useStore('permissionStore'); React.useEffect(() => { if (docRoot) { @@ -91,7 +93,7 @@ const DynamicDocumentRoot = observer((props: Props) => {
+ ); +}); + +export default SwitchNode; diff --git a/packages/tdev/circuit/components/Nodes/Switch/styles.module.scss b/packages/tdev/circuit/components/Nodes/Switch/styles.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/tdev/circuit/components/Nodes/index.ts b/packages/tdev/circuit/components/Nodes/index.ts new file mode 100644 index 000000000..94f4b75ba --- /dev/null +++ b/packages/tdev/circuit/components/Nodes/index.ts @@ -0,0 +1,9 @@ +import AndNode from './And'; +import OrNode from './Or'; +import SwitchNode from './Switch'; + +export const nodeTypes = { + SwitchNode, + AndNode, + OrNode +}; diff --git a/packages/tdev/circuit/models/CircuitRoom.ts b/packages/tdev/circuit/models/CircuitRoom.ts index 2152d51a4..689d8ecef 100644 --- a/packages/tdev/circuit/models/CircuitRoom.ts +++ b/packages/tdev/circuit/models/CircuitRoom.ts @@ -1,6 +1,6 @@ import DynamicRoom from '@tdev-models/documents/DynamicRooms'; import DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; -import { DocumentType, RoomType } from '@tdev-api/document'; +import { DocumentType, FlowNodeData, NodeDataMapping, NodeType, RoomType } from '@tdev-api/document'; import { action, computed, observable } from 'mobx'; import FlowNode from './FlowNode'; import DocumentStore from '@tdev-stores/DocumentStore'; @@ -38,7 +38,7 @@ class CircuitRoom extends DynamicRoom { @computed get nodes() { - return this.flowNodes.map((fn) => fn.nodeData); + return this.flowNodes.map((fn) => fn.node); } @computed @@ -63,7 +63,11 @@ class CircuitRoom extends DynamicRoom { } const node = this.flowNodes.find((doc) => doc.id === change.id); if (node) { - node.setData(applyNodeChanges([change], [node.nodeData])[0], Source.LOCAL, new Date()); + node.setData( + applyNodeChanges([change], [node.node])[0] as FlowNodeData, + Source.LOCAL, + new Date() + ); } }); } @@ -96,12 +100,13 @@ class CircuitRoom extends DynamicRoom { } @action - addFlowNode() { + addFlowNode(type: N, data: NodeDataMapping[N]) { this.documentStore.create({ documentRootId: this.dynamicRoot.rootDocumentId, type: DocumentType.FlowNode, data: { - data: {}, + data: data, + type: type, position: { x: 100, y: 100 diff --git a/packages/tdev/circuit/models/FlowEdge.ts b/packages/tdev/circuit/models/FlowEdge.ts index f818ebfc6..258c3c5e7 100644 --- a/packages/tdev/circuit/models/FlowEdge.ts +++ b/packages/tdev/circuit/models/FlowEdge.ts @@ -9,6 +9,7 @@ import { } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import { TypeMeta } from '@tdev-models/DocumentRoot'; +import FlowNode from './FlowNode'; export interface MetaInit { readonly?: boolean; @@ -67,6 +68,22 @@ class FlowEdge extends iDocument { return { ...this.flowData, id: this.id }; } + get sourceId() { + return this.data.source; + } + + get source(): FlowNode | undefined { + return this.store.find(this.sourceId) as FlowNode; + } + + get targetId() { + return this.data.target; + } + + get target(): FlowNode | undefined { + return this.store.find(this.targetId) as FlowNode; + } + @computed get isSelected() { return this.flowData.selected; diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index dfdd2b8f4..45f0e6b12 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -5,12 +5,19 @@ import { Document as DocumentProps, TypeDataMapping, Access, - FlowNodeData + FlowNodeData, + NodeType, + FlowNodeDataFull, + NodeDataMapping } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import { TypeMeta } from '@tdev-models/DocumentRoot'; import { Node } from '@xyflow/react'; import { merge, toMerged } from 'es-toolkit'; +import FlowEdge from './FlowEdge'; +import iDeriver from './derivers'; +import Or from './derivers/Or'; +import And from './derivers/And'; export interface MetaInit { readonly?: boolean; @@ -25,6 +32,7 @@ export class ModelMeta extends TypeMeta { get defaultData(): TypeDataMapping[DocumentType.FlowNode] { return { + type: NodeType.OrNode, data: {}, position: { x: 0, @@ -34,17 +42,36 @@ export class ModelMeta extends TypeMeta { } } -class FlowNode extends iDocument { - @observable.ref accessor flowData: FlowNodeData; +interface DeriverMapping { + [NodeType.SwitchNode]: iDeriver; + [NodeType.OrNode]: Or; + [NodeType.AndNode]: And; +} + +function createDeriver(node: FlowNode): DeriverMapping[NType] { + switch (node.flowData.type) { + case NodeType.OrNode: + return new Or(node as FlowNode) as DeriverMapping[NType]; + case NodeType.AndNode: + return new And(node as FlowNode) as DeriverMapping[NType]; + default: + return new iDeriver(node) as unknown as DeriverMapping[NType]; + } +} + +class FlowNode extends iDocument { + @observable.ref accessor flowData: FlowNodeData; + @observable.ref accessor deriver: DeriverMapping[NType]; + constructor(props: DocumentProps, store: DocumentStore) { super(props, store); - this.flowData = props.data; + this.flowData = props.data as FlowNodeData; + this.deriver = createDeriver(this); } @action setData(data: TypeDataMapping[DocumentType.FlowNode], from: Source, updatedAt?: Date): void { - delete data.type; - this.flowData = data; + this.flowData = data as FlowNodeData; if (updatedAt) { this.updatedAt = new Date(updatedAt); } @@ -63,13 +90,27 @@ class FlowNode extends iDocument { } } + /** + * tdev data + */ + @computed get data(): TypeDataMapping[DocumentType.FlowNode] { - return { ...this.flowData }; + return { ...this.flowData } as TypeDataMapping[DocumentType.FlowNode]; } + /** + * react flow's node + */ @computed - get nodeData() { - return { ...this.flowData, data: { ...this.flowData.data, label: this.id }, id: this.id }; + get node(): FlowNodeDataFull { + return { ...this.flowData, id: this.id } as FlowNodeDataFull; + } + + /** + * react flow's node data + */ + get nodeData(): NodeDataMapping[NType] { + return this.flowData.data; } @computed @@ -79,6 +120,29 @@ class FlowNode extends iDocument { } return new ModelMeta({}); } + + @computed + get edges(): FlowEdge[] { + return this.store + .findByDocumentRoot(this.documentRootId) + .filter((doc) => doc.type === DocumentType.FlowEdge) + .filter((doc) => doc.sourceId === this.id || doc.targetId === this.id); + } + + @computed + get sourceEdges(): FlowEdge[] { + return this.edges.filter((edge) => edge.sourceId === this.id); + } + + @computed + get targetEdges(): FlowEdge[] { + return this.edges.filter((edge) => edge.targetId === this.id); + } + + @computed + get power(): number { + return (this.flowData.data as { power?: number }).power || this.deriver.power || 0; + } } export default FlowNode; diff --git a/packages/tdev/circuit/models/derivers/And.ts b/packages/tdev/circuit/models/derivers/And.ts new file mode 100644 index 000000000..1cd2d2f4a --- /dev/null +++ b/packages/tdev/circuit/models/derivers/And.ts @@ -0,0 +1,21 @@ +import iDeriver from '.'; +import type FlowNode from '../FlowNode'; +import { NodeType } from '@tdev-api/document'; +import { computed } from 'mobx'; + +class And extends iDeriver { + constructor(node: FlowNode) { + super(node); + } + + @computed + get output(): boolean { + return this.flowNode.targetEdges.every((e) => (e.source?.power || 0) > 0); + } + + get power(): number { + return this.output ? 1 : 0; + } +} + +export default And; diff --git a/packages/tdev/circuit/models/derivers/Or.ts b/packages/tdev/circuit/models/derivers/Or.ts new file mode 100644 index 000000000..5dd0b92f8 --- /dev/null +++ b/packages/tdev/circuit/models/derivers/Or.ts @@ -0,0 +1,21 @@ +import iDeriver from '.'; +import type FlowNode from '../FlowNode'; +import { NodeType } from '@tdev-api/document'; +import { computed } from 'mobx'; + +class Or extends iDeriver { + constructor(node: FlowNode) { + super(node); + } + + @computed + get output(): boolean { + return this.flowNode.targetEdges.some((e) => (e.source?.power || 0) > 0); + } + + get power(): number { + return this.output ? 1 : 0; + } +} + +export default Or; diff --git a/packages/tdev/circuit/models/derivers/index.ts b/packages/tdev/circuit/models/derivers/index.ts new file mode 100644 index 000000000..660bc1d4c --- /dev/null +++ b/packages/tdev/circuit/models/derivers/index.ts @@ -0,0 +1,17 @@ +import type { NodeType } from '@tdev-api/document'; +import type { Node } from '@xyflow/react'; +import { action, computed, observable } from 'mobx'; +import type FlowNode from '../FlowNode'; + +class iDeriver { + readonly flowNode: FlowNode; + constructor(node: FlowNode) { + this.flowNode = node; + } + + get power() { + return 0; + } +} + +export default iDeriver; diff --git a/src/api/document.ts b/src/api/document.ts index 243bf3b05..c6af14274 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -111,7 +111,22 @@ export interface ExcaliData { image: string; } -export type FlowNodeData = Omit; +export enum NodeType { + SwitchNode = 'SwitchNode', + OrNode = 'OrNode', + AndNode = 'AndNode' +} + +export interface NodeDataMapping { + [NodeType.OrNode]: {}; + [NodeType.AndNode]: {}; + [NodeType.SwitchNode]: { + power: 0 | 1; + }; +} + +export type FlowNodeDataFull = Node; +export type FlowNodeData = Omit, 'id'>; export type FlowEdgeData = Omit; export type StateType = @@ -183,7 +198,7 @@ export interface TypeDataMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootData; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRootsData; [DocumentType.NetpbmGraphic]: NetpbmGraphicData; - [DocumentType.FlowNode]: FlowNodeData; + [DocumentType.FlowNode]: FlowNodeData; [DocumentType.FlowEdge]: FlowEdgeData; // Add more mappings as needed } @@ -206,7 +221,7 @@ export interface TypeModelMapping { [DocumentType.DynamicDocumentRoot]: DynamicDocumentRootModel; [DocumentType.DynamicDocumentRoots]: DynamicDocumentRoots; [DocumentType.NetpbmGraphic]: NetpbmGraphic; - [DocumentType.FlowNode]: FlowNode; + [DocumentType.FlowNode]: FlowNode; [DocumentType.FlowEdge]: FlowEdge; /** @@ -235,7 +250,7 @@ export type DocumentTypes = | DynamicDocumentRoots | NetpbmGraphic | ProgressState - | FlowNode + | FlowNode | FlowEdge; export interface Document { diff --git a/src/components/shared/Button/index.tsx b/src/components/shared/Button/index.tsx index 992432739..d46df96ee 100644 --- a/src/components/shared/Button/index.tsx +++ b/src/components/shared/Button/index.tsx @@ -20,6 +20,7 @@ export const POPUP_BUTTON_STYLE = clsx( export interface Base { onClick?: MouseEventHandler; onMouseDown?: MouseEventHandler; + onMouseUp?: MouseEventHandler; title?: string; href?: string; target?: '_blank' | `_self`; @@ -163,6 +164,7 @@ const Button = (props: Props) => { disabled={props.disabled} title={props.title} onMouseDown={props.onMouseDown} + onMouseUp={props.onMouseUp} > {props.floatingIcon && {props.floatingIcon}} From c853cde04105c7f252c063284d639f3018212984 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Thu, 11 Sep 2025 00:14:13 +0200 Subject: [PATCH 10/33] fix stream --- packages/tdev/circuit/components/Nodes/Switch/index.tsx | 4 ++-- packages/tdev/circuit/models/FlowNode.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tdev/circuit/components/Nodes/Switch/index.tsx b/packages/tdev/circuit/components/Nodes/Switch/index.tsx index 574900b55..271c26b50 100644 --- a/packages/tdev/circuit/components/Nodes/Switch/index.tsx +++ b/packages/tdev/circuit/components/Nodes/Switch/index.tsx @@ -19,9 +19,9 @@ const SwitchNode = observer((props: NodeProps) => { (e) => { e.preventDefault(); e.stopPropagation(); - doc?.setData({ ...doc.flowData, data: { power: 1 - doc.flowData.data.power } }, Source.LOCAL); + doc?.setData({ ...doc.flowData, data: { power: 1 - doc.power } }, Source.LOCAL); }, - [props.id, doc] + [props.id, doc, doc?.power] ); return (
diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index 45f0e6b12..1281cd6be 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -85,7 +85,7 @@ class FlowNode extends iDocument this.store.root.socketStore.streamUpdate(this.documentRootId, { id: this.id, data: data, - updatedAt: this.updatedAt.toISOString() + updatedAt: (this.updatedAt || new Date()).toISOString() }); } } From a0ea3ee3ac9f5cf8e24e7eb8302c8e5f1e92ba66 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Thu, 11 Sep 2025 07:07:41 +0200 Subject: [PATCH 11/33] fix setup --- .../circuit/components/Nodes/Switch/index.tsx | 4 ++-- packages/tdev/circuit/models/FlowNode.ts | 12 +++++------- packages/tdev/circuit/models/derivers/And.ts | 7 +++++-- packages/tdev/circuit/models/derivers/Or.ts | 4 ++-- .../tdev/circuit/models/derivers/Switch.ts | 18 ++++++++++++++++++ .../models/derivers/{index.ts => iDeriver.ts} | 4 +--- src/api/OfflineApi/index.ts | 7 ++++--- 7 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 packages/tdev/circuit/models/derivers/Switch.ts rename packages/tdev/circuit/models/derivers/{index.ts => iDeriver.ts} (72%) diff --git a/packages/tdev/circuit/components/Nodes/Switch/index.tsx b/packages/tdev/circuit/components/Nodes/Switch/index.tsx index 271c26b50..b34ff90ad 100644 --- a/packages/tdev/circuit/components/Nodes/Switch/index.tsx +++ b/packages/tdev/circuit/components/Nodes/Switch/index.tsx @@ -19,9 +19,9 @@ const SwitchNode = observer((props: NodeProps) => { (e) => { e.preventDefault(); e.stopPropagation(); - doc?.setData({ ...doc.flowData, data: { power: 1 - doc.power } }, Source.LOCAL); + doc?.setData({ ...doc.flowData, data: { power: 1 - doc.deriver.power } }, Source.LOCAL); }, - [props.id, doc, doc?.power] + [props.id, doc, doc] ); return (
diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index 1281cd6be..525e78035 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -15,9 +15,10 @@ import { TypeMeta } from '@tdev-models/DocumentRoot'; import { Node } from '@xyflow/react'; import { merge, toMerged } from 'es-toolkit'; import FlowEdge from './FlowEdge'; -import iDeriver from './derivers'; +import iDeriver from './derivers/iDeriver'; import Or from './derivers/Or'; import And from './derivers/And'; +import Switch from './derivers/Switch'; export interface MetaInit { readonly?: boolean; @@ -43,7 +44,7 @@ export class ModelMeta extends TypeMeta { } interface DeriverMapping { - [NodeType.SwitchNode]: iDeriver; + [NodeType.SwitchNode]: Switch; [NodeType.OrNode]: Or; [NodeType.AndNode]: And; } @@ -54,6 +55,8 @@ function createDeriver(node: FlowNode): DeriverMa return new Or(node as FlowNode) as DeriverMapping[NType]; case NodeType.AndNode: return new And(node as FlowNode) as DeriverMapping[NType]; + case NodeType.SwitchNode: + return new Switch(node as unknown as FlowNode) as DeriverMapping[NType]; default: return new iDeriver(node) as unknown as DeriverMapping[NType]; } @@ -138,11 +141,6 @@ class FlowNode extends iDocument get targetEdges(): FlowEdge[] { return this.edges.filter((edge) => edge.targetId === this.id); } - - @computed - get power(): number { - return (this.flowData.data as { power?: number }).power || this.deriver.power || 0; - } } export default FlowNode; diff --git a/packages/tdev/circuit/models/derivers/And.ts b/packages/tdev/circuit/models/derivers/And.ts index 1cd2d2f4a..05164d55f 100644 --- a/packages/tdev/circuit/models/derivers/And.ts +++ b/packages/tdev/circuit/models/derivers/And.ts @@ -1,4 +1,4 @@ -import iDeriver from '.'; +import iDeriver from './iDeriver'; import type FlowNode from '../FlowNode'; import { NodeType } from '@tdev-api/document'; import { computed } from 'mobx'; @@ -10,7 +10,10 @@ class And extends iDeriver { @computed get output(): boolean { - return this.flowNode.targetEdges.every((e) => (e.source?.power || 0) > 0); + return ( + this.flowNode.targetEdges.length >= 2 && + this.flowNode.targetEdges.every((e) => e.source?.deriver?.power > 0) + ); } get power(): number { diff --git a/packages/tdev/circuit/models/derivers/Or.ts b/packages/tdev/circuit/models/derivers/Or.ts index 5dd0b92f8..dd0a8e326 100644 --- a/packages/tdev/circuit/models/derivers/Or.ts +++ b/packages/tdev/circuit/models/derivers/Or.ts @@ -1,4 +1,4 @@ -import iDeriver from '.'; +import iDeriver from './iDeriver'; import type FlowNode from '../FlowNode'; import { NodeType } from '@tdev-api/document'; import { computed } from 'mobx'; @@ -10,7 +10,7 @@ class Or extends iDeriver { @computed get output(): boolean { - return this.flowNode.targetEdges.some((e) => (e.source?.power || 0) > 0); + return this.flowNode.targetEdges.some((e) => e.source?.deriver?.power > 0); } get power(): number { diff --git a/packages/tdev/circuit/models/derivers/Switch.ts b/packages/tdev/circuit/models/derivers/Switch.ts new file mode 100644 index 000000000..639653d35 --- /dev/null +++ b/packages/tdev/circuit/models/derivers/Switch.ts @@ -0,0 +1,18 @@ +import iDeriver from './iDeriver'; +import type FlowNode from '../FlowNode'; +import { NodeType } from '@tdev-api/document'; +import { action } from 'mobx'; +import { Source } from '@tdev-models/iDocument'; + +class Switch extends iDeriver { + constructor(node: FlowNode) { + super(node); + } + + @action + toggle() { + this.flowNode.setData({ ...this.flowNode.flowData, data: { power: 1 - this.power } }, Source.LOCAL); + } +} + +export default Switch; diff --git a/packages/tdev/circuit/models/derivers/index.ts b/packages/tdev/circuit/models/derivers/iDeriver.ts similarity index 72% rename from packages/tdev/circuit/models/derivers/index.ts rename to packages/tdev/circuit/models/derivers/iDeriver.ts index 660bc1d4c..27b0fdce9 100644 --- a/packages/tdev/circuit/models/derivers/index.ts +++ b/packages/tdev/circuit/models/derivers/iDeriver.ts @@ -1,6 +1,4 @@ import type { NodeType } from '@tdev-api/document'; -import type { Node } from '@xyflow/react'; -import { action, computed, observable } from 'mobx'; import type FlowNode from '../FlowNode'; class iDeriver { @@ -10,7 +8,7 @@ class iDeriver { } get power() { - return 0; + return (this.flowNode.flowData.data as { power?: number }).power ?? 0; } } diff --git a/src/api/OfflineApi/index.ts b/src/api/OfflineApi/index.ts index 5944f00a3..c5d6d2c92 100644 --- a/src/api/OfflineApi/index.ts +++ b/src/api/OfflineApi/index.ts @@ -172,8 +172,8 @@ export default class OfflineApi { return resolveResponse(document as unknown as T); case 'documentRoots': return resolveResponse({ - access: Access.RW_DocumentRoot, - sharedAccess: Access.RW_DocumentRoot, + access: (data as DocumentRoot).access ?? Access.RW_DocumentRoot, + sharedAccess: (data as DocumentRoot).sharedAccess ?? Access.RW_DocumentRoot, userPermissions: [], groupPermissions: [], documents: [], //documentsBy(id), // Fetching is only on GET/PUT, avoid circular dependency @@ -215,6 +215,7 @@ export default class OfflineApi { case 'users': if (parts.length === 1 && parts[0] === 'documentRoots') { const ids = query.getAll('ids'); + console.log('ids', ids); if (ids.length === 0) { resolveResponse([] as unknown as T); } @@ -321,7 +322,7 @@ export default class OfflineApi { // Method to handle PUT requests async put(url: string, data: Partial, ...config: any): AxiosPromise { const { model, id, parts } = urlParts(url); - log('put', url, data); + log('put', `${url} -> id: ${id}`, data); switch (model) { case 'documents': From fbceba29a6fbcdaa875589a59c7980da921e94b9 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 13 Sep 2025 13:02:07 +0200 Subject: [PATCH 12/33] fix lag --- packages/tdev/circuit/components/Nodes/Switch/index.tsx | 8 ++++---- packages/tdev/circuit/models/derivers/Switch.ts | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/tdev/circuit/components/Nodes/Switch/index.tsx b/packages/tdev/circuit/components/Nodes/Switch/index.tsx index b34ff90ad..72e128017 100644 --- a/packages/tdev/circuit/components/Nodes/Switch/index.tsx +++ b/packages/tdev/circuit/components/Nodes/Switch/index.tsx @@ -19,15 +19,15 @@ const SwitchNode = observer((props: NodeProps) => { (e) => { e.preventDefault(); e.stopPropagation(); - doc?.setData({ ...doc.flowData, data: { power: 1 - doc.deriver.power } }, Source.LOCAL); + doc?.deriver.toggle(); }, - [props.id, doc, doc] + [props.id, doc, doc?.deriver.power] ); return (
); }); diff --git a/packages/tdev/circuit/components/Nodes/index.ts b/packages/tdev/circuit/components/Nodes/index.ts index 94f4b75ba..20def2894 100644 --- a/packages/tdev/circuit/components/Nodes/index.ts +++ b/packages/tdev/circuit/components/Nodes/index.ts @@ -1,8 +1,10 @@ import AndNode from './And'; +import BatteryNode from './Battery'; import OrNode from './Or'; import SwitchNode from './Switch'; export const nodeTypes = { + BatteryNode, SwitchNode, AndNode, OrNode diff --git a/packages/tdev/circuit/components/Nodes/styles.module.scss b/packages/tdev/circuit/components/Nodes/styles.module.scss new file mode 100644 index 000000000..7700de991 --- /dev/null +++ b/packages/tdev/circuit/components/Nodes/styles.module.scss @@ -0,0 +1,8 @@ +.handle { + border-color: var(--ifm-color-danger); + background: var(--ifm-color-danger); + &.on { + border-color: var(--ifm-color-success); + background: var(--ifm-color-success); + } +} diff --git a/packages/tdev/circuit/components/styles.module.scss b/packages/tdev/circuit/components/styles.module.scss index 1d6814ad3..a1b12e379 100644 --- a/packages/tdev/circuit/components/styles.module.scss +++ b/packages/tdev/circuit/components/styles.module.scss @@ -3,6 +3,7 @@ flex-direction: row; justify-content: center; margin: 0 1em; + --tdev-circuit-power-color: #f8bf03; .rooms { display: flex; @@ -27,3 +28,8 @@ align-items: center; margin: 1em; } +[data-theme='dark'] { + .wrapper { + --tdev-circuit-power-color: #ddff00; + } +} diff --git a/packages/tdev/circuit/models/FlowEdge.ts b/packages/tdev/circuit/models/FlowEdge.ts index 258c3c5e7..a856017e1 100644 --- a/packages/tdev/circuit/models/FlowEdge.ts +++ b/packages/tdev/circuit/models/FlowEdge.ts @@ -5,7 +5,8 @@ import { Document as DocumentProps, TypeDataMapping, Access, - FlowEdgeData + FlowEdgeData, + NodeType } from '@tdev-api/document'; import DocumentStore from '@tdev-stores/DocumentStore'; import { TypeMeta } from '@tdev-models/DocumentRoot'; @@ -63,13 +64,35 @@ class FlowEdge extends iDocument { return { ...this.flowData }; } + @computed + get isPowerOn() { + return this.source?.deriver?.power > 0; + } + + @computed + get isGround() { + return this.isPowerOn && this.target?.flowData.type === NodeType.BatteryNode; + } + @computed get edgeData() { - return { ...this.flowData, id: this.id }; + return { + ...this.flowData, + animated: this.isPowerOn, + style: { + stroke: this.isPowerOn + ? this.isGround + ? 'var(--ifm-color-danger)' + : 'var(--tdev-circuit-power-color)' + : undefined, + strokeWidth: this.isPowerOn ? 2 : undefined + }, + id: this.id + }; } get sourceId() { - return this.data.source; + return this.flowData.source; } get source(): FlowNode | undefined { @@ -77,18 +100,21 @@ class FlowEdge extends iDocument { } get targetId() { - return this.data.target; + return this.flowData.target; } get target(): FlowNode | undefined { return this.store.find(this.targetId) as FlowNode; } - @computed get isSelected() { return this.flowData.selected; } + get isAnimated() { + return this.edgeData.animated; + } + @computed get meta(): ModelMeta { if (this.root?.type === DocumentType.FlowEdge) { diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index 525e78035..7e153a2e6 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -19,6 +19,7 @@ import iDeriver from './derivers/iDeriver'; import Or from './derivers/Or'; import And from './derivers/And'; import Switch from './derivers/Switch'; +import Battery from './derivers/Battery'; export interface MetaInit { readonly?: boolean; @@ -44,6 +45,7 @@ export class ModelMeta extends TypeMeta { } interface DeriverMapping { + [NodeType.BatteryNode]: Battery; [NodeType.SwitchNode]: Switch; [NodeType.OrNode]: Or; [NodeType.AndNode]: And; @@ -55,6 +57,8 @@ function createDeriver(node: FlowNode): DeriverMa return new Or(node as FlowNode) as DeriverMapping[NType]; case NodeType.AndNode: return new And(node as FlowNode) as DeriverMapping[NType]; + case NodeType.BatteryNode: + return new Battery(node as FlowNode) as DeriverMapping[NType]; case NodeType.SwitchNode: return new Switch(node as unknown as FlowNode) as DeriverMapping[NType]; default: @@ -133,14 +137,23 @@ class FlowNode extends iDocument } @computed - get sourceEdges(): FlowEdge[] { + get edgesOut(): FlowEdge[] { return this.edges.filter((edge) => edge.sourceId === this.id); } @computed - get targetEdges(): FlowEdge[] { + get edgesIn(): FlowEdge[] { return this.edges.filter((edge) => edge.targetId === this.id); } + + @computed + get inputEdgeA(): FlowEdge | undefined { + return this.edgesIn.find((edge) => edge.flowData.targetHandle === 'a'); + } + @computed + get inputEdgeB(): FlowEdge | undefined { + return this.edgesIn.find((edge) => edge.flowData.targetHandle === 'b'); + } } export default FlowNode; diff --git a/packages/tdev/circuit/models/derivers/And.ts b/packages/tdev/circuit/models/derivers/And.ts index 05164d55f..b45309f1c 100644 --- a/packages/tdev/circuit/models/derivers/And.ts +++ b/packages/tdev/circuit/models/derivers/And.ts @@ -11,8 +11,8 @@ class And extends iDeriver { @computed get output(): boolean { return ( - this.flowNode.targetEdges.length >= 2 && - this.flowNode.targetEdges.every((e) => e.source?.deriver?.power > 0) + this.flowNode.edgesIn.length >= 2 && + this.flowNode.edgesIn.every((e) => e.source?.deriver?.power > 0) ); } diff --git a/packages/tdev/circuit/models/derivers/Battery.ts b/packages/tdev/circuit/models/derivers/Battery.ts new file mode 100644 index 000000000..5e7393fab --- /dev/null +++ b/packages/tdev/circuit/models/derivers/Battery.ts @@ -0,0 +1,15 @@ +import iDeriver from './iDeriver'; +import type FlowNode from '../FlowNode'; +import { NodeType } from '@tdev-api/document'; + +class Battery extends iDeriver { + constructor(node: FlowNode) { + super(node); + } + + get power(): number { + return 1; + } +} + +export default Battery; diff --git a/packages/tdev/circuit/models/derivers/Or.ts b/packages/tdev/circuit/models/derivers/Or.ts index dd0a8e326..049cf4df8 100644 --- a/packages/tdev/circuit/models/derivers/Or.ts +++ b/packages/tdev/circuit/models/derivers/Or.ts @@ -10,7 +10,7 @@ class Or extends iDeriver { @computed get output(): boolean { - return this.flowNode.targetEdges.some((e) => e.source?.deriver?.power > 0); + return this.flowNode.edgesIn.some((e) => e.source?.deriver?.power > 0); } get power(): number { diff --git a/packages/tdev/circuit/models/derivers/Switch.ts b/packages/tdev/circuit/models/derivers/Switch.ts index a09c06d3c..bdd17a82e 100644 --- a/packages/tdev/circuit/models/derivers/Switch.ts +++ b/packages/tdev/circuit/models/derivers/Switch.ts @@ -1,7 +1,7 @@ import iDeriver from './iDeriver'; import type FlowNode from '../FlowNode'; import { NodeType } from '@tdev-api/document'; -import { action } from 'mobx'; +import { action, computed } from 'mobx'; import { Source } from '@tdev-models/iDocument'; class Switch extends iDeriver { @@ -9,6 +9,11 @@ class Switch extends iDeriver { super(node); } + @computed + get power() { + return (this.flowNode.flowData.data as { power?: number }).power ?? 0; + } + @action toggle() { this.flowNode.setData( diff --git a/src/api/document.ts b/src/api/document.ts index c6af14274..4aa90619a 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -112,12 +112,14 @@ export interface ExcaliData { } export enum NodeType { + BatteryNode = 'BatteryNode', SwitchNode = 'SwitchNode', OrNode = 'OrNode', AndNode = 'AndNode' } export interface NodeDataMapping { + [NodeType.BatteryNode]: {}; [NodeType.OrNode]: {}; [NodeType.AndNode]: {}; [NodeType.SwitchNode]: { From 93f146207db9054df558e535926d211b2e326498 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sat, 13 Sep 2025 18:21:27 +0200 Subject: [PATCH 14/33] add led --- packages/tdev/circuit/components/Circuit.tsx | 10 ++++- .../circuit/components/Nodes/Led/index.tsx | 41 +++++++++++++++++++ .../components/Nodes/Led/styles.module.scss | 0 .../tdev/circuit/components/Nodes/index.ts | 2 + packages/tdev/circuit/models/FlowNode.ts | 4 ++ packages/tdev/circuit/models/derivers/Led.ts | 18 ++++++++ src/api/document.ts | 2 + 7 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/tdev/circuit/components/Nodes/Led/index.tsx create mode 100644 packages/tdev/circuit/components/Nodes/Led/styles.module.scss create mode 100644 packages/tdev/circuit/models/derivers/Led.ts diff --git a/packages/tdev/circuit/components/Circuit.tsx b/packages/tdev/circuit/components/Circuit.tsx index 9f9059d8e..aa5459c18 100644 --- a/packages/tdev/circuit/components/Circuit.tsx +++ b/packages/tdev/circuit/components/Circuit.tsx @@ -17,7 +17,7 @@ import type { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { mdiBatteryChargingHigh, mdiCarBattery, mdiElectricSwitch, mdiWizardHat } from '@mdi/js'; +import { mdiBatteryChargingHigh, mdiCarBattery, mdiElectricSwitch, mdiLedOn, mdiWizardHat } from '@mdi/js'; import Button from '@tdev-components/shared/Button'; import DynamicDocumentRoot from '@tdev-models/documents/DynamicDocumentRoot'; import { NodeType, RoomType } from '@tdev-api/document'; @@ -116,6 +116,14 @@ const Circuit = observer((props: Props): React.ReactNode => { dynamicRoot.room.addFlowNode(NodeType.SwitchNode, { power: 0 }); }} /> +
); }); diff --git a/packages/tdev/circuit/components/Nodes/Battery/styles.module.scss b/packages/tdev/circuit/components/Nodes/Battery/styles.module.scss index e69de29bb..9f3b4ed73 100644 --- a/packages/tdev/circuit/components/Nodes/Battery/styles.module.scss +++ b/packages/tdev/circuit/components/Nodes/Battery/styles.module.scss @@ -0,0 +1,20 @@ +.battery { + .ground { + position: absolute; + border-top: 1px solid var(--ifm-color-danger); + width: var(--data-width); + bottom: 5px; + margin-top: -1px; + margin-left: 6px; + .pin { + position: absolute; + bottom: -22px; + &.pinAdd { + right: -30px; + } + &.pinRemove { + right: 15px; + } + } + } +} diff --git a/packages/tdev/circuit/components/Nodes/Led/index.tsx b/packages/tdev/circuit/components/Nodes/Led/index.tsx index 8e50435c3..7cfb18bbe 100644 --- a/packages/tdev/circuit/components/Nodes/Led/index.tsx +++ b/packages/tdev/circuit/components/Nodes/Led/index.tsx @@ -20,20 +20,22 @@ const LedNode = observer((props: NodeProps) => {
- {/* */} + style={{ left: '28px', bottom: '12px' }} + />
); }); diff --git a/packages/tdev/circuit/models/FlowNode.ts b/packages/tdev/circuit/models/FlowNode.ts index dbe2f4c7b..1b9b34c74 100644 --- a/packages/tdev/circuit/models/FlowNode.ts +++ b/packages/tdev/circuit/models/FlowNode.ts @@ -60,7 +60,7 @@ function createDeriver(node: FlowNode): DeriverMa case NodeType.AndNode: return new And(node as FlowNode) as DeriverMapping[NType]; case NodeType.BatteryNode: - return new Battery(node as FlowNode) as DeriverMapping[NType]; + return new Battery(node as unknown as FlowNode) as DeriverMapping[NType]; case NodeType.LedNode: return new Led(node as FlowNode) as DeriverMapping[NType]; case NodeType.SwitchNode: diff --git a/packages/tdev/circuit/models/derivers/Battery.ts b/packages/tdev/circuit/models/derivers/Battery.ts index 5e7393fab..0d8442b82 100644 --- a/packages/tdev/circuit/models/derivers/Battery.ts +++ b/packages/tdev/circuit/models/derivers/Battery.ts @@ -1,6 +1,8 @@ import iDeriver from './iDeriver'; import type FlowNode from '../FlowNode'; import { NodeType } from '@tdev-api/document'; +import { Source } from '@tdev-models/iDocument'; +import { action, computed } from 'mobx'; class Battery extends iDeriver { constructor(node: FlowNode) { @@ -10,6 +12,45 @@ class Battery extends iDeriver { get power(): number { return 1; } + + get pins(): number { + return (this.flowNode.flowData.data as { pins?: number }).pins ?? 3; + } + + @action + addPin() { + this.flowNode.setData( + { ...this.flowNode.flowData, data: { pins: this.pins + 1 } }, + Source.LOCAL, + new Date() + ); + } + + @computed + get pinIds(): string[] { + return Array.from({ length: this.pins }).map((_, i) => (i === 0 ? 'a' : i === 1 ? 'b' : `p${i}`)); + } + + @action + removePin() { + if (this.pins < 2) { + return; + } + const lastPinId = this.pinIds[this.pinIds.length - 1]; + const lastPin = this.flowNode.edgesIn.find((e) => e.flowData.targetHandle === lastPinId); + if (lastPin) { + lastPin.setData( + { ...lastPin.flowData, targetHandle: this.pinIds[this.pinIds.length - 2] }, + Source.LOCAL, + new Date() + ); + } + this.flowNode.setData( + { ...this.flowNode.flowData, data: { pins: this.pins - 1 } }, + Source.LOCAL, + new Date() + ); + } } export default Battery; diff --git a/src/api/document.ts b/src/api/document.ts index 2b3bb2570..a37861fb7 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -121,7 +121,7 @@ export enum NodeType { export interface NodeDataMapping { [NodeType.LedNode]: {}; - [NodeType.BatteryNode]: {}; + [NodeType.BatteryNode]: { pins: number }; [NodeType.OrNode]: {}; [NodeType.AndNode]: {}; [NodeType.SwitchNode]: { From 67b5d60902adf3f4ddaa5f00bbc72e2692e08668 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 14 Sep 2025 00:01:05 +0200 Subject: [PATCH 16/33] add xor --- packages/tdev/circuit/components/Circuit.tsx | 37 +++++++--- .../circuit/components/Nodes/Xor/index.tsx | 49 +++++++++++++ .../components/Nodes/Xor/styles.module.scss | 71 +++++++++++++++++++ .../tdev/circuit/components/Nodes/index.ts | 2 + .../circuit/components/styles.module.scss | 4 ++ packages/tdev/circuit/models/CircuitRoom.ts | 18 ++++- packages/tdev/circuit/models/FlowNode.ts | 4 ++ packages/tdev/circuit/models/derivers/Xor.ts | 24 +++++++ src/api/document.ts | 2 + src/components/Rooms/index.tsx | 25 +++++-- 10 files changed, 219 insertions(+), 17 deletions(-) create mode 100644 packages/tdev/circuit/components/Nodes/Xor/index.tsx create mode 100644 packages/tdev/circuit/components/Nodes/Xor/styles.module.scss create mode 100644 packages/tdev/circuit/models/derivers/Xor.ts diff --git a/packages/tdev/circuit/components/Circuit.tsx b/packages/tdev/circuit/components/Circuit.tsx index 1160137a7..34bea7240 100644 --- a/packages/tdev/circuit/components/Circuit.tsx +++ b/packages/tdev/circuit/components/Circuit.tsx @@ -13,7 +13,8 @@ import type { Node, OnReconnect, FinalConnectionState, - HandleType + HandleType, + OnNodesDelete } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; @@ -59,6 +60,13 @@ const Circuit = observer((props: Props): React.ReactNode => { }, [dynamicRoot.room] ); + const onNodesDelete = React.useCallback( + (deleted) => { + edgeReconnectSuccessful.current = true; + dynamicRoot.room.onDelete(deleted); + }, + [dynamicRoot.room] + ); const onReconnectStart = React.useCallback(() => { edgeReconnectSuccessful.current = false; }, []); @@ -96,6 +104,7 @@ const Circuit = observer((props: Props): React.ReactNode => { nodeTypes={nodeTypes} nodes={dynamicRoot.room.nodes} edges={dynamicRoot.room.edges} + onNodesDelete={onNodesDelete} onNodesChange={onChange} onEdgesChange={onChangeEdge} onConnect={onConnect} @@ -107,7 +116,7 @@ const Circuit = observer((props: Props): React.ReactNode => { > - +
diff --git a/packages/tdev/circuit/models/CircuitRoom.ts b/packages/tdev/circuit/models/CircuitRoom.ts index aba9b9c66..6fe147f02 100644 --- a/packages/tdev/circuit/models/CircuitRoom.ts +++ b/packages/tdev/circuit/models/CircuitRoom.ts @@ -21,6 +21,7 @@ import { } from '@xyflow/react'; import { Source } from '@tdev-models/iDocument'; import FlowEdge from './FlowEdge'; +const MULTI_INPUTS_ALLOWED = new Set([NodeType.BatteryNode]); class CircuitRoom extends DynamicRoom { constructor(dynamicRoot: DynamicDocumentRoot, documentStore: DocumentStore) { @@ -88,16 +89,26 @@ class CircuitRoom extends DynamicRoom { @action onConnect(connection: Parameters[0]) { - this.documentStore.create({ - documentRootId: this.dynamicRoot.rootDocumentId, - type: DocumentType.FlowEdge, - data: { - source: connection.source, - target: connection.target, - sourceHandle: connection.sourceHandle, - targetHandle: connection.targetHandle - } + const currentEdge = this.flowEdges.find((e) => { + return e.targetId === connection.target && e.flowData.targetHandle === connection.targetHandle; }); + this.documentStore + .create({ + documentRootId: this.dynamicRoot.rootDocumentId, + type: DocumentType.FlowEdge, + data: { + source: connection.source, + target: connection.target, + sourceHandle: connection.sourceHandle, + targetHandle: connection.targetHandle + } + }) + .then((newEdge) => { + const nType = currentEdge?.target?.flowData?.type as NodeType | undefined; + if (newEdge && currentEdge && !MULTI_INPUTS_ALLOWED.has(nType)) { + this.documentStore.apiDelete(currentEdge); + } + }); } @action diff --git a/packages/tdev/circuit/models/FlowEdge.ts b/packages/tdev/circuit/models/FlowEdge.ts index a856017e1..d3e68e72b 100644 --- a/packages/tdev/circuit/models/FlowEdge.ts +++ b/packages/tdev/circuit/models/FlowEdge.ts @@ -79,6 +79,11 @@ class FlowEdge extends iDocument { return { ...this.flowData, animated: this.isPowerOn, + type: + this.source?.flowData.type === NodeType.BatteryNode || + this.target?.flowData.type === NodeType.BatteryNode + ? 'smoothstep' + : 'default', style: { stroke: this.isPowerOn ? this.isGround From a191de6ec4badf7ad941d192ccbec12e00480d1a Mon Sep 17 00:00:00 2001 From: bh0fer Date: Sun, 14 Sep 2025 11:36:05 +0200 Subject: [PATCH 18/33] add not --- packages/tdev/circuit/components/Circuit.tsx | 8 +++ .../circuit/components/Nodes/Not/index.tsx | 49 +++++++++++++++++++ .../components/Nodes/Not/styles.module.scss | 14 ++++++ .../tdev/circuit/components/Nodes/index.ts | 2 + packages/tdev/circuit/models/FlowNode.ts | 6 ++- packages/tdev/circuit/models/derivers/Not.ts | 19 +++++++ src/api/document.ts | 2 + src/stores/DocumentRootStore.ts | 7 ++- 8 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 packages/tdev/circuit/components/Nodes/Not/index.tsx create mode 100644 packages/tdev/circuit/components/Nodes/Not/styles.module.scss create mode 100644 packages/tdev/circuit/models/derivers/Not.ts diff --git a/packages/tdev/circuit/components/Circuit.tsx b/packages/tdev/circuit/components/Circuit.tsx index 34bea7240..97289fd60 100644 --- a/packages/tdev/circuit/components/Circuit.tsx +++ b/packages/tdev/circuit/components/Circuit.tsx @@ -149,6 +149,14 @@ const Circuit = observer((props: Props): React.ReactNode => { dynamicRoot.room.addFlowNode(NodeType.AndNode, {}); }} /> +