Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas #267

Merged
merged 28 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0ec1768
feat(io-center): io-center init based on template - rebased on main
michelguerin Jan 15, 2025
08975ee
doc: design document
trusz Jan 14, 2025
4176b1a
feat: set up files
trusz Jan 15, 2025
06cde62
feat: add dummy component
trusz Feb 4, 2025
4ee3343
Merge remote-tracking branch 'origin/main' into feat/io-center-archit…
trusz Feb 4, 2025
c1e9b30
added canvas to io-center
Xenon27 Feb 4, 2025
b30e377
outsourced types and removed logs
Xenon27 Feb 4, 2025
9c1bbfe
outsourced types and removed logs
Xenon27 Feb 4, 2025
00f2aa2
Merge branch 'feat/io-center-canvas' of https://github.com/sprinteins…
Xenon27 Feb 4, 2025
392029c
changed names and removed duplicate code
Xenon27 Feb 4, 2025
a3a05a2
Merge branch 'main' into feat/io-center-canvas
Xenon27 Feb 4, 2025
9938837
reactive lists
Xenon27 Feb 5, 2025
1ec92bf
renamed components to use multiwords and added onDestroy
Xenon27 Feb 5, 2025
2620d15
appropriate naming instead of drag interaction
Xenon27 Feb 5, 2025
8dfb374
removed logs
Xenon27 Feb 5, 2025
f217648
moved mouse events to action and renamed circle to point
Xenon27 Feb 5, 2025
48d0dcc
update passing info to node
Xenon27 Feb 5, 2025
ec15faf
add close button placeholder
Xenon27 Feb 5, 2025
cb4951f
add aria role button
Xenon27 Feb 6, 2025
96f882a
Merge branch 'main' into feat/io-center-canvas
Xenon27 Feb 6, 2025
a525594
add luicide icons
Xenon27 Feb 6, 2025
ac4c35e
Merge branch 'main' into feat/io-center-canvas
AbdelazizTina-dev Feb 17, 2025
8485ca0
differntiate columns with background color
AbdelazizTina-dev Feb 17, 2025
92b648b
linting files
AbdelazizTina-dev Feb 17, 2025
44f2348
organize canvas components
AbdelazizTina-dev Feb 18, 2025
cd22816
small style adjustment for node elements
AbdelazizTina-dev Feb 19, 2025
12b657b
add DO elements to Canvas on selection
AbdelazizTina-dev Feb 19, 2025
4c775f6
remove manual event assignment
AbdelazizTina-dev Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { canvasStore } from "@/ui/components/canvas/canvas-store.svelte";
import type { TreeNode } from "@/ui/components/object-tree/types.object-tree";

export function addDoElementToCanvas(node: TreeNode) {
if (canvasStore.dataObjects.some(item => item.id === node.id)) {
canvasStore.dataObjects = canvasStore.dataObjects.filter((item) => item.id !== node.id);
return;
}

canvasStore.dataObjects.push({ id: node.id, name: node.name })
}
172 changes: 172 additions & 0 deletions packages/plugins/io-center/src/headless/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { canvasStore } from "@/ui/components/canvas/canvas-store.svelte";
import type { ConnectionPoint } from "@/ui/components/canvas/types.canvas";
import type { TreeNode } from "@/ui/components/object-tree/types.object-tree";

export function searchTree(tree: TreeNode[], searchTerm: string): TreeNode[] {
Expand All @@ -16,3 +18,173 @@ export function searchTree(tree: TreeNode[], searchTerm: string): TreeNode[] {
})
.filter(node => node !== null);
}

export function startDrawing(event: MouseEvent) {
event.preventDefault();
if (!event.target || !event.currentTarget) {
return;
}
canvasStore.drawStartPoint = event.target;
const currentTarget = event.currentTarget as HTMLElement;
if (!currentTarget.parentElement) {
return;
}
canvasStore.startNode = currentTarget.parentElement.getAttribute("data-title");
}

export function isWrongColumn(node1: string, node2: string) {
if (!canvasStore.container) {
return false;
}

const column1 = canvasStore.container
.querySelector(`[data-title="${node1}"]`)
?.closest(".flex-col");
const column2 = canvasStore.container
.querySelector(`[data-title="${node2}"]`)
?.closest(".flex-col");
if (!column1 || !column2) {
return false;
}
if (column1 === column2) {
return true;
}

const column1Title = column1.getAttribute("data-title");
const column2Title = column2.getAttribute("data-title");
if (!column1Title || !column2Title) {
return false;
}

if (
(column1Title === "DO" && column2Title === "LP") ||
(column1Title === "LP" && column2Title === "DO")
) {
return true;
}
return false;
}

export function isSameSide(startSide: string, targetSide: string) {
return (
(startSide === "left-circle" && targetSide === "left") ||
(startSide === "right-circle" && targetSide === "right")
);
}

export function connectionExists(fromNode: string, toNode: string) {
return canvasStore.connections.some(
(connection) =>
(connection.from.node === fromNode &&
connection.to.node === toNode) ||
(connection.from.node === toNode &&
connection.to.node === fromNode),
);
}

export function stopDrawing(targetNode: string, targetSide: string) {
const startCircle = canvasStore.lastStartPoint;
canvasStore.lastStartPoint = null;

if (!canvasStore.container) {
return;
}

const target = canvasStore.container.querySelector(
`[data-title="${targetNode}"]`,
) as HTMLElement | null;

if (startCircle && target && startCircle !== target) {
let fromNode = canvasStore.startNode;
if (!fromNode) {
return;
}
let toNode = targetNode;

if (startCircle instanceof HTMLElement)
if (
fromNode === toNode ||
isWrongColumn(fromNode, toNode) ||
isSameSide(startCircle.id, targetSide)
) {
return;
}

if (
startCircle instanceof HTMLElement &&
startCircle.id === "left-circle"
) {
[fromNode, toNode] = [toNode, fromNode];
}

if (!connectionExists(fromNode, toNode)) {
canvasStore.connections = [
...canvasStore.connections,
{
from: { node: fromNode, side: "right" },
to: { node: toNode, side: "left" },
},
];
}
}
}

export function getCoordinates(connectionPoint: ConnectionPoint) {
if (!canvasStore.svgElement || !canvasStore.container) {
return { x: 0, y: 0 };
}

const target = canvasStore.container.querySelector(
`[data-title="${connectionPoint.node}"]`,
) as HTMLElement | null;

if (!target) {
return { x: 0, y: 0 };
}

const circle = target.querySelector(
connectionPoint.side === "left" ? "#left-circle" : "#right-circle",
);

if (!circle) {
return { x: 0, y: 0 };
}

const rect = circle.getBoundingClientRect();

const svgPoint = new DOMPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
);

const transformedPoint = svgPoint.matrixTransform(
canvasStore.svgElement.getScreenCTM()?.inverse(),
);
return { x: transformedPoint.x, y: transformedPoint.y };
}

export function getCirclePosition(target: EventTarget | null) {
if (
!target ||
!(target instanceof HTMLElement) ||
!canvasStore.svgElement
) {
return { x: 0, y: 0 };
}

const rect = target.getBoundingClientRect();

const svgPoint = new DOMPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
);

const transformedPoint = svgPoint.matrixTransform(
canvasStore.svgElement.getScreenCTM()?.inverse(),
);
return { x: transformedPoint.x, y: transformedPoint.y };
}

export function redrawConnections() {
canvasStore.connections = [...canvasStore.connections];
}
6 changes: 2 additions & 4 deletions packages/plugins/io-center/src/plugin.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import type { Nullable } from "./types";
import type { IED } from "./ied/ied";
import ObjectTree from "./ui/components/object-tree/object-tree.svelte";
import CanvasArea from "./ui/components/canvas/canvas-area.svelte";

// props
const {
Expand Down Expand Up @@ -89,10 +90,7 @@
<ObjectTree />
</div>
<div slot="content">
Document: {docName}
<p>
<button onclick={addIED}>Add IED</button>
</p>
<CanvasArea />
</div>
<div slot="sidebar-right">sidebar right</div>
</Layout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { canvasStore } from './canvas-store.svelte'

export function calulateCoordinates(node: HTMLElement) {
function convertToSVGCoordinates(clientX: number, clientY: number) {
if (!canvasStore.svgElement) {
return { x: clientX, y: clientY }
}

const svgPoint = new DOMPoint(clientX, clientY)

const transformedPoint = svgPoint.matrixTransform(
canvasStore.svgElement.getScreenCTM()?.inverse()
)
return { x: transformedPoint.x, y: transformedPoint.y }
}

function updateMousePosition(event: MouseEvent) {
event.preventDefault()
canvasStore.mousePosition = convertToSVGCoordinates(
event.clientX,
event.clientY
)
}

function onMouseUp() {
canvasStore.lastStartPoint = canvasStore.drawStartPoint
canvasStore.drawStartPoint = null
}

$effect(() => {
node.addEventListener('mousemove', updateMousePosition)
node.addEventListener('mouseup', onMouseUp)

return () => {
node.removeEventListener('mousemove', updateMousePosition)
node.removeEventListener('mouseup', onMouseUp)
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts">
import NodeElement from "./node-element.svelte";
import { calulateCoordinates } from "./canvas-actions.svelte";
import { canvasStore } from "./canvas-store.svelte";
import {
getCirclePosition,
getCoordinates,
redrawConnections,
startDrawing,
stopDrawing,
} from "@/headless/utils";
</script>

<svelte:window onresize={redrawConnections} />

<div
use:calulateCoordinates
class="grid grid-cols-3 h-screen p-2 gap-2"
bind:this={canvasStore.container}
>
<div
class="flex flex-col items-center w-full gap-2 bg-gray-50"
data-title="DO"
>
<div class="text-center">Data Objects</div>
{#each canvasStore.dataObjects as node}
<NodeElement
{node}
showLeftCircle={false}
showRightCircle={true}
{startDrawing}
{stopDrawing}
/>
{/each}
</div>
<div
class="flex flex-col items-center w-full gap-2 bg-gray-50"
data-title="LC"
>
<div class="text-center">Logical Conditioners</div>
{#each canvasStore.logicalConditioners as node}
<NodeElement
{node}
showLeftCircle={true}
showRightCircle={true}
{startDrawing}
{stopDrawing}
/>
{/each}
</div>
<div
class="flex flex-col items-center w-full gap-2 bg-gray-50"
data-title="LP"
>
<div class="text-center">Logical Physical Inputs/Outputs</div>
{#each canvasStore.logicalPhysicals as node}
<NodeElement
{node}
showLeftCircle={true}
showRightCircle={false}
{startDrawing}
{stopDrawing}
/>
{/each}
</div>
</div>

<svg
class="absolute top-0 left-0 w-full h-full pointer-events-none"
bind:this={canvasStore.svgElement}
>
{#key canvasStore.connections}
{#each canvasStore.connections as connection}
<path
class="stroke-black stroke-2 fill-none"
d={`M ${getCoordinates(connection.from).x},${getCoordinates(connection.from).y}
C ${(getCoordinates(connection.from).x + getCoordinates(connection.to).x) / 2},${getCoordinates(connection.from).y}
${(getCoordinates(connection.from).x + getCoordinates(connection.to).x) / 2},${getCoordinates(connection.to).y}
${getCoordinates(connection.to).x},${getCoordinates(connection.to).y}`}
/>
{/each}
{/key}
{#if canvasStore.drawStartPoint}
<path
class="stroke-black stroke-2"
d={`M ${getCirclePosition(canvasStore.drawStartPoint).x},${getCirclePosition(canvasStore.drawStartPoint).y} L ${canvasStore.mousePosition.x},${canvasStore.mousePosition.y}`}
/>
{/if}
</svg>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Connection, NodeElement } from "./types.canvas"

class UseCanvasStore {
dataObjects = $state<NodeElement[]>([])
logicalConditioners = $state<NodeElement[]>([])
logicalPhysicals = $state<NodeElement[]>([])
connections = $state<Connection[]>([])
container = $state<HTMLDivElement | null>(null)
mousePosition = $state({ x: 0, y: 0 })
startNode = $state<string | null>("");
drawStartPoint = $state<EventTarget | null>(null)
lastStartPoint = $state<EventTarget | null>(null)
svgElement = $state<SVGGraphicsElement | null>(null)
}

export const canvasStore = new UseCanvasStore()
Loading