diff --git a/src/assets/images/loop.png b/src/assets/images/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..202f18bb75b00da8cc1e9202159f93ec54fafe8d Binary files /dev/null and b/src/assets/images/loop.png differ diff --git a/src/assets/main.css b/src/assets/main.css index 4658c7e602c8f3b22aed0f58fc5fc8c793ca1ce8..b1c4973c8510ba5f10a84e48e6e25fc2b372abcc 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -45,6 +45,8 @@ --node-control-splitter-color-dark: #ff8800; --node-control-andmerger-color: #00a2ff; --node-control-andmerger-color-dark: #0089d8; + --node-control-loop-repeat-color: #3b9743; + --node-control-loop-repeat-color-dark: #1f6926; --use-gradient: 1; --edge-scheduled-deletion-max: #ff0000; diff --git a/src/components/main/editorbar/ControlBar.vue b/src/components/main/editorbar/ControlBar.vue index f9493f99ae0d61cabd6ed019d367b4d0f8d9c6fd..545d2b3e398c423fb7d468f8825474e40bf8b971 100644 --- a/src/components/main/editorbar/ControlBar.vue +++ b/src/components/main/editorbar/ControlBar.vue @@ -35,6 +35,9 @@ export default { }, eventType(): BaseType { return this.typeService.getEventType() + }, + integerType(): BaseType { + return this.typeService.getIntegerType() } } } @@ -44,7 +47,7 @@ export default { <div id="blocks-bar"> <div id="block-entries"> <NodeEntry - v-for="entry in controlNodes" + v-for="entry in [...controlNodes]" :key="entry[0].id.id" :color-unselected="getCSSVar(entry[2])" :color-selected="getCSSVar(entry[3])" diff --git a/src/conversion/Cloner.ts b/src/conversion/Cloner.ts index e72093d90085c0741523e5abeba12c6a64953470..b3306d9163cac6062b8bbf8708793ef6f95ba544 100644 --- a/src/conversion/Cloner.ts +++ b/src/conversion/Cloner.ts @@ -1,4 +1,4 @@ -import { type ControlNode, type Node, NodeType, type Parameter, type ParameterNode, type SubSkill } from '@/datatypes' +import { type ControlNode, type ControlSlottedNode, type Node, NodeType, type Parameter, type ParameterNode, type SubSkill } from '@/datatypes' import { v4 as uuidv4 } from 'uuid' export default class Cloner { @@ -39,6 +39,20 @@ export default class Cloner { }) } + if (controlNode.isSlotted) { + return { + nodeType: node.nodeType, + id: { id: uuidv4(), hint: node.name }, + name: node.name, + position: { x: node.position.x, y: node.position.y }, + description: controlNode.description, + parameters: newParams, + controlType: controlNode.controlType, + isSlotted: controlNode.isSlotted, + slot: undefined // do not clone the node in the slot + } as ControlSlottedNode + } + return { nodeType: node.nodeType, id: { id: uuidv4(), hint: node.name }, @@ -46,12 +60,16 @@ export default class Cloner { position: { x: node.position.x, y: node.position.y }, description: controlNode.description, parameters: newParams, - controlType: controlNode.controlType + controlType: controlNode.controlType, + isSlotted: controlNode.isSlotted } as ControlNode } case NodeType.VALUE: { throw new Error('Cloner: cloneNode: cloning of Value Node is not supported yet.') } + default: { + throw new Error('Cloner: cloneNode: unknown node type.') + } } } } diff --git a/src/conversion/Converter.ts b/src/conversion/Converter.ts index b7881d667310ff1474c35c36c404b300cf647bd2..a687fb65d0ef475200764180f03f594c3288f0ba 100644 --- a/src/conversion/Converter.ts +++ b/src/conversion/Converter.ts @@ -6,6 +6,8 @@ import { type BaseTypeDataTransfer, type ControlNode, type ControlNodeDataTransfer, + type ControlSlottedNode, + ControlType, DataIdentifier, type DictData, type DictDataDataTransfer, @@ -606,7 +608,8 @@ export default class Converter { * @returns - The converted control node. */ public convertControlNodeDataTransferToControlNode( - controlNode: ControlNodeDataTransfer + controlNode: ControlNodeDataTransfer, + skill: Skill ): Result<ControlNode, BaseError> { const parameters = [] for (const parameterDT of controlNode.parameters) { @@ -619,6 +622,45 @@ export default class Converter { } parameters.push(parameter.result) } + + const isSlotted = controlNode.controlType === ControlType.LOOP_REPEAT || controlNode.controlType === ControlType.LOOP_RETRY + let slot + if (isSlotted && controlNode.slottedNodeId !== undefined && controlNode.slottedNodeId !== null) { + if (controlNode.slottedNodeId.id !== '') { + slot = { id: controlNode.slottedNodeId.id, parent: { id: skill.id } } + } + } + let name = '' + + switch (controlNode.controlType) { + case ControlType.LOOP_REPEAT: + name = 'Loop Repeat' + break + case ControlType.LOOP_RETRY: + name = 'Loop Retry' + break + default: + name = 'Unknown Control Node' + break + } + + if (isSlotted === true) { + return { + success: true, + result: { + parameters, + description: '', + nodeType: controlNode.nodeType, + id: { id: controlNode.nodeId }, + name, + position: { x: controlNode.xPos, y: controlNode.yPos }, + controlType: controlNode.controlType, + isSlotted: true, + slot + } as ControlSlottedNode + } + } + return { success: true, result: { @@ -626,9 +668,10 @@ export default class Converter { description: '', nodeType: controlNode.nodeType, id: { id: controlNode.nodeId }, - name: '', + name, position: { x: controlNode.xPos, y: controlNode.yPos }, - controlType: controlNode.controlType + controlType: controlNode.controlType, + isSlotted: false } } } @@ -1263,6 +1306,19 @@ export default class Converter { public convertControlNodeToControlNodeDataTransfer( controlNode: ControlNode ): ControlNodeDataTransfer { + const slot = { + 'py/object': pyTypes.IDENTIFICATOR, + id: '', + hint: '' + } + + if (controlNode.isSlotted) { + const controlSlottedNode = controlNode as ControlSlottedNode + if (controlSlottedNode.slot) { + slot.id = controlSlottedNode.slot.id + } + } + return { 'py/object': pyTypes.CONTROL_NODE, nodeId: controlNode.id.id, @@ -1272,7 +1328,8 @@ export default class Converter { parameters: controlNode.parameters.map((p) => this.convertParameterToParameterDataTransfer(p) ), - controlType: controlNode.controlType + controlType: controlNode.controlType, + slottedNodeId: slot } } @@ -1457,7 +1514,7 @@ export default class Converter { break } case NodeType.CONTROL: { - result = this.convertControlNodeDataTransferToControlNode(node as ControlNodeDataTransfer) + result = this.convertControlNodeDataTransferToControlNode(node as ControlNodeDataTransfer, skill) if (!result.success) { result.error.addContext(`From control node with ID: "${node.nodeId}" `) return result diff --git a/src/conversion/Updater.ts b/src/conversion/Updater.ts index 5ff2944d26a66f8e6d28f1af6cd1b32267725265..b01cc5d4cb094af1c3e130d03879aaa4c3aba6fd 100644 --- a/src/conversion/Updater.ts +++ b/src/conversion/Updater.ts @@ -484,7 +484,7 @@ export default class Updater { const newParameter = newNode as ControlNodeDataTransfer if (node === undefined) { // add - const result = this.converter.convertControlNodeDataTransferToControlNode(newParameter) + const result = this.converter.convertControlNodeDataTransferToControlNode(newParameter, parentSkill) if (!result.success) { parentSkill.corrupted = true throw result.error diff --git a/src/datatypes/index.ts b/src/datatypes/index.ts index 582dd09ac0410b4cccbb922c70db1fc2c2855bfa..1c09cac31eca2f968ca145862df2548b21f12f82 100644 --- a/src/datatypes/index.ts +++ b/src/datatypes/index.ts @@ -7,6 +7,7 @@ import type BaseData from './model/BaseData' import type BaseType from './model/BaseType' import type Cell from './model/Cell' import type ControlNode from './model/ControlNode' +import type ControlSlottedNode from './model/ControlSlottedNode' import type DictData from './model/DictData' import type DoubleGenericType from './model/DoubleGenericType' import type Edge from './model/Edge' @@ -86,6 +87,7 @@ export type { ControlGraphicNode, ControlNode, ControlNodeDataTransfer, + ControlSlottedNode, DefaultSubSkillGraphicNode, DictData, DictDataDataTransfer, diff --git a/src/datatypes/model/ControlNode.ts b/src/datatypes/model/ControlNode.ts index b0f64b3999cd51a64c82b7433f81d9e8395dd139..5f71ff4d4ff266329e3ec2fc8e38a38385f2685f 100644 --- a/src/datatypes/model/ControlNode.ts +++ b/src/datatypes/model/ControlNode.ts @@ -6,4 +6,5 @@ export default interface ControlNode extends Node { controlType: ControlType parameters: Parameter[] description: string + isSlotted: boolean } diff --git a/src/datatypes/model/ControlSlottedNode.ts b/src/datatypes/model/ControlSlottedNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcf120dd7c33e39211166e640f2891502b7afceb --- /dev/null +++ b/src/datatypes/model/ControlSlottedNode.ts @@ -0,0 +1,8 @@ +import type { ControlNode, Identificator } from '@/datatypes' + +/** + * A control slotted node is a node that can have a slot. + */ +export default interface ControlSlottedNode extends ControlNode { + slot: Identificator | undefined +} diff --git a/src/datatypes/model/ControlType.ts b/src/datatypes/model/ControlType.ts index 91249a5a2c1ac4d5c357954127a724c1df20d04c..7b4daad6a8dbb2467fc1837d567033b036e9bb7c 100644 --- a/src/datatypes/model/ControlType.ts +++ b/src/datatypes/model/ControlType.ts @@ -2,5 +2,7 @@ // see https://thisthat.dev/const-enum-vs-enum/#:~:text=Because%20there%20is%20no%20JavaScript%20object%20that%20associates%20with%20const%20enum%20is%20generated%20at%20run%20time%2C%20it%20is%20not%20possible%20to%20loop%20over%20the%20const%20enum%20values. export enum ControlType { SPLITTER = 'SPLITTER', - AND_MERGER = 'AND_MERGER' + AND_MERGER = 'AND_MERGER', + LOOP_REPEAT = 'LOOP_REPEAT', + LOOP_RETRY = 'LOOP_RETRY' } diff --git a/src/datatypes/transfer/ControlNodeDataTransfer.ts b/src/datatypes/transfer/ControlNodeDataTransfer.ts index 0058822912d6cd422ba0d27eab9044483553c0e6..1b9d225b184964b35992e43eb77fb854a64afdcd 100644 --- a/src/datatypes/transfer/ControlNodeDataTransfer.ts +++ b/src/datatypes/transfer/ControlNodeDataTransfer.ts @@ -1,8 +1,10 @@ import type { ControlType } from '../model/ControlType' +import type IdentificatorDataTransfer from './IdentificatorDataTransfer' import type NodeDataTransfer from './NodeDataTransfer' import type ParameterDataTransfer from './ParameterDataTransfer' export default interface ControlNodeDataTransfer extends NodeDataTransfer { controlType: ControlType parameters: ParameterDataTransfer[] + slottedNodeId: IdentificatorDataTransfer } diff --git a/src/render/GraphicNode.ts b/src/render/GraphicNode.ts index 1d8f258f3ae09cad613f11980f8e711d9b43047a..ac9fe38409aa5c708621b95f0f6d937581618be8 100644 --- a/src/render/GraphicNode.ts +++ b/src/render/GraphicNode.ts @@ -2,8 +2,10 @@ import type SessionService from '@/services/SessionService' import type SkillService from '@/services/SkillService' import type StatechartService from '@/services/StatechartService' import type TypeService from '@/services/TypeService' +import type { Vector2 } from 'three' import type Deletable from './Deletable' import type GraphicEdge from './GraphicEdge' +import type ControlSlottedGraphicNode from './graphicnodes/control/slotted/ControlSlottedGraphicNode' import type InputEventContext from './input/InputEventContext' import type InputEventListener from './input/InputEventListener' import { @@ -48,6 +50,7 @@ export default abstract class GraphicNode public subSkillIconSize = 27 public subSkillIconRightDist = 35 public subSkillIconTopDist = 25 + public borderRadius = [10, 10, 10, 10] public readonly isGraphicNode = true public readonly isGraphicPort = false @@ -56,6 +59,7 @@ export default abstract class GraphicNode public readonly clickable = true public readonly deletable = true public readonly isComposed = true + public slottable = false public skillService: SkillService | undefined public statechartService: StatechartService | undefined @@ -67,6 +71,9 @@ export default abstract class GraphicNode private highlightRects: GraphicObject[] = [] private selectionCorners: GraphicObject[] = [] private selected = false + public slot: ControlSlottedGraphicNode | undefined + private hoveredSlot: GraphicObject | undefined + private lastSlottedGraphicNode: ControlSlottedGraphicNode | undefined public actualPosition: Vector3 = new Vector3() /** @@ -118,7 +125,7 @@ export default abstract class GraphicNode * @param ctx - The input event context. */ public onMouseUp(ctx: InputEventContext) { - if (ctx.mutex === undefined || ctx.mutex === false || ctx.tab === undefined) return + if (ctx.mutex === undefined || ctx.mutex === false || ctx.tab === undefined || ctx.graphicEdge !== undefined) return // if selected and this is the only node selected, deselect it if (this.selected === true) { @@ -127,6 +134,11 @@ export default abstract class GraphicNode this.selected = false } } + + if (this.slottable === true) { + this.putInSlot() + } + this.deleteGhostNode() } /** @@ -134,8 +146,63 @@ export default abstract class GraphicNode * @param ctx - The input event context. */ public onMouseDrag(ctx: InputEventContext) { - if (ctx.delta === undefined || ctx.mutex === undefined || ctx.mutex === false) return - this.actualPosition.add(new Vector3(ctx.delta.x, ctx.delta.y, 0)) + if (ctx.delta === undefined || ctx.mutex === undefined || ctx.mutex === false || ctx.position === undefined) return + + this.moveBy(ctx.delta) + + const renderer = this.statechartService?.getRenderer() + + if (this.slot !== undefined) { + this.removeFromSlot() + } else if (renderer !== undefined) { + const intersections = renderer.computeIntersects() + const slotIntersection = intersections.find((i) => i.object.name === 'slot') + if (slotIntersection !== undefined) { + const slottedGraphicNode = GraphicObject.getHighestParent(slotIntersection.object as GraphicObject) as ControlSlottedGraphicNode + if (slottedGraphicNode.slottedGraphicNode === undefined && GraphicObject.getHighestParent(slotIntersection.object as GraphicObject) !== this && this.slottable === true && slottedGraphicNode.ghostNode === undefined) { + slottedGraphicNode.slotGhostNode(this) + this.hoveredSlot = slottedGraphicNode.slotBackground + + if (slottedGraphicNode !== this.lastSlottedGraphicNode) { + this.deleteGhostNode() + } + this.lastSlottedGraphicNode = slottedGraphicNode + } + } else { + this.deleteGhostNode() + this.hoveredSlot = undefined + } + } + + if (this.node.nodeType === NodeType.CONTROL) { + const controlNode = this.node as ControlNode + if (controlNode.isSlotted === true) { + const slottedGraphicNode = this as unknown as ControlSlottedGraphicNode + slottedGraphicNode.slottedGraphicNode?.syncNodePos(true) + slottedGraphicNode.slottedGraphicNode?.updateGizmos(true) + } + } + + this.updateGizmos() + } + + /** + * Move the graphic node to a position. The underlying node is moved as well, and the edges of the graphic ports are updated. + * @param position - The position to move the graphic node to. + */ + public moveTo(position: Vector2, z: number | undefined, saveAsWorld = false) { + this.actualPosition.set(position.x, position.y, z ?? this.position.z) + this.position.set(position.x, position.y, z ?? this.position.z) + + this.syncNodePos(saveAsWorld) + } + + /** + * Move the graphic node by a delta. The underlying node is moved as well, and the edges of the graphic ports are updated. + * @param delta - The delta by which to move this graphic node. + */ + public moveBy(delta: Vector2, saveAsWorld = false) { + this.actualPosition.add(new Vector3(delta.x, delta.y, 0)) if (this.sessionService?.getSettings().snapToGrid === true) { this.snapToGrid() @@ -144,16 +211,72 @@ export default abstract class GraphicNode this.position.y = this.actualPosition.y } + this.syncNodePos(saveAsWorld) + } + + /** + * Sync the position of the underlying node with the graphic node. + * @param saveAsWorld - Whether to save the position as world position or local position. + */ + public syncNodePos(saveAsWorld = false) { + let pos = this.position + + if (saveAsWorld === true) { + pos = this.localToWorld(this.position.clone()) + } + this.node.position = { - x: this.position.x, - y: this.position.y + x: pos.x, + y: pos.y } for (const graphicPort of this.graphicPorts) { graphicPort.updateEdges() } + } - this.updateGizmos() + /** + * Unslots the node from its slot. + */ + public removeFromSlot() { + if (this.slot === undefined) { + return + } + + this.slot.unslotGraphicNode() + this.slot = undefined + + this.borderRadius = [10, 10, 10, 10] + this.updateBackground() + this.deleteHighlightRects() + this.drawHighlightRects() + } + + /** + * Puts the node in the hovered slot. The node is added as a child of the slot. + */ + public putInSlot() { + if (this.hoveredSlot === undefined) { + return + } + const slottedNode = GraphicObject.getHighestParent(this.hoveredSlot) as ControlSlottedGraphicNode + this.slot = slottedNode + slottedNode.slotGraphicNode(this) + this.transferEdgesToSlot() + + this.borderRadius = [0, 0, 10, 10] + this.updateBackground() + this.deleteHighlightRects() + this.drawHighlightRects() + this.updateGizmos(true) + } + + public deleteGhostNode() { + if (this.lastSlottedGraphicNode === undefined) { + return + } + this.lastSlottedGraphicNode.unslotGhostNode() + this.lastSlottedGraphicNode = undefined } /** @@ -211,6 +334,31 @@ export default abstract class GraphicNode return false } + public bringForward() { + if (this.statechartService === undefined) { + return + } + + const renderer = this.statechartService.getRenderer() + + if (renderer === undefined || renderer.current === undefined) { + return + } + + const nodes = renderer.current[0].statechart.graphicNodes.sort((a, b) => { + return a.position.z - b.position.z + }) + + const index = nodes.findIndex((node) => node.node.id.id === this.node.id.id) + this.position.z = nodes[nodes.length - 1].position.z + this.updateGizmos() + + for (let i = index + 1; i < nodes.length; i++) { + nodes[i].position.z -= 20 + nodes[i].updateGizmos() + } + } + /** * Snaps the graphic node position to the grid. */ @@ -412,6 +560,10 @@ export default abstract class GraphicNode const graphicPort = new GraphicPort(inputPositions[i], port, true, false) graphicPort.statechartService = this.statechartService + for (let k = 0; k < connectedGraphicEdges.length; k++) { + const edge = connectedGraphicEdges[k] + edge.toPort = graphicPort + } result.push(graphicPort) this.add(graphicPort.assemble()) } @@ -435,7 +587,10 @@ export default abstract class GraphicNode const graphicPort = new GraphicPort(outputPositions[i], port, true, false) graphicPort.statechartService = this.statechartService - + for (let k = 0; k < connectedGraphicEdges.length; k++) { + const edge = connectedGraphicEdges[k] + edge.fromPort = graphicPort + } result.push(graphicPort) this.add(graphicPort.assemble()) } @@ -462,7 +617,14 @@ export default abstract class GraphicNode true ) graphicPort.statechartService = this.statechartService - + const edge = connectedGraphicEdges[0] + if (edge !== undefined) { + if (edge.fromPort !== undefined) { + edge.fromPort = graphicPort + } else if (edge.toPort !== undefined) { + edge.toPort = graphicPort + } + } result.push(graphicPort) this.add(graphicPort.assemble()) break @@ -493,9 +655,12 @@ export default abstract class GraphicNode graphicNode: this } - const graphicPort = new GraphicPort(inputPositions[i], port, !!this.sessionService?.getSettings().debug, false) + const graphicPort = new GraphicPort(inputPositions[i], port, !!this.sessionService?.getSettings().debug || node.isSlotted, false) graphicPort.statechartService = this.statechartService - + for (let k = 0; k < connectedGraphicEdges.length; k++) { + const edge = connectedGraphicEdges[k] + edge.toPort = graphicPort + } result.push(graphicPort) this.add(graphicPort.assemble()) } @@ -517,9 +682,12 @@ export default abstract class GraphicNode graphicNode: this } - const graphicPort = new GraphicPort(outputPositions[i], port, !!this.sessionService?.getSettings().debug, false) + const graphicPort = new GraphicPort(outputPositions[i], port, !!this.sessionService?.getSettings().debug || node.isSlotted, false) graphicPort.statechartService = this.statechartService - + for (let k = 0; k < connectedGraphicEdges.length; k++) { + const edge = connectedGraphicEdges[k] + edge.fromPort = graphicPort + } result.push(graphicPort) this.add(graphicPort.assemble()) } @@ -637,34 +805,41 @@ export default abstract class GraphicNode /** * Updates the gizmos. */ - public updateGizmos() { - if (this.sessionService === undefined || this.sessionService.getSettings().debug === false) + public updateGizmos(convertToWorld = false) { + if (this.sessionService === undefined || this.sessionService.getSettings().debug === false) { return + } + + let pos = this.position + + if (convertToWorld === true) { + pos = this.localToWorld(this.position.clone()) + } if (this.gizmos.length === 0) { const xpos = new TextGraphicObject( - new Vector3(this.width / -2, this.height / 2 + 60, 0), + new Vector3(this.width / -2, (this.slot === undefined ? this.height / 2 + 60 : this.height / -2 - 20), 0), TextJustification.LEFT, 15, - `X ${this.position.x.toFixed(1)}`, + `X ${pos.x.toFixed(1)}`, new Color(Renderer.getCSSVar('--primary-text-color')) ) xpos.assemble() const ypos = new TextGraphicObject( - new Vector3(this.width / -2, this.height / 2 + 40, 0), + new Vector3(this.width / -2, (this.slot === undefined ? this.height / 2 + 40 : this.height / -2 - 40), 0), TextJustification.LEFT, 15, - `Y ${this.position.y.toFixed(1)}`, + `Y ${pos.y.toFixed(1)}`, new Color(Renderer.getCSSVar('--primary-text-color')) ) ypos.assemble() const zpos = new TextGraphicObject( - new Vector3(this.width / -2, this.height / 2 + 20, 0), + new Vector3(this.width / -2, (this.slot === undefined ? this.height / 2 + 20 : this.height / -2 - 60), 0), TextJustification.LEFT, 15, - `Z ${this.position.z.toFixed(1)}`, + `Z ${pos.z.toFixed(1)}`, new Color(Renderer.getCSSVar('--primary-text-color')) ) @@ -685,9 +860,9 @@ export default abstract class GraphicNode y.textObject !== undefined && z.textObject !== undefined ) { - x.textObject.text = `X ${this.position.x.toFixed(1)}` - y.textObject.text = `Y ${this.position.y.toFixed(1)}` - z.textObject.text = `Z ${this.position.z.toFixed(1)}` + x.textObject.text = `X ${pos.x.toFixed(1)}` + y.textObject.text = `Y ${pos.y.toFixed(1)}` + z.textObject.text = `Z ${pos.z.toFixed(1)}` x.textObject.sync() y.textObject.sync() @@ -696,6 +871,30 @@ export default abstract class GraphicNode } } + public transferEdgesToSlot() { + this.transferInputEdgesToSlot() + } + + private transferInputEdgesToSlot() { + if (this.slot === undefined || this.node.nodeType !== NodeType.SUBSKILL) { + return + } + + const slotStartPort = this.slot.graphicPorts.find((gp) => gp.port.parameter.name.toLowerCase() === 'start') + const selfStartPort = this.graphicPorts.find((gp) => gp.port.parameter.name.toLowerCase() === 'start') + + if (slotStartPort === undefined || selfStartPort === undefined) { + NotificationManager.error('Could not find either self start port or slot start port') + return + } + + for (let i = selfStartPort.port.connectedGraphicEdges.length - 1; i >= 0; i--) { + const graphicEdge = selfStartPort.port.connectedGraphicEdges[i] + selfStartPort.disconnectEdge(graphicEdge) + slotStartPort.connectEdge(graphicEdge) + } + } + /** * Draws the selection corners. * @param color - The color of the corners. @@ -751,20 +950,21 @@ export default abstract class GraphicNode const n = 20 const lambda = Math.log(100) / (n + 1) - const startRadius = 10 - const endRadius = 25 const color = new Color(Renderer.getCSSVar('--node-outline-color')) for (let i = 0; i < n; i++) { const opacity = 0.5 * Math.E ** (-lambda * i) - const t = i / n + const radiusTL = this.borderRadius[0] + 7 + i / 2 + const radiusTR = this.borderRadius[1] + 7 + i / 2 + const radiusBR = this.borderRadius[2] + 7 + i / 2 + const radiusBL = this.borderRadius[3] + 7 + i / 2 const rect = new Rectangle( - this.width + 10 + i ** 1.1, - this.height + 10 + i ** 1.1, + this.width + 14 + i, + this.height + 14 + i, color, 0, color, - (1 - t) * startRadius + t * endRadius + [radiusTL, radiusTR, radiusBR, radiusBL] ).assemble() rect.materials[0].opacity = opacity @@ -774,6 +974,30 @@ export default abstract class GraphicNode } } + public updateBackground() { + const oldBackground = this.getObjectByName('background') + if (oldBackground === undefined) { + NotificationManager.warning('Trying to update node background but found none') + return + } + + GraphicObject.free(oldBackground as GraphicObject) + + const background = new Rectangle( + this.width, + this.height, + new Color(Renderer.getCSSVar('--primary-color')), + 7, + new Color(Renderer.getCSSVar('--node-outline-color')), + this.borderRadius, + false, + false + ).assemble() + + background.name = 'background' + this.add(background) + } + /** * Deletes the highlight rectangles. */ diff --git a/src/render/GraphicPort.ts b/src/render/GraphicPort.ts index 5a91fd01179a6ee91eb90d6b658a57911ab89813..57c3027498ff314254f7a4753b56d91d8fed2b2b 100644 --- a/src/render/GraphicPort.ts +++ b/src/render/GraphicPort.ts @@ -10,8 +10,10 @@ import GraphicEdge from './GraphicEdge' import Renderer from './Renderer' import TextGraphicObject from './shapes/TextGraphicObject' import { TextJustification } from './shapes/TextJustification' + /** - * Points that are part of the GraghicNode. + * Graphic ports represent the input/output ports of a graphic node. They consist of a circle, which is instanced for performance and thus needs + * to be handled differently to regular meshes. They also consist of text that represents the parameter's name. */ export default class GraphicPort extends GraphicObject { public currentEdge: GraphicEdge | undefined @@ -219,16 +221,6 @@ export default class GraphicPort extends GraphicObject { ctx.graphicEdge.fromPort.port.connectedGraphicEdges.forEach((ge) => ge.setScheduledDeletion(false)) } - /** - * Handles the double click event. - */ - public onMouseDbClick(): void {} - - /** - * Handles the mouse move event. - */ - public onMouseMove(): void {} - /** * Updates the position of the "from" and "to" of all edges. */ @@ -250,6 +242,19 @@ export default class GraphicPort extends GraphicObject { * @param graphicEdge - The edge to connect. */ public connectEdge(graphicEdge: GraphicEdge) { + if (this.isInput() === true) { + graphicEdge.toPort = this + if (graphicEdge.edge !== undefined) { + graphicEdge.edge.toNode = this.port.graphicNode.node + graphicEdge.edge.toParameter = this.port.parameter + } + } else { + graphicEdge.fromPort = this + if (graphicEdge.edge !== undefined) { + graphicEdge.edge.fromNode = this.port.graphicNode.node + graphicEdge.edge.fromParameter = this.port.parameter + } + } if (graphicEdge.fromPort !== undefined && graphicEdge.toPort !== undefined) { graphicEdge.update( graphicEdge.fromPort.getSnapPosition(), @@ -270,11 +275,13 @@ export default class GraphicPort extends GraphicObject { * @param graphicEdge - The graphic edge to disconnect. */ public disconnectEdge(graphicEdge: GraphicEdge) { - const index = this.port.connectedGraphicEdges.indexOf(graphicEdge) + const index = this.port.connectedGraphicEdges.findIndex((ge) => + ge.fromPort?.port.parameter.id.id === graphicEdge.fromPort?.port.parameter.id.id && + ge.toPort?.port.parameter.id.id === graphicEdge.toPort?.port.parameter.id.id) if (index === -1) { NotificationManager.error('Could not find edge to disconnect') - return + return } this.port.connectedGraphicEdges.splice(index, 1) @@ -286,16 +293,8 @@ export default class GraphicPort extends GraphicObject { * @returns A Vector3. */ public getSnapPosition(): Vector3 { - const snap = this.getObjectByName('snap') const pos = new Vector3() - - if (snap === undefined) { - this.getWorldPosition(pos) - return pos - } - - snap.getWorldPosition(pos) - return pos + return this.getWorldPosition(pos) } /** @@ -480,4 +479,14 @@ export default class GraphicPort extends GraphicObject { this.edgeExits(portA.port, portB.port) ) } + + /** + * Handles the double click event. + */ + public onMouseDbClick(): void {} + + /** + * Handles the mouse move event. + */ + public onMouseMove(): void {} } diff --git a/src/render/Renderer.ts b/src/render/Renderer.ts index b527d9aa0de26e4602a1e4efe3aaa84d4497e434..adfa6bb6de46c5f4bdd0834f364f0c900ad48192 100644 --- a/src/render/Renderer.ts +++ b/src/render/Renderer.ts @@ -6,11 +6,12 @@ import type StateService from '@/services/StateService' import type TypeService from '@/services/TypeService' import type GraphicEdge from './GraphicEdge' import type GraphicNode from './GraphicNode' +import type ControlSlottedGraphicNode from './graphicnodes/control/slotted/ControlSlottedGraphicNode' import type GraphicPort from './GraphicPort' import type InputEventContext from './input/InputEventContext' import type LoadedResources from './LoadedResources' import type Tool from './tools/Tool' -import { GraphicObject, NodeType, type Tab } from '@/datatypes' +import { type ControlNode, type ControlSlottedNode, GraphicObject, NodeType, type Tab } from '@/datatypes' import NotificationManager from '@/notification/NotificationManager' import { Color, @@ -242,12 +243,32 @@ export default class Renderer { const elapsed = this.now - this.lastFrame this.stats.begin() + let skippedRender = false + let edgeOnFrameRequestRender = false + if (this.current !== undefined) { + for (let i = 0; i < this.current[0].statechart.graphicEdges.length; i++) { + const edge = this.current[0].statechart.graphicEdges[i] + edge.onFrame() + if (edge.scheduledDeletion === true) { + edgeOnFrameRequestRender = true + } + } + } + if (elapsed > this.fpsInterval - this.fpsTolerance && this.loaded === true && this.current !== undefined && this.renderRequested === true) { this.lastFrame = this.now - (elapsed % this.fpsInterval) this.composer.render() this.frameCount++ this.renderRequested = false + } else { + skippedRender = true + } + + if (elapsed > this.fpsInterval - this.fpsTolerance && this.loaded === true && this.current !== undefined && skippedRender === true && edgeOnFrameRequestRender === true) { + this.lastFrame = this.now - (elapsed % this.fpsInterval) + this.composer.render() + this.frameCount++ } this.stats.end() } @@ -509,12 +530,6 @@ export default class Renderer { .getRenderStrategy() .generateGraphicEdges(tab.skill, tab.statechart.graphicNodes, this.statechartService) - for (const graphicEdge of tab.statechart.graphicEdges) { - scene.add(graphicEdge.assemble()) - graphicEdge.fromPort?.updateColor() - graphicEdge.toPort?.updateColor() - } - if (tab.skill.native === true) { return } @@ -524,6 +539,47 @@ export default class Renderer { graphicPort.updateImplicitEdge() } } + + // assign slots for slotted graphic nodes + for (let i = 0; i < tab.statechart.graphicNodes.length; i++) { + const graphicNode = tab.statechart.graphicNodes[i] + if (graphicNode.node.nodeType !== NodeType.CONTROL) { + continue + } + + const controlNode = graphicNode.node as ControlNode + if (controlNode.isSlotted === false) { + continue + } + + const slottedControlNode = controlNode as ControlSlottedNode + if (slottedControlNode.slot === undefined) { + continue + } + + const idToCheck = slottedControlNode.slot.id + if (idToCheck === undefined) { + continue + } + + const slot = tab.statechart.graphicNodes.find((graphicNode) => { + return graphicNode.node.id.id === idToCheck + }) + + if (slot === undefined) { + NotificationManager.error(`Could not find slotted node with id ${idToCheck}`) + continue + } + + const slottedGraphicNode = graphicNode as ControlSlottedGraphicNode + slottedGraphicNode.slotGraphicNode(slot) + } + + for (const graphicEdge of tab.statechart.graphicEdges) { + scene.add(graphicEdge.assemble()) + graphicEdge.fromPort?.updateColor() + graphicEdge.toPort?.updateColor() + } } /** @@ -552,10 +608,10 @@ export default class Renderer { * Computes the intersections on the pointer position. * @returns The intersections. */ - private computeIntersects(): Intersection<Object3D>[] { + public computeIntersects(pos?: Vector2): Intersection<Object3D>[] { if (this.current === undefined) return [] - this.raycaster.setFromCamera(this.pointer, this.camera) + this.raycaster.setFromCamera(pos ?? this.pointer, this.camera) const toCheck: Object3D[] = [] // setup an instance of Frustum based on the projection and view matrix of camera @@ -763,7 +819,7 @@ export default class Renderer { const c = candidate as GraphicObject if (c.name === 'instancedCircles' || c.name === 'instancedOutlines') break - if (c.name === 'background' || c.name === 'button-background') { + if (c.name === 'background' || c.name === 'button-background' || c.name === 'slot' || c.name === 'slotBackground') { this.hoveredPort = undefined return } @@ -773,9 +829,8 @@ export default class Renderer { let closestPort: [GraphicObject, number] | undefined this.current[1].traverse((obj) => { - if ((obj as GraphicObject).isGraphicPort === true) { - const port = obj as GraphicObject - + const port = obj as GraphicObject + if (port.isGraphicPort === true) { // get world position of port const portPos = new Vector3() port.getWorldPosition(portPos) @@ -858,9 +913,10 @@ export default class Renderer { if (candidate) { while (candidate.parent && candidate.parent !== this.current[1]) { candidate = candidate.parent + const candidateGraphicsObj = candidate as GraphicObject - if ((candidate as GraphicObject).clickable) { - ;(candidate as GraphicObject).onMouseDbClick({ + if (candidateGraphicsObj.clickable) { + candidateGraphicsObj.onMouseDbClick({ button: event.button }) } diff --git a/src/render/graphicnodes/ControlFlowSubSkillGraphicNode.ts b/src/render/graphicnodes/ControlFlowSubSkillGraphicNode.ts index e2444eed728c1a9a8b419646798b0fd3bc4554ae..a79f2dc753a649f3e56d6e6fe0301ef62b6bd795 100644 --- a/src/render/graphicnodes/ControlFlowSubSkillGraphicNode.ts +++ b/src/render/graphicnodes/ControlFlowSubSkillGraphicNode.ts @@ -15,6 +15,7 @@ import { TextJustification } from '../shapes/TextJustification' export default class ControlFlowSubSkillGraphicNode extends GraphicNode { private tempHeight = this.height private tempWidth = this.width + public slottable = true /** * Constructor for a default sub skill graphic node. @@ -82,7 +83,9 @@ export default class ControlFlowSubSkillGraphicNode extends GraphicNode { new Color(Renderer.getCSSVar('--primary-color')), 7, new Color(Renderer.getCSSVar('--node-outline-color')), - 10 + this.borderRadius, + false, + false ).assemble() background.name = 'background' @@ -131,8 +134,8 @@ export default class ControlFlowSubSkillGraphicNode extends GraphicNode { const size = new Vector3() bounds.getSize(size) - const left = -size.x / 2 - 3.5 - const right = size.x / 2 + 3.5 + const left = -size.x / 2 + 3.5 + const right = size.x / 2 - 3.5 const top = size.y / 2 for (let i = 0; i < this.inputs.length; i++) { diff --git a/src/render/graphicnodes/DataFlowSubSkillNode.ts b/src/render/graphicnodes/DataFlowSubSkillNode.ts index e5bece0c9afebc96cc588519b0f818a296b7df9f..5d6f4c47942824c286224f0cfa7254b9d50e7a39 100644 --- a/src/render/graphicnodes/DataFlowSubSkillNode.ts +++ b/src/render/graphicnodes/DataFlowSubSkillNode.ts @@ -15,6 +15,7 @@ import { TextJustification } from '../shapes/TextJustification' export default class DataFlowSubSkillNode extends GraphicNode { private tempHeight = this.height private tempWidth = this.width + public slottable = true /** * Constructor for a default sub skill graphic node. @@ -85,7 +86,9 @@ export default class DataFlowSubSkillNode extends GraphicNode { new Color(Renderer.getCSSVar('--primary-color')), 7, new Color(Renderer.getCSSVar('--node-outline-color')), - 10 + this.borderRadius, + false, + false ).assemble() background.name = 'background' @@ -134,8 +137,8 @@ export default class DataFlowSubSkillNode extends GraphicNode { const size = new Vector3() bounds.getSize(size) - const left = -size.x / 2 - 3.5 - const right = size.x / 2 + 3.5 + const left = -size.x / 2 + 3.5 + const right = size.x / 2 - 3.5 const top = size.y / 2 for (let i = 0; i < this.inputs.length; i++) { diff --git a/src/render/graphicnodes/DefaultSubSkillGraphicNode.ts b/src/render/graphicnodes/DefaultSubSkillGraphicNode.ts index d677f182a1f303c44ce6172e5453c799167b8360..98b63a43ecd292714cded99fdf0551165c372df7 100644 --- a/src/render/graphicnodes/DefaultSubSkillGraphicNode.ts +++ b/src/render/graphicnodes/DefaultSubSkillGraphicNode.ts @@ -16,6 +16,7 @@ import { TextJustification } from '../shapes/TextJustification' export default class DefaultSubSkillGraphicNode extends GraphicNode { private tempHeight = this.height private tempWidth = this.width + public slottable = true /** * Constructor for a default sub skill graphic node. @@ -82,7 +83,9 @@ export default class DefaultSubSkillGraphicNode extends GraphicNode { new Color(Renderer.getCSSVar('--primary-color')), 7, new Color(Renderer.getCSSVar('--node-outline-color')), - 10 + this.borderRadius, + false, + false ).assemble() background.name = 'background' diff --git a/src/render/graphicnodes/ParameterGraphicNode.ts b/src/render/graphicnodes/ParameterGraphicNode.ts index c0080d962d9a98cfb11816cd917b7aff867b6090..55a17ed853203dd82112318ec1473f26e7a2f3f1 100644 --- a/src/render/graphicnodes/ParameterGraphicNode.ts +++ b/src/render/graphicnodes/ParameterGraphicNode.ts @@ -91,7 +91,9 @@ export default class ParameterGraphicNode extends GraphicNode { this.color, 7, new Color(Renderer.getCSSVar('--node-outline-color')), - 10 + this.borderRadius, + false, + false ).assemble() background.name = 'background' @@ -122,8 +124,8 @@ export default class ParameterGraphicNode extends GraphicNode { const size = new Vector3() bounds.getSize(size) - const left = -size.x / 2 - 1.5 - const right = size.x / 2 + 1.5 + const left = -size.x / 2 + 3.5 + const right = size.x / 2 - 3.5 if (this.parameter.isInput) { result.inputs.push(new Vector3(right, 0, 10)) diff --git a/src/render/graphicnodes/control/AndMergerGraphicNode.ts b/src/render/graphicnodes/control/AndMergerGraphicNode.ts index 4f6479734dd908b9c2249cb97a798ff60b64185e..d6278c716ef681f95e4a6c5270a6a82a023c04a3 100644 --- a/src/render/graphicnodes/control/AndMergerGraphicNode.ts +++ b/src/render/graphicnodes/control/AndMergerGraphicNode.ts @@ -1,6 +1,5 @@ import { type ControlNode, - ControlType, GraphicObject, type Parameter, type Port @@ -28,7 +27,7 @@ export default class AndMergerGraphicNode extends ControlGraphicNode { * @param node - The node this graphic node represents. */ public constructor(node: ControlNode) { - super(node, ControlType.AND_MERGER) + super(node) this.color = new Color(Renderer.getCSSVar('--node-control-andmerger-color')) this.inputs = node.parameters.filter((p) => p.isInput === true) @@ -90,7 +89,7 @@ export default class AndMergerGraphicNode extends ControlGraphicNode { reduceButton.assemble() this.add(reduceButton) - this.outline = background.outline + this.outline = background.outline as GraphicObject this.text = text this.expandButton = expandButton this.reduceButton = reduceButton diff --git a/src/render/graphicnodes/control/ControlGraphicNode.ts b/src/render/graphicnodes/control/ControlGraphicNode.ts index 7c80386163b21e3471747adfc3cda62d54e7c22b..af7748b39760f333ecf4641665d3f587332a42fe 100644 --- a/src/render/graphicnodes/control/ControlGraphicNode.ts +++ b/src/render/graphicnodes/control/ControlGraphicNode.ts @@ -1,7 +1,7 @@ import type GraphicEdge from '@/render/GraphicEdge' import type GraphicPort from '@/render/GraphicPort' import type InputEventContext from '@/render/input/InputEventContext' -import { type ControlNode, type ControlType, GraphicObject } from '@/datatypes' +import { type ControlNode, GraphicObject } from '@/datatypes' import Rectangle from '@/render/shapes/Rectangle' import { Color, type Material, Vector3 } from 'three' import GraphicNode from '../../GraphicNode' @@ -10,7 +10,6 @@ import GraphicNode from '../../GraphicNode' * */ export default abstract class ControlGraphicNode extends GraphicNode { - public controlType: ControlType public width = 200 public isButtonHovered = false protected portGap = 40 @@ -23,11 +22,9 @@ export default abstract class ControlGraphicNode extends GraphicNode { /** * Constructor for a control graphic node. * @param node - The node this graphic node represents. - * @param controlType - The control type of this control graphic node. */ - public constructor(node: ControlNode, controlType: ControlType) { + public constructor(node: ControlNode) { super(node) - this.controlType = controlType } /** @@ -75,7 +72,7 @@ export default abstract class ControlGraphicNode extends GraphicNode { ) newBackground.assemble() - this.outline = newBackground.outline + this.outline = newBackground.outline as GraphicObject newBackground.name = 'background' // some fuckery to make the inside of the rectangle transparent ;(newBackground.inner!.material as Material).opacity = 0.0 diff --git a/src/render/graphicnodes/control/SplitterGraphicNode.ts b/src/render/graphicnodes/control/SplitterGraphicNode.ts index 33eff08a692155d133027d87164f16a12a944e90..1e0449f5452a65b5ac99cd4889940bb6d6e99285 100644 --- a/src/render/graphicnodes/control/SplitterGraphicNode.ts +++ b/src/render/graphicnodes/control/SplitterGraphicNode.ts @@ -1,6 +1,5 @@ import { type ControlNode, - ControlType, GraphicObject, type Parameter, type Port @@ -28,7 +27,7 @@ export default class SplitterGraphicNode extends ControlGraphicNode { * @param node - The node that this graphic node represents. */ public constructor(node: ControlNode) { - super(node, ControlType.SPLITTER) + super(node) this.color = new Color(Renderer.getCSSVar('--node-control-splitter-color')) this.inputs = node.parameters.filter((p) => p.isInput === true) @@ -90,7 +89,7 @@ export default class SplitterGraphicNode extends ControlGraphicNode { this.add(reduceButton) - this.outline = background.outline + this.outline = background.outline as GraphicObject this.text = text this.expandButton = expandButton this.reduceButton = reduceButton diff --git a/src/render/graphicnodes/control/slotted/ControlSlottedGraphicNode.ts b/src/render/graphicnodes/control/slotted/ControlSlottedGraphicNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..31adf10bc7c13dae5e3a5bf9fb8db0a6c3070ea8 --- /dev/null +++ b/src/render/graphicnodes/control/slotted/ControlSlottedGraphicNode.ts @@ -0,0 +1,33 @@ +import type { ControlSlottedNode, GraphicObject } from '@/datatypes' +import type Rectangle from '@/render/shapes/Rectangle' +import GraphicNode from '@/render/GraphicNode' +import { Vector3 } from 'three' + +export default abstract class ControlSlottedGraphicNode extends GraphicNode { + public width = 354 // for highlight rects + public titleHeight = 40 + public baseHeight = 80 + public baseWidth = 354 + public tempSlotHeight = 230 + public slotHeight = 230 + public slotPosition: Vector3 = new Vector3(0, 0, 0) + public slottedGraphicNode: GraphicNode | undefined + public ghostNode: Rectangle | undefined + public slotBackground: GraphicObject | undefined + + public constructor(node: ControlSlottedNode) { + super(node) + } + + public abstract assemble(): GraphicObject + public abstract slotGraphicNode(graphicNode: GraphicNode): void + public abstract slotGhostNode(graphicNode: GraphicNode): void + public abstract unslotGraphicNode(): void + public abstract unslotGhostNode(): void + public abstract resizeToFitSlotted(): void + public abstract getGraphicPortsPositions(): { + inputs: Vector3[] + outputs: Vector3[] + } + public abstract onMouseDbClick(): void +} diff --git a/src/render/graphicnodes/control/slotted/LoopRepeatGraphicNode.ts b/src/render/graphicnodes/control/slotted/LoopRepeatGraphicNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8911895c448a39f31196c8c298802f8f03caf5b --- /dev/null +++ b/src/render/graphicnodes/control/slotted/LoopRepeatGraphicNode.ts @@ -0,0 +1,350 @@ +import type GraphicNode from '@/render/GraphicNode' +import { type ControlSlottedNode, GraphicObject, type Port } from '@/datatypes' +import NotificationManager from '@/notification/NotificationManager' +import GraphicEdge from '@/render/GraphicEdge' +import Renderer from '@/render/Renderer' +import Rectangle from '@/render/shapes/Rectangle' +import TextGraphicObject from '@/render/shapes/TextGraphicObject' +import { TextJustification } from '@/render/shapes/TextJustification' +import { Box3, Color, Vector2, Vector3 } from 'three' +import ControlSlottedGraphicNode from './ControlSlottedGraphicNode' + +export default class LoopRepeatGraphicNode extends ControlSlottedGraphicNode { + public borderRadius: number[] = [10, 10, 0, 0] + + // all components that need to be redrawn on slot/unslot + private title: GraphicObject | undefined + private titleBackground: GraphicObject | undefined + private titleSeparator: GraphicObject | undefined + private ghostEdge: GraphicEdge | undefined + + public constructor(node: ControlSlottedNode) { + super(node) + this.inputs = node.parameters.filter((p) => p.isInput === true) + this.outputs = node.parameters.filter((p) => p.isInput === false) + this.color = new Color(Renderer.getCSSVar('--node-control-loop-repeat-color')) + } + + public assemble(existingPorts?: Port[]): GraphicObject { + const title = new TextGraphicObject( + new Vector3(), + TextJustification.CENTER, + 20, + this.node.name, + new Color(Renderer.getCSSVar('--node-text-color')) + ) + + title.onSyncAlways = () => { + this.statechartService?.getRenderer()?.requestRender() + } + + title.assemble() + this.title = title + + this.width = this.baseWidth + if (this.slottedGraphicNode !== undefined) { + this.width = this.slottedGraphicNode.width + 14 + } else if (this.ghostNode !== undefined) { + this.width = this.ghostNode.width + 14 + } + + this.height = Math.max(this.inputs.length, this.outputs.length) * this.subSkillPortGap + this.baseHeight // used for highlight rects + this.titleHeight = this.height + this.tempSlotHeight = Math.max(this.titleHeight, this.slottedGraphicNode?.height ?? 0, this.ghostNode?.height ?? 0) + + const titleBackground = new Rectangle( + this.width, + this.titleHeight, + new Color(Renderer.getCSSVar('--node-control-loop-repeat-color')), + 7, + new Color(Renderer.getCSSVar('--node-outline-color')), + [10, 10, 0, 0], + false, + false + ).assemble() + + this.titleBackground = titleBackground + titleBackground.name = 'background' + + const titleSeparator = new Rectangle( + this.width - 42, + 4, + new Color(Renderer.getCSSVar('--node-control-loop-repeat-color-dark')), + 0, + new Color(), + 2, + true + ).assemble() + + this.titleSeparator = titleSeparator + + const slotBackground = new Rectangle( + this.width, + this.tempSlotHeight + 14, + new Color(Renderer.getCSSVar('--node-control-loop-repeat-color')), + 7, + new Color(Renderer.getCSSVar('--node-control-loop-repeat-color-dark')), + [0, 0, 17, 17], + false, + false + ).assemble() + + this.slotBackground = slotBackground + + slotBackground.children[0].name = 'slot' + slotBackground.name = 'slotBackground' + this.add(title, titleBackground, slotBackground) + + slotBackground.materials.forEach((m) => m.opacity = 0.5) + + title.position.set(0, this.titleHeight / 2 - this.subSkillTitleTopDist, 3) + + titleSeparator.position.set(0, this.titleHeight / 2 - this.subSkillTitleSeparatorTopDist, 0.1) + this.add(title, titleSeparator) + + const bounds = new Box3().setFromObject(titleBackground) + const size = new Vector3() + bounds.getSize(size) + + const bottom = size.y / 2 + slotBackground.position.set(0, -bottom - this.tempSlotHeight / 2 - 14, 3) + + this.generateGraphicPorts(existingPorts) + + for (let i = 0; i < this.graphicPorts.length; i++) { + const port = this.graphicPorts[i] + port.updateEdges() + } + + return this + } + + public slotGraphicNode(graphicNode: GraphicNode): void { + // assign slots + this.slottedGraphicNode = graphicNode + + this.resizeToFitSlotted() + this.unslotGhostNode() + + const node = this.node as ControlSlottedNode + node.slot = graphicNode.node.id + graphicNode.slot = this + + const slot = this.slotBackground + + if (slot === undefined) { + return + } + + slot.add(graphicNode) + + graphicNode.moveTo(new Vector2(0, 0), 5 - 1.5, true) // slotbackground itself is offset by 3 in z + this.slottedGraphicNode = graphicNode + } + + public slotGhostNode(graphicNode: GraphicNode): void { + let slot = this.slotBackground + const renderer = this.statechartService?.getRenderer() + if (slot === undefined || renderer === undefined) { + return + } + + const current = renderer.current + + if (current === undefined) { + return + } + + this.ghostNode = new Rectangle( + graphicNode.width, + graphicNode.height, + graphicNode.color, + 7, + new Color(Renderer.getCSSVar('--node-outline-color')), + [0, 0, 10, 10], + true, + false + ) + + this.resizeToFitSlotted() + + slot = this.slotBackground + + if (slot === undefined) { + return + } + + this.ghostNode.assemble() + + const title = new TextGraphicObject( + new Vector3(), + TextJustification.CENTER, + 20, + graphicNode.node.name, + new Color(Renderer.getCSSVar('--node-text-color')) + ).assemble() + + const titleSeparator = new Rectangle( + graphicNode.width - 42, + 4, + new Color(Renderer.getCSSVar('--primary-color-dark')), + 0, + new Color(), + 2, + true + ).assemble() + + this.ghostNode.add(title, titleSeparator) + title.materials.forEach((m) => { + m.transparent = true + m.opacity = 0.5 + }) + title.position.set(0, graphicNode.height / 2 - graphicNode.subSkillTitleTopDist, 3) + titleSeparator.position.set(0, graphicNode.height / 2 - graphicNode.subSkillTitleSeparatorTopDist, 0.1) + titleSeparator.materials.forEach((m) => { + m.transparent = true + m.opacity = 0.5 + }) + + this.ghostNode.materials.forEach((m) => { + m.transparent = true + m.opacity = 0.5 + }) + slot.add(this.ghostNode) + this.ghostNode.position.set(0, 0, slot.position.z) + + // if there is a connection to the start port of the slotted node, create a ghost edge going from the original port to the start port of the loop + const startPort = graphicNode.graphicPorts.find((p) => p.port.parameter.name.toLowerCase() === 'start') // Scuffed? + if (startPort === undefined) { + console.error('Could not find start port') + return + } + + if (startPort.port.connectedGraphicEdges.length === 0) { + return + } + + const endPort = this.graphicPorts.find((p) => p.port.parameter.name.toLowerCase() === 'start') + + if (endPort === undefined) { + console.error('Could not find end port') + return + } + + const connectedGRaphicEdge = startPort.port.connectedGraphicEdges[0] + + const startPortPos = connectedGRaphicEdge.fromPort?.getSnapPosition() + const endPortPos = endPort.getSnapPosition() + + if (startPortPos === undefined || endPortPos === undefined) { + return + } + + const startColor = startPort.port.graphicNode.color.clone() + const endColor = endPort.port.graphicNode.color.clone() + + this.ghostEdge = new GraphicEdge(startPortPos, endPortPos, startColor, endColor) + + const scene = current[1] + scene.add(this.ghostEdge.assemble()) + + this.ghostEdge.line!.material.transparent = true + this.ghostEdge.line!.material.opacity = 0.1 + + renderer.requestRender() + } + + public unslotGraphicNode(): void { + if (this.slottedGraphicNode === undefined || this.slotBackground === undefined) { + return + } + + this.width = this.baseWidth + this.tempSlotHeight = this.baseHeight + + const position = this.localToWorld(this.slotBackground.position.clone()) + this.slottedGraphicNode.removeFromParent() + const ctrlSlottedNode = this.node as ControlSlottedNode + ctrlSlottedNode.slot = undefined + + const renderer = this.statechartService?.getRenderer() + if (renderer !== undefined && renderer.current !== undefined) { + renderer.current[1].add(this.slottedGraphicNode) + this.slottedGraphicNode.moveTo(new Vector2(position.x, position.y), renderer.getNextNodeZ()) + } + + this.slottedGraphicNode = undefined + this.resizeToFitSlotted() + this.statechartService?.getRenderer()?.requestRender() + } + + public unslotGhostNode(): void { + if (this.ghostNode === undefined) { + return + } + GraphicObject.free(this.ghostNode) + this.ghostNode = undefined + if (this.ghostEdge !== undefined) { + this.ghostEdge.removeFromParent() + this.ghostEdge.remove() + GraphicObject.free(this.ghostEdge) + this.ghostEdge = undefined + } + this.resizeToFitSlotted() + } + + public resizeToFitSlotted() { + if (this.titleBackground === undefined || this.titleSeparator === undefined || this.slotBackground === undefined || this.title === undefined) { + return + } + + GraphicObject.free(this.title) + GraphicObject.free(this.titleBackground) + GraphicObject.free(this.titleSeparator) + GraphicObject.free(this.slotBackground) + + this.deleteInstancedCircles() + + for (let i = this.graphicPorts.length - 1; i >= 0; i--) { + const gp = this.graphicPorts[i] + GraphicObject.free(gp) + } + + this.assemble(this.graphicPorts.flatMap((p) => p.port)) + } + + public getGraphicPortsPositions(): { inputs: Vector3[], outputs: Vector3[] } { + const result: { inputs: Vector3[], outputs: Vector3[] } = { inputs: [], outputs: [] } + + const background = this.getObjectByName('background') + if (background === undefined) { + NotificationManager.error('Could not find bounds') + return result + } + const bounds = new Box3().setFromObject(background) + const size = new Vector3() + bounds.getSize(size) + + const left = -size.x / 2 + 3.5 + const right = size.x / 2 - 3.5 + const top = size.y / 2 + + for (let i = 0; i < this.inputs.length; i++) { + const pos = new Vector3(left, top - this.subSkillPortTopDist - this.subSkillPortGap * i, 10) + + result.inputs.push(pos) + } + + for (let i = 0; i < this.outputs.length; i++) { + const pos = new Vector3(right, top - this.subSkillPortTopDist - this.subSkillPortGap * i, 10) + + result.outputs.push(pos) + } + + return result + } + + public onMouseDbClick(): void { + throw new Error('Method not implemented.') + } +} diff --git a/src/render/shapes/Circle.ts b/src/render/shapes/Circle.ts index 62c494680fe1c48f47a8521cb5a046ea6c2620d9..ea9026c871b24db02f614c1098a34361fac556bb 100644 --- a/src/render/shapes/Circle.ts +++ b/src/render/shapes/Circle.ts @@ -62,7 +62,7 @@ export default class Circle extends GraphicObject { * @returns This graphic object. */ public assemble(): GraphicObject { - const geometry = new CircleGeometry(this.radius, 16) + const geometry = new CircleGeometry(this.radius, 32) const material = new MeshBasicMaterial({ color: this.color }) const circle = new Mesh(geometry, material) diff --git a/src/render/shapes/Rectangle.ts b/src/render/shapes/Rectangle.ts index 0ecfebeea92d678c3c1e16ed17c4e68453681941..cd27db3b41f20c945dcb46831bb5fddbdd1990c8 100644 --- a/src/render/shapes/Rectangle.ts +++ b/src/render/shapes/Rectangle.ts @@ -1,5 +1,6 @@ import { Color, Mesh, MeshBasicMaterial, Shape, ShapeGeometry } from 'three' import GraphicObject from '../GraphicObject' +import Outline from './Outline' /** * A simple rectangle. @@ -14,7 +15,7 @@ export default class Rectangle extends GraphicObject { public readonly isComposed = false public inner: Mesh | undefined - public outline: Rectangle | undefined + public outline: Mesh | GraphicObject | undefined /** * Constructor for a rectangle. The object is not immediately assembled and requires @@ -24,7 +25,7 @@ export default class Rectangle extends GraphicObject { * @param color - The color of the rectangle. * @param outlineWidth - The width of the outline. * @param outlineColor - The color of the outline. - * @param radius - The radius of the corners. + * @param radius - The radius of the corners. It can be a single number or an array of 4 numbers. If it is an array, the order is top-left, top-right, bottom-right, bottom-left. */ public constructor( public readonly width: number, @@ -32,8 +33,9 @@ export default class Rectangle extends GraphicObject { public readonly color: Color, public readonly outlineWidth = 0, public readonly outlineColor = new Color('white'), - public readonly radius = 0, - public readonly ignoreRaycast = false + public readonly radius: number | number[] = 0, + public readonly ignoreRaycast = false, + public readonly legacyOutline = true ) { super() this.name = 'rectangle' @@ -73,63 +75,111 @@ export default class Rectangle extends GraphicObject { * @returns This graphic object. */ public assemble(): GraphicObject { + const rectangle = this.generateRectangle(this.width, this.height, this.getRadius(), this.color) + + this.inner = rectangle[0] + this.meshes.push(rectangle[0]) + + if (Array.isArray(rectangle[0].material)) { + this.materials.push(...rectangle[0].material) + } else { + this.materials.push(rectangle[0].material) + } + + if (this.outlineWidth > 0) { + if (this.legacyOutline === false) { + const radius = this.getRadius() + const outline = this.generateRectangle(this.width + 2 * this.outlineWidth, this.height + 2 * this.outlineWidth, radius.map((r) => r > 0 ? r + this.outlineWidth : 0), this.outlineColor) + + this.outline = outline[0] + this.meshes.push(outline[0]) + + if (Array.isArray(outline[0].material)) { + this.materials.push(...outline[0].material) + } else { + this.materials.push(outline[0].material) + } + + this.add(outline[0]) + outline[0].translateZ(-1) + } else { + const outline = new Outline( + rectangle[1].getPoints(), + this.position.z - 1.1, + this.outlineWidth, + this.outlineColor + ) + outline.assemble() + this.add(outline) + + this.outline = outline + } + } + + this.add(rectangle[0]) + + return this + } + + private generateRectangle(width: number, height: number, radius: number[], color: Color): [Mesh, Shape] { const rectShape = new Shape() const x = this.position.x const y = this.position.y - const width = this.width - const height = this.height - const radius = this.radius // Adjust x and y to be the center of the rectangle const centerX = x - width / 2 const centerY = y - height / 2 - rectShape.moveTo(centerX, centerY + radius) - rectShape.lineTo(centerX, centerY + height - radius) - rectShape.quadraticCurveTo(centerX, centerY + height, centerX + radius, centerY + height) - rectShape.lineTo(centerX + width - radius, centerY + height) - rectShape.quadraticCurveTo( - centerX + width, - centerY + height, - centerX + width, - centerY + height - radius - ) - rectShape.lineTo(centerX + width, centerY + radius) - rectShape.quadraticCurveTo(centerX + width, centerY, centerX + width - radius, centerY) - rectShape.lineTo(centerX + radius, centerY) - rectShape.quadraticCurveTo(centerX, centerY, centerX, centerY + radius) + + rectShape.moveTo(centerX, centerY + radius[3]) // Start at bottom left corner + + // Top-left corner + rectShape.lineTo(centerX, centerY + height - radius[0]) + if (radius[0] > 0) { + rectShape.quadraticCurveTo(centerX, centerY + height, centerX + radius[0], centerY + height) + } + + // Top edge + if (radius[1] > 0) { + rectShape.lineTo(centerX + width - radius[1], centerY + height) + rectShape.quadraticCurveTo(centerX + width, centerY + height, centerX + width, centerY + height - radius[1]) + } else { + rectShape.lineTo(centerX + width, centerY + height) + } + + // Right edge + rectShape.lineTo(centerX + width, centerY + radius[2]) + if (radius[2] > 0) { + rectShape.quadraticCurveTo(centerX + width, centerY, centerX + width - radius[2], centerY) + } + + // Bottom edge + rectShape.lineTo(centerX + radius[3], centerY) + if (radius[3] > 0) { + rectShape.quadraticCurveTo(centerX, centerY, centerX, centerY + radius[3]) + } const geometry = new ShapeGeometry(rectShape) geometry.computeBoundingBox() geometry.center() - const material = new MeshBasicMaterial({ color: this.color, transparent: true }) + const material = new MeshBasicMaterial({ color, transparent: true }) const mesh = new Mesh(geometry, material) if (this.ignoreRaycast === true) { mesh.layers.set(1) } - if (this.outlineWidth > 0) { - const outline = new Rectangle( - this.width + this.outlineWidth * 2, - this.height + this.outlineWidth * 2, - this.outlineColor, - 0, - this.outlineColor, - this.radius + this.outlineWidth - ) - outline.assemble() - this.add(outline) - outline.position.set(0, 0, -1) - - this.outline = outline - } - - this.meshes.push(mesh) - this.materials.push(material) - this.add(mesh) - - this.inner = mesh + return [mesh, rectShape] + } - return this + private getRadius(): number[] { + if (Array.isArray(this.radius)) { + if (this.radius.length === 4) { + return this.radius + } else { + return [this.radius[0], this.radius[0], this.radius[0], this.radius[0]] + } + } else { + return [this.radius, this.radius, this.radius, this.radius] + } } } diff --git a/src/render/strategies/ControlRenderStrategy.ts b/src/render/strategies/ControlRenderStrategy.ts index 75754b728ca6ec23c84fcc5e2c750f80f4137526..b011fbe28ce8adebb9e41ac89b3e199f4e0ff24f 100644 --- a/src/render/strategies/ControlRenderStrategy.ts +++ b/src/render/strategies/ControlRenderStrategy.ts @@ -2,6 +2,7 @@ import type GraphicNode from '../GraphicNode' import type IRenderStrategy from './IRenderStrategy' import { type ControlNode, + type ControlSlottedNode, ControlType, type Node, NodeType, @@ -15,9 +16,9 @@ import NotificationManager from '@/notification/NotificationManager' import { Vector3 } from 'three' import GraphicEdge from '../GraphicEdge' import AndMergerGraphicNode from '../graphicnodes/control/AndMergerGraphicNode' +import LoopRepeatGraphicNode from '../graphicnodes/control/slotted/LoopRepeatGraphicNode' import SplitterGraphicNode from '../graphicnodes/control/SplitterGraphicNode' import ControlFlowSubSkillGraphicNode from '../graphicnodes/ControlFlowSubSkillGraphicNode' -import DefaultSubSkillGraphicNode from '../graphicnodes/DefaultSubSkillGraphicNode' import ParameterGraphicNode from '../graphicnodes/ParameterGraphicNode' /** @@ -62,6 +63,7 @@ export class ControlRenderStrategy implements IRenderStrategy { * Generate a graphic node for the control render strategy. * @param node - The node that the generated graphic node represents. * @returns The generated graphic node. + * @throws Error if the node type is not supported. */ public generateGraphicNode(node: Node): GraphicNode { switch (node.nodeType) { @@ -78,12 +80,14 @@ export class ControlRenderStrategy implements IRenderStrategy { return new SplitterGraphicNode(control) case ControlType.AND_MERGER: return new AndMergerGraphicNode(control) + case ControlType.LOOP_REPEAT: + return new LoopRepeatGraphicNode(control as ControlSlottedNode) default: - return new SplitterGraphicNode(control) + throw new Error('Unsupported control type.') } } default: { - return new DefaultSubSkillGraphicNode(node as SubSkill) + throw new Error('Unsupported node type.') } } } diff --git a/src/render/strategies/DataRenderStrategy.ts b/src/render/strategies/DataRenderStrategy.ts index 8dcd3282974a724d7ae4087ab247e05387c4475b..d774439e1382e648c16abda3736b3f410801410d 100644 --- a/src/render/strategies/DataRenderStrategy.ts +++ b/src/render/strategies/DataRenderStrategy.ts @@ -2,6 +2,7 @@ import type GraphicNode from '../GraphicNode' import type IRenderStrategy from './IRenderStrategy' import { type ControlNode, + type ControlSlottedNode, ControlType, type Node, NodeType, @@ -15,6 +16,7 @@ import NotificationManager from '@/notification/NotificationManager' import { Vector3 } from 'three' import GraphicEdge from '../GraphicEdge' import AndMergerGraphicNode from '../graphicnodes/control/AndMergerGraphicNode' +import LoopRepeatGraphicNode from '../graphicnodes/control/slotted/LoopRepeatGraphicNode' import SplitterGraphicNode from '../graphicnodes/control/SplitterGraphicNode' import DataFlowSubSkillNode from '../graphicnodes/DataFlowSubSkillNode' import ParameterGraphicNode from '../graphicnodes/ParameterGraphicNode' @@ -62,6 +64,7 @@ export class DataRenderStrategy implements IRenderStrategy { * Generate a graphic node for the control render strategy. * @param node - The node that the generated graphic node represents. * @returns The generated graphic node. + * @throws Error if the node type is not supported. */ public generateGraphicNode(node: Node): GraphicNode { switch (node.nodeType) { @@ -78,12 +81,14 @@ export class DataRenderStrategy implements IRenderStrategy { return new SplitterGraphicNode(control) case ControlType.AND_MERGER: return new AndMergerGraphicNode(control) + case ControlType.LOOP_REPEAT: + return new LoopRepeatGraphicNode(control as ControlSlottedNode) default: - return new SplitterGraphicNode(control) + throw new Error('Unsupported control type.') } } default: { - return new DataFlowSubSkillNode(node as SubSkill) + throw new Error('Unsupported node type.') } } } diff --git a/src/render/strategies/DefaultRenderStrategy.ts b/src/render/strategies/DefaultRenderStrategy.ts index d2320e5d044d8da8f0d4c423a6292864e4637f06..9cf24a301262a101e48dc66f96c3db120a5310f4 100644 --- a/src/render/strategies/DefaultRenderStrategy.ts +++ b/src/render/strategies/DefaultRenderStrategy.ts @@ -2,6 +2,7 @@ import type GraphicNode from '../GraphicNode' import type IRenderStrategy from './IRenderStrategy' import { type ControlNode, + type ControlSlottedNode, ControlType, type Node, NodeType, @@ -15,6 +16,7 @@ import NotificationManager from '@/notification/NotificationManager' import { Vector3 } from 'three' import GraphicEdge from '../GraphicEdge' import AndMergerGraphicNode from '../graphicnodes/control/AndMergerGraphicNode' +import LoopRepeatGraphicNode from '../graphicnodes/control/slotted/LoopRepeatGraphicNode' import SplitterGraphicNode from '../graphicnodes/control/SplitterGraphicNode' import DefaultSubSkillGraphicNode from '../graphicnodes/DefaultSubSkillGraphicNode' import ParameterGraphicNode from '../graphicnodes/ParameterGraphicNode' @@ -55,6 +57,7 @@ export class DefaultRenderStrategy implements IRenderStrategy { * Generate a graphic node for the default render strategy. * @param node - The node that the generated graphic node represents. * @returns The generated graphic node. + * @throws Error if the node type is not supported. */ public generateGraphicNode(node: Node): GraphicNode { switch (node.nodeType) { @@ -71,12 +74,14 @@ export class DefaultRenderStrategy implements IRenderStrategy { return new SplitterGraphicNode(control) case ControlType.AND_MERGER: return new AndMergerGraphicNode(control) + case ControlType.LOOP_REPEAT: + return new LoopRepeatGraphicNode(control as ControlSlottedNode) default: - return new SplitterGraphicNode(control) + throw new Error('Unsupported control type.') } } default: { - return new DefaultSubSkillGraphicNode(node as SubSkill) + throw new Error('Unsupported node type.') } } } diff --git a/src/render/tools/DefaultTool.ts b/src/render/tools/DefaultTool.ts index 6691a4ddb7f17a5e7ae8b2adaee54066e5c575c6..c7b822ba3a6246f9a6a7500d9069045b2a27650a 100644 --- a/src/render/tools/DefaultTool.ts +++ b/src/render/tools/DefaultTool.ts @@ -70,25 +70,13 @@ export default class DefaultTool extends Tool { const highest = GraphicObject.getHighestParent(ctx.object) - // sort all nodes by z - const nodes = this.renderer.current[0].statechart.graphicNodes.sort((a, b) => { - return a.position.z - b.position.z - }) - if (highest.isGraphicNode === true) { - // find index of highest - const index = nodes.findIndex((node) => node.node.id.id === (highest as GraphicNode).node.id.id) - const greatestZ = nodes[nodes.length - 1].position.z - nodes[index].position.z = greatestZ - nodes[index].updateGizmos() - - // reduce z of all nodes after index by 20 - for (let i = index + 1; i < nodes.length; i++) { - nodes[i].position.z -= 20 - nodes[i].updateGizmos() - } + const graphicNode = highest as GraphicNode + graphicNode.bringForward() } + const nodes = this.renderer.current[0].statechart.graphicNodes + if (ctx.object.isGraphicNode === true) { const node = ctx.object as GraphicNode this.selectedNodes = [] @@ -102,7 +90,8 @@ export default class DefaultTool extends Tool { node.setSelected(true, true) } - for (const node of nodes) { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i] if (node.isSelected() === true) { this.selectedNodes.push(node) } diff --git a/src/services/StatechartService.ts b/src/services/StatechartService.ts index b009f6a18e9aac141d3d3febb25d47c982ace5fc..5e946e80013ff8d4a708d02fa2836d3fe234f786 100644 --- a/src/services/StatechartService.ts +++ b/src/services/StatechartService.ts @@ -46,6 +46,7 @@ import TypeService from './TypeService' export default class StatechartService { private draggedNode: Node | undefined + // Node, icon, color unhovered, color hovered private controlNodes: [ControlNode, string, string, string][] = [] /** @@ -100,7 +101,8 @@ export default class StatechartService { isInput: false } ], - description: 'Splits an incoming Event connection into two.' + description: 'Splits an incoming Event connection into two.', + isSlotted: false }, 'split', '--node-control-splitter-color', @@ -142,11 +144,82 @@ export default class StatechartService { isInput: false } ], - description: 'Merges two Event type connections into one.' + description: 'Merges two Event type connections into one.', + isSlotted: false }, 'merge', '--node-control-andmerger-color', '--node-control-andmerger-color-dark' + ], + [ + { + nodeType: NodeType.CONTROL, + id: { id: 'LOOP_REPEAT' }, + name: 'Loop Repeat', + position: { x: 0, y: 0 }, + controlType: ControlType.LOOP_REPEAT, + parameters: [ + { + id: { id: '0' }, + name: 'Start', + description: 'Start', + type: this.typeService.getEventType(), + required: true, + values: [], + isInput: true + }, + { + id: { id: '1' }, + name: 'Iterations', + description: 'Number of iterations', + type: this.typeService.getIntegerType(), + required: true, + values: [], + isInput: true + }, + { + id: { id: '2' }, + name: 'Break', + description: 'Break', + type: this.typeService.getEventType(), + required: false, + values: [], + isInput: true + }, + { + id: { id: '3' }, + name: 'Success', + description: 'Success', + type: this.typeService.getEventType(), + required: false, + values: [], + isInput: false + }, + { + id: { id: '4' }, + name: 'Failure', + description: 'Failure', + type: this.typeService.getEventType(), + required: false, + values: [], + isInput: false + }, + { + id: { id: '5' }, + name: 'Abort', + description: 'Abort', + type: this.typeService.getEventType(), + required: false, + values: [], + isInput: false + } + ], + description: 'Repeats the contained subskill a given number of times.', + isSlotted: true + }, + 'loop', + '--node-control-loop-repeat-color', + '--node-control-loop-repeat-color-dark' ] ] } diff --git a/src/services/TypeService.ts b/src/services/TypeService.ts index 5bb39815d4318e4ae938b21d45d92f51f17e787c..95384467206a6e3b0d77c003f27d6ae130a42a6d 100644 --- a/src/services/TypeService.ts +++ b/src/services/TypeService.ts @@ -518,6 +518,16 @@ export default class TypeService { return stringType } + public getIntegerType(): BaseType { + const integerType = this.dataCollector + .getTypes() + .find((type) => type.name.toUpperCase() === 'INTEGER') + if (!integerType) { + throw new Error('Integer type not found') + } + return integerType + } + /** * Getter for the description of a type. * @param skillId - The skill id.