Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a8d6650
initial setup, wip
lebalz Sep 3, 2025
dc005ab
setup flow nodes
lebalz Sep 8, 2025
51a512c
Merge branch 'main' into feature/live-nand-game
lebalz Sep 8, 2025
f8ff840
UPDATE INPUT STYLES
lebalz Sep 8, 2025
5848331
update dynamic docs
lebalz Sep 8, 2025
de197bf
setup initial circuit mode
lebalz Sep 8, 2025
da6210b
support streamed updates
lebalz Sep 8, 2025
09af1c0
implement reconnections
lebalz Sep 8, 2025
dd8e83c
hide attribution
lebalz Sep 8, 2025
a390ea8
Merge branch 'main' into feature/live-nand-game
lebalz Sep 9, 2025
f984bb4
introduce and, or and switch
lebalz Sep 10, 2025
c853cde
fix stream
lebalz Sep 10, 2025
a0ea3ee
fix setup
lebalz Sep 11, 2025
74dd444
Merge branch 'main' into feature/live-nand-game
lebalz Sep 13, 2025
fbceba2
fix lag
lebalz Sep 13, 2025
aca2972
add basic nodes
lebalz Sep 13, 2025
93f1462
add led
lebalz Sep 13, 2025
d2d7f78
update flow node
lebalz Sep 13, 2025
67b5d60
add xor
lebalz Sep 13, 2025
d097722
ensure propper internal updates
lebalz Sep 14, 2025
a191de6
add not
lebalz Sep 14, 2025
03898f8
fix type
lebalz Sep 14, 2025
f7936c5
add decimal display
lebalz Sep 14, 2025
80a03d5
Merge branch 'main' into feature/live-nand-game
lebalz Oct 14, 2025
190d1fa
update yarn.lock
lebalz Oct 14, 2025
1a6f0a7
Merge branch 'main' into feature/live-nand-game
lebalz Oct 14, 2025
50b1b4c
Merge branch 'main' into feature/live-nand-game
lebalz Oct 14, 2025
8f7cda4
Merge branch 'main' into feature/live-nand-game
lebalz Oct 14, 2025
30365a5
Merge branch 'main' into feature/live-nand-game
lebalz Oct 14, 2025
fcf3487
Merge branch 'main' into feature/live-nand-game
lebalz Dec 1, 2025
5b0d7a5
add yarn.lock
lebalz Dec 1, 2025
4b55dfd
some basic stuff
lebalz Dec 1, 2025
1ac7da4
Merge branch 'main' into feature/live-nand-game
lebalz Dec 18, 2025
54300c6
add type mapping to plugin
lebalz Dec 18, 2025
25c14c2
Merge branch 'main' into feature/live-nand-game
lebalz Dec 18, 2025
57adf0f
update to new plugin architecture
lebalz Dec 18, 2025
71fa191
move to hfr packages
lebalz Dec 18, 2025
1076f86
format
lebalz Dec 18, 2025
2333a33
warn
lebalz Dec 18, 2025
6e05de9
debug
lebalz Dec 18, 2025
42b33ee
Merge branch 'main' into feature/live-nand-game
lebalz Dec 18, 2025
99a4949
propper imports
lebalz Dec 19, 2025
1674190
Merge branch 'main' into feature/live-nand-game
lebalz Dec 28, 2025
0736412
fix dynamic documents in offline api
lebalz Dec 28, 2025
da1b789
remove logs, fix type error
lebalz Dec 28, 2025
27431bc
update gates
lebalz Dec 28, 2025
0ee69bf
fix es-tk import
lebalz Dec 30, 2025
7204730
use smaller gate asstes
lebalz Dec 30, 2025
25ffcc5
Merge branch 'main' into feature/live-nand-game
lebalz Dec 30, 2025
4cf3144
fix package versions
lebalz Dec 30, 2025
ed95c02
fix max width
lebalz Dec 30, 2025
e9438b9
add copy paste
lebalz Dec 30, 2025
544aa17
copy edges too
lebalz Dec 30, 2025
78495bb
use smoothstep
lebalz Dec 30, 2025
4fd45d2
update switch
lebalz Dec 30, 2025
8ffa403
introduce document roots deletion in offline api
lebalz Dec 31, 2025
b2d1aee
update deps
lebalz Dec 31, 2025
00d9a04
update assets
lebalz Jan 1, 2026
f3bba09
cleanup copy paste hook usage
lebalz Jan 1, 2026
670809e
Merge branch 'main' into feature/live-nand-game
lebalz Jan 1, 2026
68070f1
Merge branch 'main' into feature/live-nand-game
lebalz Jan 1, 2026
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
4 changes: 4 additions & 0 deletions packages/hfr/_category_.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
label: '@hfr/'
link:
type: generated-index
title: '@hfr/ Packages'
16 changes: 16 additions & 0 deletions packages/hfr/circuit/README.mdx
Original file line number Diff line number Diff line change
@@ -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
<DynamicDocumentRoots id="e8844ea2-cdc7-4e9c-ba72-eef74511ff5f" name="Schaltkreise" roomType='circuit'/>
```

<BrowserWindow>
<DynamicDocumentRoots id="e7abf364-54c5-4b0d-a078-dfb22c8bab5f" name="Schaltkreise" roomType='circuit'/>
</BrowserWindow>
195 changes: 195 additions & 0 deletions packages/hfr/circuit/components/Circuit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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, useReactFlow } 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';
import { useCopyPaste } from './hooks/useCopyPaste';
import CircuitRoom from '../models/CircuitRoom';

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');
useCopyPaste(dynamicRoot.room);
const edgeReconnectSuccessful = React.useRef(true);
const onChange = React.useCallback<OnNodesChange<Node>>(
(change) => {
dynamicRoot.room!.onNodesChange(change);
},
[dynamicRoot.room]
);
const onChangeEdge = React.useCallback<OnEdgesChange<Edge>>(
(change) => {
dynamicRoot.room!.onEdgeChange(change);
},
[dynamicRoot.room]
);
const onConnect = React.useCallback<OnConnect>(
(connection) => {
dynamicRoot.room!.onConnect(connection);
},
[dynamicRoot.room]
);
const onNodesDelete = React.useCallback<OnNodesDelete>(
(deleted) => {
edgeReconnectSuccessful.current = true;
dynamicRoot.room!.onDelete(deleted);
},
[dynamicRoot.room]
);
const onReconnectStart = React.useCallback<OnReconnectStart>(() => {
edgeReconnectSuccessful.current = false;
}, []);

const onReconnect = React.useCallback<OnReconnect>(
(oldEdge, newConnection) => {
edgeReconnectSuccessful.current = true;
dynamicRoot.room!.reconnectEdge(oldEdge.id, newConnection);
},
[dynamicRoot.room]
);

const onReconnectEnd = React.useCallback<OnReconnectEnd>((_, 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 (
<div className={clsx(styles.wrapper)}>
<div className={clsx(styles.rooms)}>
<h1 className={clsx(styles.name)}>
{dynamicRoot.name} <PermissionsPanel documentRootId={dynamicRoot.rootDocumentId} />
</h1>
<div style={{ width: '100%', height: '80vh' }}>
<ReactFlow
nodeTypes={nodeTypes}
nodes={dynamicRoot.room!.nodes}
edges={dynamicRoot.room!.edges}
onNodesDelete={onNodesDelete}
onNodesChange={onChange}
onEdgesChange={onChangeEdge}
onConnect={onConnect}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
fitView
minZoom={0.1}
maxZoom={4}
snapToGrid={true}
snapGrid={[10, 10]}
proOptions={{ hideAttribution: true }}
>
<MiniMap />
<Controls />
<Panel position="top-right" className={clsx(styles.panel)}>
<Button
icon={mdiElectricSwitch}
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.SwitchNode, { power: 0 });
}}
/>
<Button
text="OR"
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.OrNode, {});
}}
/>
<Button
text="XOR"
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.XorNode, {});
}}
/>
<Button
text="AND"
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.AndNode, {});
}}
/>
<Button
text="123"
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.DecimalDisplayNode, { pins: 4 });
}}
/>
<Button
text="NOT"
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.NotNode, {});
}}
/>
<Button
icon={mdiLedOn}
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.LedNode, {});
}}
/>
<Button
icon={mdiCarBattery}
size={1}
color="blue"
onClick={() => {
dynamicRoot.room!.addFlowNode(NodeType.BatteryNode, { pins: 5 });
}}
/>
</Panel>
<Background />
</ReactFlow>
</div>
</div>
</div>
);
});
export default Circuit;
51 changes: 51 additions & 0 deletions packages/hfr/circuit/components/Nodes/And/index.tsx
Original file line number Diff line number Diff line change
@@ -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<AndNode>) => {
const documentStore = useStore('documentStore');
const doc = documentStore.find(props.id) as FlowNode<NodeType.AndNode> | undefined;
if (!doc) {
return null;
}
return (
<NodeWrapper node={doc} className={clsx(styles.and, shared.gate)}>
<AndGate className={shared.image} />
AND
<Handle
type="target"
position={Position.Left}
style={{ top: '10px' }}
className={clsx(doc.inputEdgeA?.isPowerOn && shared.on, shared.handle)}
id="a"
/>
<Handle
type="target"
position={Position.Left}
style={{ top: '30px' }}
className={clsx(doc.inputEdgeB?.isPowerOn && shared.on, shared.handle)}
id="b"
/>
<Handle
type="source"
position={Position.Right}
style={{
right: '0px'
}}
className={clsx(doc.deriver.output && shared.on, shared.handle)}
/>
</NodeWrapper>
);
});

export default AndNode;
4 changes: 4 additions & 0 deletions packages/hfr/circuit/components/Nodes/And/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.and {
width: 60px;
height: 40px;
}
68 changes: 68 additions & 0 deletions packages/hfr/circuit/components/Nodes/Battery/index.tsx
Original file line number Diff line number Diff line change
@@ -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<BatteryNode>) => {
const documentStore = useStore('documentStore');
const updateNodeInternals = useUpdateNodeInternals();
const doc = documentStore.find(props.id) as FlowNode<NodeType.BatteryNode> | undefined;
if (!doc) {
return null;
}
const pins = doc?.deriver.pins ?? 3;
return (
<NodeWrapper node={doc} className={styles.battery}>
<Icon path={mdiCarBattery} color="var(--ifm-color-primary)" size={1} />
<Handle
type="source"
position={Position.Top}
className={clsx(shared.on, shared.handle)}
style={{ top: -2, left: '17px' }}
/>
{Array.from({ length: pins }).map((_, i) => (
<Handle
key={i}
id={i === 0 ? 'a' : i === 1 ? 'b' : `p${i}`}
type="target"
position={i === 0 ? Position.Bottom : Position.Top}
className={clsx(shared.handle)}
style={{ top: `${25 - (i === 0 ? 5 : 0)}px`, left: `${5 + i * 50}px` }}
/>
))}
<div className={clsx(styles.ground)} style={{ ['--data-width' as any]: `${(pins - 1) * 50}px` }}>
<Button
icon={mdiPlusCircle}
size={0.3}
className={clsx(styles.pin, styles.pinAdd)}
onClick={() => {
doc?.deriver.addPin();
updateNodeInternals(props.id);
}}
/>
<Button
icon={mdiMinusCircle}
size={0.3}
className={clsx(styles.pin, styles.pinRemove)}
onClick={() => {
doc?.deriver.removePin();
updateNodeInternals(props.id);
}}
/>
</div>
</NodeWrapper>
);
});

export default BatteryNode;
20 changes: 20 additions & 0 deletions packages/hfr/circuit/components/Nodes/Battery/styles.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Loading