diff --git a/packages/hfr/_category_.yml b/packages/hfr/_category_.yml new file mode 100644 index 000000000..4e3bd40e5 --- /dev/null +++ b/packages/hfr/_category_.yml @@ -0,0 +1,4 @@ +label: '@hfr/' +link: + type: generated-index + title: '@hfr/ Packages' diff --git a/packages/hfr/circuit/README.mdx b/packages/hfr/circuit/README.mdx new file mode 100644 index 000000000..8a771309a --- /dev/null +++ b/packages/hfr/circuit/README.mdx @@ -0,0 +1,16 @@ +--- +page_id: 980c75ef-0868-422e-9578-5ce228a48a0a +--- + +import DynamicDocumentRoots from '@tdev-components/documents/DynamicDocumentRoots'; +import BrowserWindow from '@tdev-components/BrowserWindow'; + +# Schaltkreise + +```mdx + +``` + + + + \ No newline at end of file diff --git a/packages/hfr/circuit/components/Circuit.tsx b/packages/hfr/circuit/components/Circuit.tsx new file mode 100644 index 000000000..6d7ed532d --- /dev/null +++ b/packages/hfr/circuit/components/Circuit.tsx @@ -0,0 +1,192 @@ +import clsx from 'clsx'; + +import { observer } from 'mobx-react-lite'; +import styles from './styles.module.scss'; +import React from 'react'; +import PermissionsPanel from '@tdev-components/PermissionsPanel'; +import { Background, ReactFlow, MiniMap, Controls, Panel } from '@xyflow/react'; +import type { + OnConnect, + Edge, + OnEdgesChange, + OnNodesChange, + Node, + OnReconnect, + FinalConnectionState, + HandleType, + OnNodesDelete +} from '@xyflow/react'; + +import '@xyflow/react/dist/style.css'; +import { mdiCarBattery, mdiElectricSwitch, mdiLedOn } from '@mdi/js'; +import Button from '@tdev-components/shared/Button'; +import { useStore } from '@tdev-hooks/useStore'; +import { nodeTypes } from './Nodes'; +import { NodeType } from '@hfr/circuit'; +import { DynamicRoomProps } from '@tdev-stores/ComponentStore'; + +type OnReconnectEnd = ( + event: MouseEvent | TouchEvent, + edge: Edge, + handleType: HandleType, + connectionState: FinalConnectionState +) => void; + +type OnReconnectStart = (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void; + +const Circuit = observer((props: DynamicRoomProps<'circuit'>): React.ReactNode => { + const { dynamicRoot } = props; + 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 onNodesDelete = React.useCallback( + (deleted) => { + edgeReconnectSuccessful.current = true; + dynamicRoot.room!.onDelete(deleted); + }, + [dynamicRoot.room] + ); + const onReconnectStart = React.useCallback(() => { + edgeReconnectSuccessful.current = false; + }, []); + + const onReconnect = React.useCallback( + (oldEdge, newConnection) => { + edgeReconnectSuccessful.current = true; + dynamicRoot.room!.reconnectEdge(oldEdge.id, newConnection); + }, + [dynamicRoot.room] + ); + + const onReconnectEnd = React.useCallback((_, edge) => { + if (!edgeReconnectSuccessful.current) { + const doc = documentStore.find(edge.id); + if (doc) { + documentStore.apiDelete(doc); + } + } + + edgeReconnectSuccessful.current = true; + }, []); + if (!dynamicRoot || !dynamicRoot.room) { + return null; + } + + return ( +
+
+

+ {dynamicRoot.name} +

+
+ + + + +
+
+
+ ); +}); +export default Circuit; diff --git a/packages/hfr/circuit/components/Nodes/And/assets/Gate-AND.svg b/packages/hfr/circuit/components/Nodes/And/assets/Gate-AND.svg new file mode 100644 index 000000000..904eb0b57 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/And/assets/Gate-AND.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/hfr/circuit/components/Nodes/And/index.tsx b/packages/hfr/circuit/components/Nodes/And/index.tsx new file mode 100644 index 000000000..76f2b35a7 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/And/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position } from '@xyflow/react'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import NodeWrapper from '../NodeWrapper'; +import AndGate from './assets/Gate-AND.svg'; + +export type AndNode = Node<{}, 'AndNode'>; + +const AndNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const doc = documentStore.find(props.id) as FlowNode | undefined; + if (!doc) { + return null; + } + return ( + + + AND + + + + + ); +}); + +export default AndNode; diff --git a/packages/hfr/circuit/components/Nodes/And/styles.module.scss b/packages/hfr/circuit/components/Nodes/And/styles.module.scss new file mode 100644 index 000000000..7a19fa092 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/And/styles.module.scss @@ -0,0 +1,4 @@ +.and { + width: 80px; + height: 60px; +} diff --git a/packages/hfr/circuit/components/Nodes/Battery/index.tsx b/packages/hfr/circuit/components/Nodes/Battery/index.tsx new file mode 100644 index 000000000..57f44ada4 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Battery/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position, useUpdateNodeInternals } from '@xyflow/react'; +import { mdiCarBattery, mdiMinusCircle, mdiPlusCircle } from '@mdi/js'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import Icon from '@mdi/react'; +import Button from '@tdev-components/shared/Button'; +import NodeWrapper from '../NodeWrapper'; + +export type BatteryNode = Node<{}, 'BatteryNode'>; + +const BatteryNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const updateNodeInternals = useUpdateNodeInternals(); + const doc = documentStore.find(props.id) as FlowNode | undefined; + if (!doc) { + return null; + } + const pins = doc?.deriver.pins ?? 3; + return ( + + + + {Array.from({ length: pins }).map((_, i) => ( + + ))} +
+
+
+ ); +}); + +export default BatteryNode; diff --git a/packages/hfr/circuit/components/Nodes/Battery/styles.module.scss b/packages/hfr/circuit/components/Nodes/Battery/styles.module.scss new file mode 100644 index 000000000..9f3b4ed73 --- /dev/null +++ b/packages/hfr/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/hfr/circuit/components/Nodes/DecimalDisplay/index.tsx b/packages/hfr/circuit/components/Nodes/DecimalDisplay/index.tsx new file mode 100644 index 000000000..d3f044495 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/DecimalDisplay/index.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position, useUpdateNodeInternals } from '@xyflow/react'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import Button from '@tdev-components/shared/Button'; +import { mdiMinusCircle, mdiPlusCircle } from '@mdi/js'; +import NodeWrapper from '../NodeWrapper'; + +export type DecimalDisplayNode = Node<{}, 'DecimalDisplayNode'>; + +const PIN_HEIGHT = 20; + +const DecimalDisplayNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const updateNodeInternals = useUpdateNodeInternals(); + const doc = documentStore.find(props.id) as FlowNode | undefined; + if (!doc) { + return null; + } + const pins = doc.deriver.pins; + return ( + +
+ {Array.from({ length: pins }).map((_, i) => { + const handle = i === 0 ? 'a' : i === 1 ? 'b' : `p${i}`; + const edge = doc.edgesIn.find((e) => e.flowData.targetHandle === handle); + return ( + 0 && shared.on + )} + style={{ + top: `${pins * PIN_HEIGHT - 9 - i * PIN_HEIGHT}px`, + ['--data-label' as any]: `"${1 << i}"` //`"2^${i}"` + }} + /> + ); + })} +
+
+
+
+ ); +}); + +export default DecimalDisplayNode; diff --git a/packages/hfr/circuit/components/Nodes/DecimalDisplay/styles.module.scss b/packages/hfr/circuit/components/Nodes/DecimalDisplay/styles.module.scss new file mode 100644 index 000000000..14f335724 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/DecimalDisplay/styles.module.scss @@ -0,0 +1,37 @@ +.decimal { + display: flex; + width: 40px; + height: var(--data-size); + align-items: center; + justify-content: space-between; + border: 1px solid var(--ifm-font-color-base); + border-radius: var(--ifm-global-radius); + .pins { + display: flex; + flex-direction: row; + .pin { + &::after { + content: var(--data-label, '2'); + font-size: 20%; + position: absolute; + top: -3px; + left: 8px; + } + } + } + .valueContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + .pinBtn { + opacity: 0.2; + transition: opacity 0.3s ease; + } + &:hover { + .pinBtn { + opacity: 1; + } + } + } +} diff --git a/packages/hfr/circuit/components/Nodes/Led/index.tsx b/packages/hfr/circuit/components/Nodes/Led/index.tsx new file mode 100644 index 000000000..7b7de77cd --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Led/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position } from '@xyflow/react'; +import { mdiLedOff, mdiLedOn } from '@mdi/js'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import Icon from '@mdi/react'; +import NodeWrapper from '../NodeWrapper'; + +export type LedNode = Node<{}, 'LedNode'>; + +const LedNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const doc = documentStore.find(props.id) as FlowNode | undefined; + const isPowered = doc?.inputEdgeA ? doc?.deriver.power : 0; + if (!doc) { + return null; + } + return ( + + + + + + ); +}); + +export default LedNode; diff --git a/packages/hfr/circuit/components/Nodes/Led/styles.module.scss b/packages/hfr/circuit/components/Nodes/Led/styles.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hfr/circuit/components/Nodes/NodeWrapper.tsx b/packages/hfr/circuit/components/Nodes/NodeWrapper.tsx new file mode 100644 index 000000000..178da9056 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/NodeWrapper.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { observer } from 'mobx-react-lite'; + +interface Props { + node: FlowNode; + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +} + +const NodeWrapper = observer((props: Props) => { + return ( +
+ {props.children} +
+ ); +}); + +export default NodeWrapper; diff --git a/packages/hfr/circuit/components/Nodes/Not/assets/Gate-NOT.svg b/packages/hfr/circuit/components/Nodes/Not/assets/Gate-NOT.svg new file mode 100644 index 000000000..d2c7050a3 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Not/assets/Gate-NOT.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/hfr/circuit/components/Nodes/Not/index.tsx b/packages/hfr/circuit/components/Nodes/Not/index.tsx new file mode 100644 index 000000000..94ebbe852 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Not/index.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position } from '@xyflow/react'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import NodeWrapper from '../NodeWrapper'; +import NotGate from './assets/Gate-NOT.svg'; + +export type NotNode = Node<{}, 'NotNode'>; + +const NotNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const doc = documentStore.find(props.id) as FlowNode | undefined; + if (!doc) { + return null; + } + return ( + + + NOT + + 0 && shared.on, shared.handle)} + position={Position.Right} + style={{ right: '0px' }} + /> + + ); +}); + +export default NotNode; diff --git a/packages/hfr/circuit/components/Nodes/Not/styles.module.scss b/packages/hfr/circuit/components/Nodes/Not/styles.module.scss new file mode 100644 index 000000000..77f80854c --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Not/styles.module.scss @@ -0,0 +1,6 @@ +.not { + width: 40px; + height: 40px; + font-size: 50%; + padding-right: 10px; +} diff --git a/packages/hfr/circuit/components/Nodes/Or/assets/Gate-OR.svg b/packages/hfr/circuit/components/Nodes/Or/assets/Gate-OR.svg new file mode 100644 index 000000000..83ed1e2c9 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Or/assets/Gate-OR.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/hfr/circuit/components/Nodes/Or/index.tsx b/packages/hfr/circuit/components/Nodes/Or/index.tsx new file mode 100644 index 000000000..3047513b6 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Or/index.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position } from '@xyflow/react'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import { NodeType } from '@hfr/circuit'; +import NodeWrapper from '../NodeWrapper'; +import OrGate from './assets/Gate-OR.svg'; + +export type OrNode = Node<{}, 'OrNode'>; + +const OrNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const doc = documentStore.find(props.id) as FlowNode | undefined; + if (!doc) { + return null; + } + return ( + + + OR + + + + + ); +}); + +export default OrNode; diff --git a/packages/hfr/circuit/components/Nodes/Or/styles.module.scss b/packages/hfr/circuit/components/Nodes/Or/styles.module.scss new file mode 100644 index 000000000..b3f4e7584 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Or/styles.module.scss @@ -0,0 +1,4 @@ +.or { + width: 80px; + height: 60px; +} diff --git a/packages/hfr/circuit/components/Nodes/Switch/index.tsx b/packages/hfr/circuit/components/Nodes/Switch/index.tsx new file mode 100644 index 000000000..f1954fba7 --- /dev/null +++ b/packages/hfr/circuit/components/Nodes/Switch/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import shared from '../styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { Handle, Node, NodeProps, Position } from '@xyflow/react'; +import Button from '@tdev-components/shared/Button'; +import { mdiElectricSwitch, mdiElectricSwitchClosed } from '@mdi/js'; +import FlowNode from '@hfr/circuit/models/FlowNode'; +import NodeWrapper from '../NodeWrapper'; +import { NodeType } from '@hfr/circuit'; + +export type SwitchNode = Node<{}, 'SwitchNode'>; + +const SwitchNode = observer((props: NodeProps) => { + const documentStore = useStore('documentStore'); + const doc = documentStore.find(props.id) as FlowNode | undefined; + const onClick = React.useCallback>( + (e) => { + e.preventDefault(); + e.stopPropagation(); + doc?.deriver.toggle(); + }, + [props.id, doc, doc?.deriver.power] + ); + const isPowered = doc?.inputEdgeA ? doc?.deriver.power : 0; + if (!doc) { + return null; + } + return ( + +