diff --git a/doc/EXECUTION_STATE_DETECTION.md b/doc/EXECUTION_STATE_DETECTION.md new file mode 100644 index 0000000..848a382 --- /dev/null +++ b/doc/EXECUTION_STATE_DETECTION.md @@ -0,0 +1,106 @@ +# Execution State Detection — Design Notes + +## Problem + +Online monitoring needs to display which nodes are currently "active" (being executed) on the PLC. ADS provides variable read/write and PLC state queries, but **no direct way to query whether a specific code block is executing**. We must infer execution state ourselves. + +## Execution Model + +Every PLC program has a **Task** that cyclically calls a **PRG** (entry point). From there, execution flows deterministically through the call chain: + +``` +Task (cyclic) → PRG → FB calls → Method calls → ... +``` + +At each branching point (IF, CASE, FOR), the taken branch depends on runtime variable values — which we *can* read via ADS. + +## Two Categories + +### 1. Deterministic call chain (PRG → FB → Method) + +The PRG entry is always active while the Task runs. From there, the execution graph is fully known from the flow document. At each conditional branch, we can read the condition variable via ADS and determine which path is active. + +**Approach: Graph-based condition propagation** + +1. Start from the PRG entry node — always active when PLC is in `Run` state +2. Walk the exec chain (ENO, TRUE, DO branches) +3. At each conditional node (IF, CASE), read the runtime condition value from ADS +4. Propagate "active" only along the taken branch +5. When a method call node is active → the corresponding method body entry is active +6. Recurse into the method body with the same logic + +**Example:** +``` +Entry(MAIN) → Timer → Counter → Comparison → IF(COND=true) + ├─ TRUE → MethodCall(CleanupCycle) → active + └─ FALSE → ... → idle +``` + +If `IF.COND` reads `TRUE` from ADS → the TRUE branch and everything downstream (including CleanupCycle method body) is marked active. + +**Advantages:** +- No extra PLC code or variables needed +- Uses only existing ADS variable reads (already available for value display) +- Computation happens entirely on the monitor server / frontend +- Follows the actual execution semantics accurately + +**Limitations:** +- Only works for call chains originating from the flow graph we control +- Timing: we read condition values at poll intervals, not at exact PLC scan boundaries — but this is acceptable for visualization (same polling we use for value display) + +### 2. Non-deterministic access (Property GET/SET) + +Property accessors are fundamentally different: + +- A **GET** accessor runs whenever *any* external code reads the property (`fbInstance.PropertyName`) +- A **SET** accessor runs whenever *any* external code writes to it +- The caller may be outside our flow graph (another POU, library code, HMI, etc.) +- The same property can be accessed from multiple places, with different cycle times +- There is no single "call node" in our graph that gates the accessor execution + +The graph-based approach cannot determine whether a property accessor is running, because the caller is not necessarily part of the flow we're monitoring. + +**Approach: Generated execution counter** + +The code generator injects a hidden counter variable into each property accessor body: + +```iecst +// Auto-generated by FlowForge — do not modify +VAR + _ff_execCount : UDINT; // Increments each call +END_VAR + +// First line of GET/SET body: +_ff_execCount := _ff_execCount + 1; +``` + +The monitor polls `_ff_execCount` at regular intervals: +- If `current - previous > 0` → accessor is being called → mark as **active** +- If unchanged → no calls since last poll → mark as **idle** + +**Advantages:** +- Works regardless of who calls the accessor +- Accurate: directly measures actual execution +- Minimal PLC overhead: single UDINT increment per call + +**Considerations:** +- Requires the code generator to inject the counter variable and increment statement +- UDINT overflow: wraps at ~4.29 billion — not a practical concern (at 1ms cycle, wraps after ~49 days; and we only check delta, not absolute value) +- Polling interval determines detection latency (same as variable value display) + +## Summary + +| Aspect | PRG / FB / Method body | Property GET / SET | +|--------|----------------------|-------------------| +| Call chain | Known from flow graph | Unknown (external callers) | +| Detection method | Graph-based condition propagation | Generated `_ff_execCount` counter | +| Extra PLC code needed | No | Yes (one UDINT + increment per accessor) | +| Accuracy | Condition-value dependent | Direct measurement | +| Computation location | Monitor server / frontend | Monitor reads counter via ADS | + +## Implementation Notes + +- The graph-based propagation reuses the execution chain walker (`computeExecutionOrder` / `computeExecutionChains`) — extend it to evaluate runtime conditions +- The `_ff_execCount` variable follows the naming convention `_ff_*` to avoid collision with user variables and to allow the monitor to discover them automatically via ADS symbol enumeration +- Both approaches use the same polling interval as variable value display (no additional ADS traffic) +- Edge case: nested FB instances calling the same property — the counter captures all calls regardless of source, which is the desired behavior diff --git a/doc/VISILY_PROMPTS.md b/doc/VISILY_PROMPTS.md new file mode 100644 index 0000000..e618e9b --- /dev/null +++ b/doc/VISILY_PROMPTS.md @@ -0,0 +1,289 @@ +# FlowForge UI/UX Koncepció — Visily.ai Promptok + +4 prompt, mindegyik max 4000 karakter. Sorrendben generáld! + +Design tokenek a Unity Visual Scripting referencia képekből (csspicker.dev): +- Canvas: #1a1a1a, vonalrács #222 40px +- Node body: #262626, border #333, border-radius 4px, shadow 0 4px 15px rgba(0,0,0,0.5) +- Header: színezett bg + ikon + kétsoros (típus 11px felül, név 13px alul) +- Selected: #3b82f6 border + glow +- Port kör: 8px, execution: chevron nyíl +- Sidebar: #252525, toolbar: #333 + +--- + +## Prompt 1 — Teljes Editor Layout (Edit mód) + +``` +Design a dark-themed visual node-based PLC programming editor called "FlowForge". Style matches Unity Visual Scripting exactly. + +LAYOUT (full-screen, no scroll): +- TOP: Slim toolbar (48px), #333333 +- LEFT: Node Toolbox panel (260px wide, #252525) +- CENTER: Flow Canvas (#1a1a1a with thin line grid #222, 40px spacing) +- RIGHT: Node Inspector panel (280px wide, #252525) +- BOTTOM: Collapsed Watch bar (32px, #333) + +TOOLBAR (left-center-right): +- Left: "FF" teal logo + "Motor Control v2.1" breadcrumb +- Center: Segmented toggle "Edit" (active, filled #3b82f6) / "Online" (inactive, gray) +- Right: Build (hammer icon), Deploy (rocket icon), "Saved 2 min ago" muted, user avatar circle + +LEFT PANEL — NODE TOOLBOX: +- "Nodes" title, search input below (#171717, border #404040, "Search nodes...") +- Filter chips: "All" (active #3b82f6), "Favorites", "I/O", "Logic", "Timers", "Math" +- Expandable category sections with colored dots: + I/O (teal #4A9E9E): Digital Input, Digital Output, Analog Input, Analog Output + Timers (blue #5BA0D5): TON, TOF, TP + Logic (collapsed), Math (collapsed), Counters (collapsed) +- Each item: colored icon | name (bold) | description (muted #999) | star for favorite +- Footer: "Drag to canvas or double-click to add" + +CENTER — CANVAS: +6 connected nodes on line grid. Nodes are dark rounded rectangles (#262626, 4px radius, 1px #333 border, shadow 0 4px 15px rgba(0,0,0,0.5)). Each node has a colored category header strip with icon + TWO lines (small type name 11px + node name 13px bold below). Ports are 8px colored circles at node edges with 11px labels. Execution ports use chevron arrows, not circles. + +Nodes left-to-right: +1. "Start Button" (teal I/O header) — OUT port (green #84cc16) +2. "Delay T#2S" (blue Timer header) — IN, PT inputs / Q, ET outputs +3. "Part Counter" (steel blue Counter header, SELECTED: #3b82f6 border + blue glow) — CU, RESET, PV / Q, CV +4. ">=" (purple Compare header) — A, B inputs / OUT output +5. "Motor ON" (teal Output header) — IN port +6. "Batch Done" (teal Output header) — IN port + +Wires: smooth bezier, 2px. green=#84cc16 (BOOL), blue=#60a5fa (INT), teal=#2dd4bf (TIME). +MiniMap bottom-right, zoom controls bottom-left. + +RIGHT PANEL — NODE INSPECTOR: +- "Properties" title + "CTU" pill badge +- General: Label "Part Counter", Type "CTU (Up Counter)", ID "counter_1" +- Input Ports: CU "← Start Button.OUT" (teal chip), RESET "Not connected", PV "10" +- Output Ports: Q "Not connected", CV "→ Comparison.A" (teal chip) +- Notes textarea +- Footer: "1 node selected" + +BOTTOM: "▸ Watch" left, "0 variables" right + +STYLE: Inter font, #e0e0e0 primary text, #999 secondary, #3b82f6 accent. Clean, professional. +``` + +--- + +## Prompt 2 — Canvas Node-ok részletesen (zoom) + +``` +Design a zoomed-in canvas showing 6 interconnected nodes for a PLC visual editor "FlowForge". Matches Unity Visual Scripting exactly. Show ONLY the canvas area, no panels or toolbar. + +BACKGROUND: #1a1a1a with thin line grid (#222 lines, 40px spacing) + +NODE DESIGN (critical): +- Body: #262626, border-radius 4px, 1px solid #333, shadow 0 4px 15px rgba(0,0,0,0.5) +- Header: colored background strip with icon on left + TWO text lines: + Line 1: component type (11px, semibold, slightly transparent) + Line 2: node name (13px, medium weight) +- Header bottom border: 1px solid rgba(0,0,0,0.2) +- Port rows: 24px each, 8px colored circle at edge + label (11px, #a3a3a3) +- Execution ports: white chevron arrow (not circle) +- Width: 160-200px + +CATEGORY HEADER COLORS: +- I/O: teal #4A9E9E, gamepad icon +- Timer: sky blue #5BA0D5, clock icon +- Counter: steel blue #6A8EBF, tally icon +- Compare: purple #8B6BBF, compare icon + +PORT COLORS (circle + wire): +- BOOL: green #84cc16 +- INT: blue #60a5fa +- TIME: teal #2dd4bf + +NODE 1 — "Start Button": +Header teal bg: "INPUT" (11px) / "Start Button" (13px bold) +Right side: OUT green circle + +NODE 2 — "Delay T#2S": +Header blue bg: "TIMER" / "Delay T#2S" +Left: IN (green), PT (teal) | Right: Q (green), ET (teal) + +NODE 3 — "Part Counter" SELECTED (bright #3b82f6 border + box-shadow 0 0 0 1px #3b82f6): +Header steel blue bg: "COUNTER" / "Part Counter" +Left: CU (green), RESET (green), PV (blue) | Right: Q (green), CV (blue) + +NODE 4 — ">=": +Header purple bg: "COMPARE" / ">=" +Left: A (blue), B (blue) | Right: OUT (green) + +NODE 5 — "Motor ON": +Header teal bg: "OUTPUT" / "Motor ON" +Left: IN (green) + +NODE 6 — "Batch Done": +Header teal bg: "OUTPUT" / "Batch Done" +Left: IN (green) + +WIRES (smooth bezier curves, 2px): +- Start Button OUT -> Delay IN: GREEN #84cc16 +- Start Button OUT -> Part Counter CU: GREEN (branches from same port) +- Delay Q -> Motor ON IN: GREEN +- Part Counter CV -> >= A: BLUE #60a5fa +- >= OUT -> Batch Done IN: GREEN + +Dark MiniMap (bottom-right), zoom buttons (bottom-left). +``` + +--- + +## Prompt 3 — Oldalsó panelek részletesen + +``` +Design two side panels for a dark PLC node editor "FlowForge" matching Unity Visual Scripting style. Show both panels side by side on #1a1a1a background. + +=== LEFT — NODE TOOLBOX (260px wide) === + +Background #252525, right border 1px #444. + +Header: "Nodes" (14px semibold, #e0e0e0) + +Search: full-width input (#171717, border 1px #404040, focus border #3b82f6, placeholder "Search nodes..." with magnifying glass icon, 12px) + +Filter chips (horizontal): "All" (active — #3b82f6 fill, white text), "Favorites", "I/O", "Logic", "Timers", "Counters", "Math", "Compare" — inactive: #333 bg, #999 text, 10px + +Category sections with collapsible headers: + +"I/O" (expanded): +Header row: teal dot #4A9E9E + "I/O" bold + "4 nodes" #999 + chevron down +Item rows (each ~36px, #333 bg, 3px radius, 2px gap): +- teal square icon | "Digital Input" (bold #e0e0e0) | "Read PLC input" (#999) | star outline +- "Digital Output" — Write PLC output +- "Analog Input" — Read analog value (star FILLED = favorited) +- "Analog Output" — Write analog value + +"Timers" (expanded): +Blue dot #5BA0D5 + "Timers" + "3 nodes" +- "TON" — On-delay timer +- "TOF" — Off-delay timer +- "TP" — Pulse timer + +"Logic" (collapsed): green dot #6B9E5B + "Logic" + "4 nodes" + chevron right +"Math" (collapsed): orange dot #C89B5B + "Math" + "4 nodes" +"Counters" (collapsed): steel blue dot #6A8EBF + "Counters" + "3 nodes" + +Footer: "Drag to canvas or double-click to add" (#666, 11px) + +=== RIGHT — NODE INSPECTOR (280px wide) === + +Background #252525, left border 1px #444. + +Header: "Properties" (14px semibold) + small pill "CTU" (#333 bg, #999 text) + +Collapsible sections (11px uppercase headers, #999): + +"General": +- Label: text input (#171717, border #404040) -> "Part Counter" +- Type: read-only pill "CTU (Up Counter)" colored #6A8EBF +- ID: muted #666 -> "counter_1" + +"Input Ports": +- CU (BOOL): "← Start Button.OUT" as teal #4A9E9E link chip with arrow +- RESET (BOOL): "Not connected" italic #666 +- PV (INT): number input (#171717) -> "10" + +"Output Ports": +- Q (BOOL): "Not connected" italic #666 +- CV (INT): "→ Comparison.A" teal chip + +"Notes": +- Textarea (#171717, border #404040), placeholder "Add notes..." + +Footer: "1 node selected" (#666, 12px) + +FONTS: Inter 12-13px, JetBrains Mono 11px for values/monospace. +Text: #e0e0e0 primary, #a3a3a3 labels, #999 secondary, #666 muted. +``` + +--- + +## Prompt 4 — Online Monitor nézet (élő PLC adatokkal) + +``` +Design the ONLINE MONITORING view of a dark PLC node editor "FlowForge". Same Unity Visual Scripting style but showing LIVE PLC data. The canvas should look ALIVE — like a control room dashboard overlaid on a node graph. + +TOOLBAR CHANGES from edit mode: +- Segmented toggle: "Online" active (filled #22C55E green, white text), "Edit" inactive gray +- New: green dot + "PLC-Demo (5.39.123.1.1.1)" label + "Connected" green text +- Right side: red "Go Offline" button + +LEFT PANEL: Collapsed to thin 16px vertical strip with toggle arrow + +CANVAS (#1a1a1a, line grid #222, 40px): +Same 6 nodes (#262626, 4px radius, #333 border), now with live data overlays: + +Each port shows CURRENT VALUE in a small rounded pill next to the label: +- BOOL TRUE: "TRUE" in green #84cc16 on rgba(132,204,22,0.12) bg +- BOOL FALSE: "FALSE" in gray #666 on rgba(100,100,100,0.1) bg +- INT: "7" in blue #60a5fa on rgba(96,165,250,0.12) bg +- TIME: "T#1400MS" in teal #2dd4bf on rgba(45,212,191,0.12) bg + +Node execution glow effects: +- ACTIVE: green border #22C55E + glow shadow (0 0 15px rgba(34,197,94,0.35)) +- ERROR: red border #EF4444 + red glow +- IDLE: normal, no glow + +Node states: +- "Start Button" ACTIVE (green glow), OUT: TRUE +- "Delay T#2S" ACTIVE, IN: TRUE, PT: T#2S, Q: TRUE, ET: T#1400MS +- "Part Counter" ACTIVE+SELECTED (#3b82f6 border, green glow behind), CU: TRUE, RESET: FALSE, PV: 10, Q: FALSE, CV: 7 +- "Motor ON" ACTIVE, IN: TRUE +- ">=" ACTIVE, A: 7, B: 10, OUT: FALSE +- "Batch Done" IDLE (no glow), IN: FALSE + +WIRE BADGES: Active wires 3px thick, brighter. At each wire midpoint, a floating dark pill (#1a1a1a, 1px #444 border) with type-colored value text: +- Start Button -> Delay: "TRUE" (green) +- Counter CV -> >= A: "7" (blue) +- >= OUT -> Batch Done: "FALSE" (gray) + +RIGHT PANEL — INSPECTOR with extra "Live Values" section at top: +Green dot + "Live Values" header: +CU: TRUE (green pill), RESET: FALSE (gray), PV: 10 (blue), Q: FALSE (gray), CV: 7 (blue) + +BOTTOM — WATCH TABLE (expanded ~200px): +Header: "Watch" + "2 variables" badge + search input + "Add" button +Table: Variable | Value | Type | Timestamp | x +Row 1: MAIN.counter_1.CV | "7" (blue mono) | INT | 14:23:05.123 | x +Row 2: MAIN.timer_1.ET | "T#1400MS" (teal mono) | TIME | 14:23:05.089 | x +Alternating row bg: #252525 / #2a2a2a. Headers: uppercase 10px #999. +``` + +--- + +## Használati sorrend + +1. **Prompt 1** — Teljes layout áttekintés +2. **Prompt 2** — Canvas zoom, node-ok és vezetékek részletesen +3. **Prompt 3** — Toolbox + Inspector panelek +4. **Prompt 4** — Online monitor nézet élő adatokkal + +## Frissített design tokenek (referencia képekből) + +| Token | Érték | +|-------|-------| +| Canvas háttér | #1a1a1a | +| Grid | vonalrács, #222, 40px | +| Node body | #262626 | +| Node border | 1px solid #333 | +| Node radius | 4px | +| Node shadow | 0 4px 15px rgba(0,0,0,0.5) | +| Sidebar/panel bg | #252525 | +| Toolbar bg | #333333 | +| Panel border | 1px solid #444 | +| Input fields | #171717, border #404040 | +| Selected node | #3b82f6 border + glow | +| Primary text | #e0e0e0 | +| Secondary text | #999 | +| Port labels | #a3a3a3 | +| Muted/disabled | #666 | +| BOOL port | #84cc16 (green) | +| INT port | #60a5fa (blue) | +| TIME port | #2dd4bf (teal) | +| REAL port | #f97316 (orange) | +| STRING port | #CC5BAA (magenta) | +| Execution port | white chevron arrow | diff --git a/src/frontend/index.html b/src/frontend/index.html index f33068d..bb49201 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -3,6 +3,9 @@ + + + FlowForge diff --git a/src/frontend/ref/1_k7Zt-r8gQ2d8q3cWWKbK3w-3971025397.gif b/src/frontend/ref/1_k7Zt-r8gQ2d8q3cWWKbK3w-3971025397.gif new file mode 100644 index 0000000..5b9c583 Binary files /dev/null and b/src/frontend/ref/1_k7Zt-r8gQ2d8q3cWWKbK3w-3971025397.gif differ diff --git a/src/frontend/ref/FlowForge WebApp - Edit Mode-1.png b/src/frontend/ref/FlowForge WebApp - Edit Mode-1.png new file mode 100644 index 0000000..1397466 Binary files /dev/null and b/src/frontend/ref/FlowForge WebApp - Edit Mode-1.png differ diff --git a/src/frontend/ref/FlowForge WebApp - Edit Mode-2.png b/src/frontend/ref/FlowForge WebApp - Edit Mode-2.png new file mode 100644 index 0000000..be773de Binary files /dev/null and b/src/frontend/ref/FlowForge WebApp - Edit Mode-2.png differ diff --git a/src/frontend/ref/FlowForge WebApp - Edit Mode.png b/src/frontend/ref/FlowForge WebApp - Edit Mode.png new file mode 100644 index 0000000..7e75ccb Binary files /dev/null and b/src/frontend/ref/FlowForge WebApp - Edit Mode.png differ diff --git a/src/frontend/ref/i7xtwzw7leu11-2549386816.png b/src/frontend/ref/i7xtwzw7leu11-2549386816.png new file mode 100644 index 0000000..ec4c7a0 Binary files /dev/null and b/src/frontend/ref/i7xtwzw7leu11-2549386816.png differ diff --git a/src/frontend/ref/i7xtwzw7leu11-2549386816_react.txt b/src/frontend/ref/i7xtwzw7leu11-2549386816_react.txt new file mode 100644 index 0000000..f337193 --- /dev/null +++ b/src/frontend/ref/i7xtwzw7leu11-2549386816_react.txt @@ -0,0 +1,704 @@ +import React, { useState } from 'react'; +import { + Play, + Pause, + SkipForward, + Upload, + Download, + Zap, + Search, + MousePointer2, + Layers, + Box, + Tag, + Eye, + ChevronRight, + ChevronDown, + X, + Plus, + User, + Settings, + Activity, + ArrowRight, + Maximize2 +} from 'lucide-react'; + +interface NodeProps { + title: string; + color: string; + inputs?: string[]; + outputs?: string[]; + style?: React.CSSProperties; + icon?: React.ReactNode; + type?: 'event' | 'action' | 'logic' | 'data'; +} + +const Node: React.FC = ({ title, color, inputs = [], outputs = [], style, icon, type = 'action' }) => { + return ( +
+
+ {icon && {icon}} + {title} +
+
+ {inputs.map((input, i) => ( +
+
+ {input} +
+ ))} + {outputs.map((output, i) => ( +
+ {output} +
+
+ ))} +
+
+ ); +}; + +const SidebarItem = ({ label, icon: Icon, color, expanded = false, children }: any) => { + const [isOpen, setIsOpen] = useState(expanded); + return ( +
+
setIsOpen(!isOpen)}> +
+ + {Icon && } + {label} +
+ +
+ {isOpen && children &&
{children}
} +
+ ); +}; + +const App: React.FC = () => { + return ( +
+ {/* Top Toolbar */} +
+
+
+ + + +
+
+ + + +
+
+
+ +
+ {/* Left Sidebar */} + + + {/* Main Graph Area */} +
+
+
+
Graph
+
+
+ Player Damage +
+
+
+ Zoom + + 1x +
+
+ + + + + + +
+
+
+ +
+
+ + {/* Nodes Recreated from Image */} + } + /> + + + + } + /> + + } + /> + +
+
+ Collider +
+
+ +
+ + Get Tag +
+
+ + {/* Connections (Simplified SVG) */} + + + + + + + + + + +
+ + {/* Script Preview Panel */} +
+
+ Script Preview +
+ Player Damage +
+
+
+
+ {Array.from({ length: 11 }).map((_, i) =>
{i + 1}
)} +
+
+                
+                  float health;
+ GameObject explosion;
+
+ void OnCollisionEnter(CollisionEventArgs e)
+ {'{'}
+   if (e.collider.tag == "Enemy")
+   {'{'}
+ +     GameObject.Instantiate(explosion, e.contact, Quaternion.identity);
+     health = health - e.force.magnitude; +

+   {'}'}
+ {'}'} +
+
+
+
+
+
+ + +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/src/frontend/ref/maxresdefault-1766503585.jpg b/src/frontend/ref/maxresdefault-1766503585.jpg new file mode 100644 index 0000000..70e335f Binary files /dev/null and b/src/frontend/ref/maxresdefault-1766503585.jpg differ diff --git a/src/frontend/ref/movement-in-Visual-scripting-934982328.jpg b/src/frontend/ref/movement-in-Visual-scripting-934982328.jpg new file mode 100644 index 0000000..7f477cf Binary files /dev/null and b/src/frontend/ref/movement-in-Visual-scripting-934982328.jpg differ diff --git a/src/frontend/ref/ss75-3505835148.png b/src/frontend/ref/ss75-3505835148.png new file mode 100644 index 0000000..5402a2f Binary files /dev/null and b/src/frontend/ref/ss75-3505835148.png differ diff --git a/src/frontend/ref/ss75-3505835148_react.txt b/src/frontend/ref/ss75-3505835148_react.txt new file mode 100644 index 0000000..97a907c --- /dev/null +++ b/src/frontend/ref/ss75-3505835148_react.txt @@ -0,0 +1,358 @@ +import React from 'react'; +import { RefreshCw, CircleDot, ArrowUpRight, ChevronRight, Play, Settings2, Square } from 'lucide-react'; + +interface PortProps { + type: 'exec' | 'data'; + direction: 'input' | 'output'; + color?: string; + label?: string; +} + +const Port = ({ type, direction, color = '#fff', label }: PortProps) => { + const isExec = type === 'exec'; + return ( +
+ {direction === 'input' && label && {label}} +
+ {isExec ? ( + + ) : ( +
+ )} +
+ {direction === 'output' && label && {label}} +
+ ); +}; + +interface NodeProps { + title: string; + subtitle?: string; + headerColor?: string; + isSelected?: boolean; + icon?: React.ReactNode; + children?: React.ReactNode; + style?: React.CSSProperties; +} + +const Node = ({ title, subtitle, headerColor = '#3f3f3f', isSelected, icon, children, style }: NodeProps) => { + return ( +
+
+
{icon}
+
+
{title}
+ {subtitle &&
{subtitle}
} +
+
+
+ {children} +
+
+ ); +}; + +const VisualNodeEditor = () => { + return ( +
+ {/* SVG Layer for Connections */} + + + + + + + + {/* Execution Line: On Update -> Set Velocity */} + + + {/* Data Line: Get Velocity -> Expose (Curved) */} + + + {/* Data Line: Expose -> Create (Curved) */} + + + {/* Data Line: Create -> Set Velocity (Curved) */} + + + + {/* Nodes */} + + {/* Node 1: On Update */} + } + style={{ left: 60, top: 50, width: 160 }} + > +
+ +
+
+ + {/* Node 2: Get Velocity */} + } + style={{ left: 60, top: 350, width: 160 }} + > +
+
+ + + +
+
+ +
+
+
+ + {/* Node 3: Expose Vector 2 */} + } + style={{ left: 300, top: 250, width: 200 }} + > +
+
Instance
+
Static
+
+
+
+ +
+
X
+
Y
+
Normalized
+
Magnitude
+
Sqr Magnitude
+
+
+ + + {/* Node 4: Vector 2 Create */} + } + style={{ left: 560, top: 280, width: 150 }} + > +
+
+
+
X
+
Y
+
+
+
+
+
+
+
+ + {/* Node 5: Set Velocity (Selected) */} + } + style={{ left: 780, top: 50, width: 180 }} + > +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ + +
+ ); +}; + +export default VisualNodeEditor; \ No newline at end of file diff --git a/src/frontend/ref/visual-programming-maya_bifrost_1_crop-3764903204.jpg b/src/frontend/ref/visual-programming-maya_bifrost_1_crop-3764903204.jpg new file mode 100644 index 0000000..ed3591a Binary files /dev/null and b/src/frontend/ref/visual-programming-maya_bifrost_1_crop-3764903204.jpg differ diff --git a/src/frontend/ref/visual-scripting-1024x491-4137351285.png b/src/frontend/ref/visual-scripting-1024x491-4137351285.png new file mode 100644 index 0000000..66db98c Binary files /dev/null and b/src/frontend/ref/visual-scripting-1024x491-4137351285.png differ diff --git a/src/frontend/ref/vs-sample-spawn-gameobject-2178841912.png b/src/frontend/ref/vs-sample-spawn-gameobject-2178841912.png new file mode 100644 index 0000000..b38308c Binary files /dev/null and b/src/frontend/ref/vs-sample-spawn-gameobject-2178841912.png differ diff --git a/src/frontend/ref/what-is-visual-programming_1200x630.jpg b/src/frontend/ref/what-is-visual-programming_1200x630.jpg new file mode 100644 index 0000000..d80b8da Binary files /dev/null and b/src/frontend/ref/what-is-visual-programming_1200x630.jpg differ diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 1e32d6f..abaeb16 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -1,9 +1,1307 @@ -.app { +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: #1a1a1a; + color: #e0e0e0; +} + +/* ── Editor page layout ────────────────────────────────────────────── */ + +.ff-editor-page { width: 100vw; height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; } -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +.ff-editor-content { + flex: 1; + display: flex; + position: relative; + overflow: hidden; + min-height: 0; +} + +.ff-editor-center { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.ff-flow-canvas { + flex: 1; + min-height: 0; +} + +/* ── Toolbar ───────────────────────────────────────────────────────── */ + +.ff-editor-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + background: #333; + border-bottom: 1px solid #444; + height: 48px; + flex-shrink: 0; + gap: 12px; +} + +.ff-toolbar-left { + display: flex; + align-items: center; + gap: 12px; +} + +.ff-toolbar-center { + display: flex; + align-items: center; + gap: 16px; +} + +.ff-toolbar-right { + display: flex; + align-items: center; + gap: 12px; +} + +.ff-toolbar-brand { + display: flex; + align-items: center; + gap: 6px; +} + +.ff-toolbar-brand-icon { + width: 28px; + height: 28px; + border-radius: 6px; + background: #4A9E9E; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 12px; + color: #fff; +} + +.ff-toolbar-brand-name { + color: #666; + font-size: 13px; +} + +.ff-toolbar-breadcrumb { + color: #666; + font-size: 12px; +} + +.ff-toolbar-project { + color: #e0e0e0; + font-size: 13px; + font-weight: 600; +} + +.ff-toolbar-target { + font-size: 12px; + color: #e0e0e0; +} + +.ff-toolbar-error { + font-size: 13px; + color: #ef4444; +} + +.ff-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +.ff-toolbar-connected { + font-size: 11px; + color: #22c55e; + font-weight: 600; +} + +/* Mode toggle */ + +.ff-mode-toggle { + display: flex; + align-items: center; + background: #222; + border-radius: 6px; + padding: 2px; + border: 1px solid #444; +} + +.ff-mode-btn { + padding: 5px 20px; + border-radius: 4px; + border: none; + background: transparent; + color: #666; + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.ff-mode-btn-active-edit { + background: #3b82f6; + color: #fff; +} + +.ff-mode-btn-active-online { + background: #22c55e; + color: #fff; +} + +.ff-toolbar-saved { + font-size: 11px; + color: #666; +} + +.ff-toolbar-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #7c3aed; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + color: #fff; +} + +/* ── Buttons ───────────────────────────────────────────────────────── */ + +.ff-btn { + border: none; + border-radius: 4px; + padding: 5px 12px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; + display: flex; + align-items: center; + gap: 4px; +} + +.ff-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.ff-btn-primary { + background: #3b82f6; + color: #fff; +} + +.ff-btn-primary:hover:not(:disabled) { + background: #2563eb; +} + +.ff-btn-danger { + background: #ef4444; + color: #fff; +} + +.ff-btn-danger:hover:not(:disabled) { + background: #dc2626; +} + +.ff-btn-ghost { + border: 1px solid #444; + background: transparent; + color: #999; +} + +.ff-btn-ghost:hover:not(:disabled) { + background: #3a3a3a; +} + +.ff-btn-sm { + padding: 4px 12px; + font-size: 11px; +} + +/* ── Node base styles ──────────────────────────────────────────────── */ + +.ff-node { + background: #262626; + border: 1px solid #333; + border-radius: 4px; + width: 200px; + font-size: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.ff-node-header { + padding: 5px 10px; + text-align: left; + border-bottom: 1px solid rgba(0,0,0,0.2); + display: flex; + justify-content: space-between; + align-items: flex-start; + border-radius: 3px 3px 0 0; +} + +.ff-node-header-info { + display: flex; + flex-direction: column; +} + +.ff-node-exec-order { + background: rgba(255,255,255,0.2); + color: #fff; + font-size: 10px; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-weight: 600; + padding: 1px 6px; + border-radius: 8px; + white-space: nowrap; + line-height: 1.4; +} + +.ff-node-entry .ff-node-header { background: linear-gradient(135deg, #d94a3d 0%, #8b2520 100%); } +/* varRead/varWrite header gradient set dynamically via inline style (data-type dependent) */ +.ff-node-timer .ff-node-header { background: linear-gradient(135deg, #5a9ed6 0%, #3872a8 100%); } +.ff-node-counter .ff-node-header { background: linear-gradient(135deg, #5a9ed6 0%, #3872a8 100%); } +.ff-node-comparison .ff-node-header { background: linear-gradient(135deg, #26a5a5 0%, #005f6b 100%); } +.ff-node-if .ff-node-header { background: linear-gradient(135deg, #af6cc4 0%, #7b3f94 100%); } +.ff-node-for .ff-node-header { background: linear-gradient(135deg, #af6cc4 0%, #7b3f94 100%); } +.ff-node-methodCall .ff-node-header { background: linear-gradient(135deg, #e09a48 0%, #b0712a 100%); } +.ff-node-methodEntry .ff-node-header { background: linear-gradient(135deg, #d94a3d 0%, #8b2520 100%); } +.ff-node-return .ff-node-header { background: linear-gradient(135deg, #d94a3d 0%, #8b2520 100%); } +.ff-node-propertyEntry .ff-node-header { background: linear-gradient(135deg, #d94a3d 0%, #8b2520 100%); } + +.ff-node-category { + display: block; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + color: rgba(255,255,255,0.7); + letter-spacing: 0.5px; +} + +.ff-node-label { + font-size: 13px; + font-weight: 600; + color: #fff; + text-shadow: 0 0 1px rgba(0,0,0,0.2); +} + +.ff-node-type-path { + display: block; + font-size: 9px; + font-weight: 600; + color: rgba(255,255,255,0.75); + letter-spacing: 0.3px; + text-shadow: 0 0 1px rgba(0,0,0,0.2); +} + +.ff-node-body { + padding: 4px 0; + display: flex; + flex-direction: column; + gap: 0; +} + +/* ── Ports ─────────────────────────────────────────────────────────── */ + +.ff-port { + display: flex; + align-items: center; + gap: 5px; + min-height: 24px; + padding: 2px 8px; + position: relative; +} + +.ff-port-input { + justify-content: flex-start; +} + +.ff-port-output { + justify-content: flex-end; +} + +.ff-port-row { + display: flex; + justify-content: space-between; + position: relative; +} + +.ff-port-row.ff-port-exec { + border-bottom: 1px solid rgba(255,255,255,0.06); +} + +.ff-port-row .ff-port { + flex: 1; + min-width: 0; +} + +.ff-port-label { + color: #a3a3a3; + font-size: 11px; + font-weight: 500; +} + +.ff-port-value { + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 10px; + font-weight: 600; + padding: 1px 6px; + border-radius: 3px; + white-space: nowrap; +} + +/* Per-type value pill colors */ +.ff-port-value-bool { + color: #84cc16; + background: rgba(132, 204, 22, 0.12); +} + +.ff-port-value-int { + color: #60a5fa; + background: rgba(96, 165, 250, 0.12); +} + +.ff-port-value-time { + color: #2dd4bf; + background: rgba(45, 212, 191, 0.12); +} + +.ff-port-value-real { + color: #f97316; + background: rgba(249, 115, 22, 0.12); +} + +.ff-port-value-default { + color: #666; + background: rgba(100, 100, 100, 0.1); +} + +/* Boolean FALSE gets muted */ +.ff-port-value-false { + color: #666 !important; + background: rgba(100, 100, 100, 0.1) !important; +} + +/* ── Execution state styling ───────────────────────────────────────── */ + +.ff-exec-idle { + /* default — no change */ +} + +.ff-exec-active { + border-color: #ffffff !important; + box-shadow: 0 0 4px rgba(255, 255, 255, 0.5), 0 0 8px rgba(255, 255, 255, 0.25), 0 4px 15px rgba(0, 0, 0, 0.5) !important; +} + +.ff-exec-error { + border-color: #ef4444 !important; + box-shadow: 0 0 15px rgba(239, 68, 68, 0.35), 0 4px 15px rgba(0, 0, 0, 0.5) !important; +} + +/* ── Online overlay ────────────────────────────────────────────────── */ + +.ff-online-overlay { + border-radius: 4px; + transition: box-shadow 0.2s, outline-color 0.2s; +} + +.ff-online-active { + outline: 2px solid #22c55e; + outline-offset: 2px; +} + +.ff-online-error { + outline: 2px solid #ef4444; + outline-offset: 2px; +} + +/* ── Side panels ──────────────────────────────────────────────────── */ + +.ff-panel-left { + background: #252525; + border-right: 1px solid #444; + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: hidden; +} + +.ff-panel-right { + background: #252525; + border-left: 1px solid #444; + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: auto; +} + +/* Shared panel header bar */ +.ff-panel-header { + display: flex; + align-items: center; + padding: 8px 14px; + border-bottom: 1px solid #444; + background: #1e1e1e; + flex-shrink: 0; + cursor: pointer; + transition: background 0.15s; + gap: 8px; +} + +.ff-panel-header:hover { + background: #282828; +} + +.ff-panel-title { + font-size: 12px; + font-weight: 600; + color: #999; + transition: color 0.15s; +} + +.ff-panel-header:hover .ff-panel-title { + color: #ccc; +} + +/* Collapsed side panels */ +.ff-panel-collapsed { + width: 20px; + background: #252525; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-shrink: 0; + cursor: pointer; +} + +.ff-panel-collapsed-left { + border-right: 1px solid #444; +} + +.ff-panel-collapsed-right { + border-left: 1px solid #444; +} + +.ff-panel-collapsed-text { + color: #999; + font-size: 12px; + font-weight: 600; + writing-mode: vertical-rl; + transform: rotate(180deg); +} + +/* ── Toolbox (NodePalette) ────────────────────────────────────────── */ + +.ff-toolbox-search-wrapper { + padding: 8px 14px; + flex-shrink: 0; +} + +.ff-toolbox-search { + width: 100%; + padding: 7px 10px 7px 30px; + font-size: 12px; + background: #171717; + border: 1px solid #404040; + border-radius: 4px; + color: #e0e0e0; + outline: none; + box-sizing: border-box; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 8px center; +} + +.ff-toolbox-filters { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 14px 8px; + flex-wrap: wrap; + flex-shrink: 0; +} + +.ff-toolbox-chip { + padding: 3px 10px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + background: #333; + color: #999; +} + +.ff-toolbox-chip-active { + background: #3b82f6; + color: #fff; +} + +.ff-toolbox-categories { + flex: 1; + overflow: auto; + padding: 0 14px 14px; +} + +.ff-toolbox-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + cursor: pointer; +} + +.ff-toolbox-section-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.ff-toolbox-section-name { + font-size: 12px; + font-weight: 600; + color: #e0e0e0; + flex: 1; +} + +.ff-toolbox-section-count { + font-size: 10px; + color: #666; +} + +.ff-toolbox-section-arrow { + font-size: 10px; + color: #666; +} + +.ff-toolbox-node-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background: #333; + border-radius: 3px; + margin-bottom: 2px; + cursor: grab; +} + +.ff-toolbox-node-icon { + width: 16px; + height: 16px; + border-radius: 3px; + opacity: 0.8; + flex-shrink: 0; +} + +.ff-toolbox-node-name { + font-size: 12px; + font-weight: 600; + color: #e0e0e0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ff-toolbox-node-desc { + font-size: 10px; + color: #999; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ff-toolbox-footer { + height: 32px; + padding: 0 14px; + border-top: 1px solid #444; + flex-shrink: 0; + font-size: 10px; + color: #666; + display: flex; + align-items: center; +} + +/* ── Inspector ────────────────────────────────────────────────────── */ + + +.ff-inspector-badge { + padding: 2px 8px; + border-radius: 10px; + color: #fff; + font-size: 10px; + font-weight: 600; +} + +.ff-inspector-body { + padding: 12px 14px; + flex: 1; +} + +.ff-inspector-section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: #999; + margin-bottom: 10px; + margin-top: 8px; + padding-top: 10px; + border-top: 1px solid #333; +} + +.ff-inspector-section-title:first-child { + border-top: none; + margin-top: 0; + padding-top: 0; +} + +.ff-inspector-section { + border-top: 1px solid #333; + padding-top: 4px; + margin-top: 4px; +} + +.ff-inspector-section:first-child { + border-top: none; + padding-top: 0; + margin-top: 0; +} + +.ff-inspector-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + cursor: pointer; +} + +/* Breadcrumb navigation */ +.ff-inspector-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 0 4px; + font-size: 12px; +} + +.ff-inspector-breadcrumb-link { + color: #3b82f6; + cursor: pointer; +} + +.ff-inspector-breadcrumb-link:hover { + text-decoration: underline; +} + +.ff-inspector-breadcrumb-sep { + color: #666; +} + +.ff-inspector-breadcrumb-current { + color: #e0e0e0; + font-weight: 600; +} + +/* Select dropdown */ +.ff-inspector-select { + padding: 3px 8px; + font-size: 11px; + background: #171717; + border: 1px solid #404040; + border-radius: 4px; + color: #e0e0e0; + outline: none; +} + +/* Type picker popup */ +.ff-type-picker { + position: relative; +} + +.ff-type-picker-trigger { + user-select: none; +} + +.ff-type-picker-popup { + position: absolute; + top: calc(100% + 4px); + right: 0; + background: #252525; + border: 1px solid #444; + border-radius: 6px; + z-index: 100; + width: 200px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; +} + +.ff-type-picker-search { + padding: 7px 10px; + font-size: 11px; + background: #171717; + border: none; + border-bottom: 1px solid #444; + border-radius: 6px 6px 0 0; + color: #e0e0e0; + outline: none; +} + +.ff-type-picker-list { + max-height: 240px; + overflow-y: auto; + padding: 4px; +} + +.ff-type-picker-category { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px 3px; + font-size: 10px; + font-weight: 600; + color: #666; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.ff-type-picker-option { + display: block; + padding: 4px 10px 4px 20px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ff-type-picker-option:hover { + background: #333; +} + +.ff-type-picker-option-active { + font-weight: 700; +} + +.ff-type-picker-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + display: inline-block; +} + +.ff-type-picker-empty { + padding: 12px; + text-align: center; + color: #666; + font-size: 11px; +} + +/* Toggle switch */ +.ff-toggle { + width: 32px; + height: 18px; + border-radius: 9px; + border: 1px solid #444; + background: #333; + padding: 2px; + cursor: pointer; + position: relative; + transition: background 0.2s, border-color 0.2s; + flex-shrink: 0; +} + +.ff-toggle-on { + background: #22c55e; + border-color: #22c55e; +} + +.ff-toggle-thumb { + display: block; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + transition: transform 0.2s; +} + +.ff-toggle-on .ff-toggle-thumb { + transform: translateX(14px); +} + +.ff-inspector-live-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 10px; +} + +.ff-inspector-live-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #22c55e; +} + +.ff-inspector-live-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: #22c55e; +} + +.ff-inspector-live-box { + background: rgba(34,197,94,0.04); + border: 1px solid rgba(34,197,94,0.15); + border-radius: 4px; + padding: 8px 10px; + margin-bottom: 16px; +} + +.ff-inspector-live-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.ff-inspector-live-row:last-child { + margin-bottom: 0; +} + +.ff-inspector-live-name { + font-size: 12px; + color: #a3a3a3; +} + +.ff-inspector-field { + margin-bottom: 10px; +} + +.ff-inspector-kv { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; +} + +.ff-inspector-kv-label { + font-size: 11px; + color: #666; + flex-shrink: 0; + min-width: 72px; +} + +.ff-inspector-kv-value { + font-size: 12px; + color: #e0e0e0; + text-align: right; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ff-inspector-field-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: #666; + margin-bottom: 4px; +} + +.ff-inspector-input { + width: 100%; + padding: 6px 10px; + font-size: 12px; + background: #171717; + border: 1px solid #404040; + border-radius: 4px; + color: #e0e0e0; + outline: none; + box-sizing: border-box; +} + +.ff-inspector-type-badge { + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.ff-inspector-id { + font-size: 11px; + color: #666; + font-family: "JetBrains Mono", "Fira Code", monospace; +} + +.ff-inspector-port-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.ff-inspector-port-name { + font-size: 12px; + color: #e0e0e0; +} + +.ff-inspector-connection-chip { + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; +} + +.ff-inspector-not-connected { + font-size: 11px; + color: #666; + font-style: italic; +} + +.ff-inspector-notes { + width: 100%; + height: 60px; + padding: 8px 10px; + font-size: 12px; + background: #171717; + border: 1px solid #404040; + border-radius: 4px; + color: #e0e0e0; + resize: none; + outline: none; + box-sizing: border-box; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.ff-inspector-footer { + height: 32px; + padding: 0 14px; + border-top: 1px solid #444; + display: flex; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; +} + +.ff-inspector-footer-text { + font-size: 11px; + color: #666; +} + +.ff-inspector-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #666; + font-size: 13px; +} + +/* ── Watch panel ───────────────────────────────────────────────────── */ + +.ff-watch-panel { + background: #252525; + border-top: 1px solid #444; + display: flex; + flex-direction: column; + flex-shrink: 0; + margin: 0 -4px; +} + +.ff-watch-controls { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 6px 14px; + border-bottom: 1px solid #444; + flex-shrink: 0; +} + +.ff-watch-badge { + padding: 1px 8px; + border-radius: 8px; + background: rgba(59, 130, 246, 0.12); + color: #3b82f6; + font-size: 10px; + font-weight: 600; +} + + +.ff-watch-content { + overflow-y: auto; + flex: 1; +} + +.ff-watch-add { + display: flex; + gap: 8px; +} + +.ff-watch-input { + width: 200px; + padding: 4px 8px; + font-size: 11px; + font-family: "JetBrains Mono", "Fira Code", monospace; + background: #171717; + color: #e0e0e0; + border: 1px solid #404040; + border-radius: 4px; + outline: none; +} + +.ff-watch-input:focus { + border-color: #3b82f6; +} + +.ff-watch-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + table-layout: fixed; +} + +.ff-watch-table th { + text-align: left; + padding: 6px 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: #666; + border-bottom: 1px solid #444; +} + +.ff-watch-table th:nth-child(1) { width: 40%; } +.ff-watch-table th:nth-child(2) { width: 18%; } +.ff-watch-table th:nth-child(3) { width: 12%; } +.ff-watch-table th:nth-child(4) { width: 22%; } +.ff-watch-table th:nth-child(5) { width: 8%; } + +.ff-watch-table td { + padding: 5px 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ff-watch-table tbody tr:nth-child(even) { + background: #2a2a2a; +} + +.ff-watch-path { + font-family: "JetBrains Mono", "Fira Code", monospace; + color: #a3a3a3; + font-size: 11px; +} + +.ff-watch-value { + font-family: "JetBrains Mono", "Fira Code", monospace; + font-weight: 600; + font-size: 12px; +} + +.ff-watch-value-bool { color: #84cc16; } +.ff-watch-value-int { color: #60a5fa; } +.ff-watch-value-time { color: #2dd4bf; } +.ff-watch-value-real { color: #f97316; } +.ff-watch-value-default { color: #666; } + +.ff-watch-type { + color: #666; + font-size: 11px; +} + +.ff-watch-time { + color: #666; + font-size: 11px; + font-family: "JetBrains Mono", "Fira Code", monospace; +} + +.ff-watch-empty { + color: #475569; + text-align: center; + padding: 12px !important; +} + +.ff-watch-remove { + color: #666; + cursor: pointer; + font-size: 14px; + background: none; + border: none; + padding: 0; +} + +.ff-watch-remove:hover { + color: #ef4444; +} + +/* Collapsed watch bar */ +.ff-watch-collapsed { + height: 32px; + background: #1e1e1e; + border-top: 1px solid #444; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 14px; + flex-shrink: 0; + cursor: pointer; + transition: background 0.15s; + margin: 0 -4px; +} + +.ff-watch-collapsed:hover { + background: #282828; +} + +.ff-watch-collapsed-title { + font-size: 12px; + font-weight: 600; + color: #999; +} + +.ff-watch-collapsed:hover .ff-watch-collapsed-title { + color: #ccc; +} + +.ff-watch-collapsed-count { + font-size: 10px; + color: #666; +} + +/* ── Status bar ───────────────────────────────────────────────────── */ + +.ff-status-bar { + height: 24px; + background: #1e1e1e; + border-top: 1px solid #444; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 12px; + font-size: 11px; + flex-shrink: 0; +} + +.ff-status-bar-left, +.ff-status-bar-right { + display: flex; + align-items: center; + gap: 16px; +} + +.ff-status-bar-item { + display: flex; + align-items: center; + gap: 6px; + color: #999; +} + +.ff-status-bar-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.ff-status-bar-mono { + font-family: "JetBrains Mono", "Fira Code", monospace; + color: #999; +} + +/* ── Resize dividers ──────────────────────────────────────────────── */ + +.ff-resize-divider { + flex-shrink: 0; + background: transparent; + transition: background 0.15s; +} + +.ff-resize-divider:hover, +.ff-resize-divider:active { + background: rgba(59, 130, 246, 0.4); +} + +.ff-resize-vertical { + width: 4px; + cursor: col-resize; +} + +.ff-resize-horizontal { + height: 4px; + cursor: row-resize; + margin: 0 -4px; +} + +/* ── React Flow overrides ──────────────────────────────────────────── */ + +.react-flow__background { + background: #1a1a1a !important; +} + +.react-flow__controls button { + background: #2d2d2d; + color: #94a3b8; + border: 1px solid #444; +} + +.react-flow__controls button:hover { + background: #3a3a3a; +} + +.react-flow__minimap { + background: rgba(30,30,30,0.9) !important; + border: 1px solid #444; + border-radius: 4px; +} + +.react-flow__edge-path { + stroke: #8b95a3; + stroke-width: 2; +} + +.react-flow__handle { + width: 8px; + height: 8px; + background: #64748b; + border: 1.5px solid #1a1a1a; +} + +.react-flow__handle-left { left: -5px; } +.react-flow__handle-right { right: -5px; } + +.react-flow__node { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; +} + +.react-flow__node-flowGroup { + z-index: -1 !important; + pointer-events: none !important; +} + + +.react-flow__node.selected .ff-node { + border-color: #3b82f6 !important; + box-shadow: 0 0 0 1px #3b82f6, 0 4px 15px rgba(0,0,0,0.5) !important; } diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 135aa82..b28c321 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,52 +1,9 @@ // Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) // SPDX-License-Identifier: AGPL-3.0-or-later -import { useCallback } from "react"; -import { - ReactFlow, - Background, - Controls, - MiniMap, - addEdge, - useNodesState, - useEdgesState, - type OnConnect, -} from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; +import { EditorPageDemo } from "./features/editor/EditorPageDemo"; import "./App.css"; -const initialNodes = [ - { - id: "1", - type: "input", - data: { label: "Start" }, - position: { x: 250, y: 0 }, - }, -]; - export default function App() { - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); - - const onConnect: OnConnect = useCallback( - (params) => setEdges((eds) => addEdge(params, eds)), - [setEdges], - ); - - return ( -
- - - - - -
- ); -} \ No newline at end of file + return ; +} diff --git a/src/frontend/src/api/types.ts b/src/frontend/src/api/types.ts index 4a73666..ff3ab54 100644 --- a/src/frontend/src/api/types.ts +++ b/src/frontend/src/api/types.ts @@ -117,3 +117,19 @@ export interface PlcVariableValue { dataType: string; timestamp: string; } + +// Online monitoring + +export type NodeExecutionState = "idle" | "active" | "error"; + +export interface NodeOnlineData { + nodeId: string; + executionState: NodeExecutionState; + variables: PlcVariableValue[]; +} + +export type ConnectionStatus = + | "disconnected" + | "connecting" + | "connected" + | "error"; diff --git a/src/frontend/src/features/editor/EditorConcept.tsx b/src/frontend/src/features/editor/EditorConcept.tsx new file mode 100644 index 0000000..d213925 --- /dev/null +++ b/src/frontend/src/features/editor/EditorConcept.tsx @@ -0,0 +1,1064 @@ +// Copyright (c) 2026 Qubernetic (Biró, Csaba Attila) +// SPDX-License-Identifier: AGPL-3.0-or-later + +// Static UI concept — Edit mode + Online Monitor mode with mock live data. +// Toggle between modes via toolbar. Layout from Visily mockups, colors from Unity VS refs. + +import React, { useState } from "react"; + +// ── Design Tokens (Unity VS reference) ────────────────────────────────────── + +const T = { + canvasBg: "#1a1a1a", + gridLine: "#222", + panelBg: "#252525", + toolbarBg: "#333333", + nodeBg: "#262626", + nodeBorder: "#333", + panelBorder: "#444", + inputBg: "#171717", + inputBorder: "#404040", + textPrimary: "#e0e0e0", + textSecondary: "#999", + textLabel: "#a3a3a3", + textMuted: "#666", + accentBlue: "#3b82f6", + accentGreen: "#22c55e", + accentRed: "#ef4444", + catIO: "#4A9E9E", + catTimer: "#5BA0D5", + catCounter: "#6A8EBF", + catLogic: "#6B9E5B", + catMath: "#C89B5B", + catCompare: "#8B6BBF", + portBool: "#84cc16", + portInt: "#60a5fa", + portTime: "#2dd4bf", + portReal: "#f97316", + fontUI: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", + fontMono: "'JetBrains Mono', 'Fira Code', monospace", + nodeRadius: 4, + nodeShadow: "0 4px 15px rgba(0,0,0,0.5)", + selectedGlow: "0 0 0 1px #3b82f6, 0 4px 15px rgba(0,0,0,0.5)", + activeGlow: "0 0 15px rgba(34,197,94,0.35), 0 4px 15px rgba(0,0,0,0.5)", + activeSelectedGlow: "0 0 0 1px #3b82f6, 0 0 15px rgba(34,197,94,0.35), 0 4px 15px rgba(0,0,0,0.5)", +} as const; + +const css = { + flexRow: { display: "flex", alignItems: "center" } as React.CSSProperties, + flexCol: { display: "flex", flexDirection: "column" } as React.CSSProperties, + gap: (n: number) => ({ gap: n }) as React.CSSProperties, + ellipsis: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } as React.CSSProperties, +}; + +function portColor(type: string) { + switch (type) { + case "BOOL": return T.portBool; + case "INT": return T.portInt; + case "TIME": return T.portTime; + case "REAL": return T.portReal; + default: return T.textMuted; + } +} + +// ── Value Pill (live data badge) ──────────────────────────────────────────── + +function ValuePill({ value, type }: { value: string; type: string }) { + const isBoolTrue = type === "BOOL" && value === "TRUE"; + const isBoolFalse = type === "BOOL" && value === "FALSE"; + const color = isBoolFalse ? T.textMuted : portColor(type); + const bg = isBoolFalse + ? "rgba(100,100,100,0.1)" + : `${portColor(type)}1F`; // ~12% opacity + + return ( + + {value} + + ); +} + +// ── Types ──────────────────────────────────────────────────────────────────── + +type EditorMode = "edit" | "online"; + +// ── Toolbar ───────────────────────────────────────────────────────────────── + +function Toolbar({ mode, onToggle }: { mode: EditorMode; onToggle: () => void }) { + return ( +
+ {/* Left */} +
+
+
FF
+ FlowForge +
+ Projects > + + Motor Control v2.1 + +
+ + {/* Center — Edit / Online toggle */} +
+
+ + +
+ + {/* Connection status — only in Online mode */} + {mode === "online" && ( +
+
+ PLC-Demo (5.39.123.1.1.1) + Connected +
+ )} +
+ + {/* Right */} +
+ + {mode === "edit" && ( + + )} + {mode === "online" && ( + + )} + {mode === "edit" && ( + Saved 2 min ago + )} +
CB
+
+
+ ); +} + +// ── Collapsed Node Toolbox (Online mode) ──────────────────────────────────── + +function CollapsedToolbox() { + return ( +
+ + NODES ▸ + +
+ ); +} + +// ── Node Toolbox (Edit mode — expanded panel) ─────────────────────────────── + +const toolboxCategories = [ + { + name: "I/O", color: T.catIO, expanded: true, + nodes: [ + { name: "Digital Input", desc: "Read PLC input", fav: false }, + { name: "Digital Output", desc: "Write PLC output", fav: false }, + { name: "Analog Input", desc: "Read analog value", fav: true }, + { name: "Analog Output", desc: "Write analog value", fav: false }, + ], + }, + { + name: "Timers", color: T.catTimer, expanded: true, + nodes: [ + { name: "TON", desc: "On-delay timer", fav: false }, + { name: "TOF", desc: "Off-delay timer", fav: false }, + { name: "TP", desc: "Pulse timer", fav: false }, + ], + }, + { name: "Logic", color: T.catLogic, expanded: false, nodes: [], count: 4 }, + { name: "Math", color: T.catMath, expanded: false, nodes: [], count: 4 }, + { name: "Counters", color: T.catCounter, expanded: false, nodes: [], count: 3 }, +]; + +const filterChips = ["All", "\u2605 Favorites", "I/O", "Logic", "Timers", "Counters", "Math", "Compare"]; + +function NodeToolbox() { + return ( + + ); +} + +// ── Canvas Node (Online with live values) ─────────────────────────────────── + +interface LivePortDef { + name: string; + type: "BOOL" | "INT" | "TIME"; + side: "left" | "right"; + value?: string; +} + +function CanvasNode({ + category, + categoryColor, + name, + ports, + selected, + active, + style, +}: { + category: string; + categoryColor: string; + name: string; + ports: LivePortDef[]; + selected?: boolean; + active?: boolean; + style?: React.CSSProperties; +}) { + const leftPorts = ports.filter((p) => p.side === "left"); + const rightPorts = ports.filter((p) => p.side === "right"); + const rows = Math.max(leftPorts.length, rightPorts.length); + + let borderColor = T.nodeBorder; + let shadow = T.nodeShadow; + if (selected && active) { + borderColor = T.accentBlue; + shadow = T.activeSelectedGlow; + } else if (selected) { + borderColor = T.accentBlue; + shadow = T.selectedGlow; + } else if (active) { + borderColor = T.accentGreen; + shadow = T.activeGlow; + } + + return ( +
+ {/* Header */} +
+ {category} + {name} +
+ + {/* Ports with live values */} +
+ {Array.from({ length: rows }).map((_, i) => { + const lp = leftPorts[i]; + const rp = rightPorts[i]; + return ( +
+ {/* Left port */} +
+ {lp ? ( + <> +
+ {lp.name} + {lp.value && } + + ) : } +
+ {/* Right port */} +
+ {rp && ( + <> + {rp.value && } + {rp.name} +
+ + )} +
+
+ ); + })} +
+
+ ); +} + +// ── SVG Wires with value badges ───────────────────────────────────────────── + +function Wire({ + x1, y1, x2, y2, color, active, label, +}: { + x1: number; y1: number; x2: number; y2: number; + color: string; active?: boolean; label?: string; +}) { + const cx1 = x1 + (x2 - x1) * 0.4; + const cx2 = x2 - (x2 - x1) * 0.4; + const mx = (x1 + x2) / 2; + const my = (y1 + y2) / 2; + return ( + + + {label && ( + + + + {label} + + + )} + + ); +} + +// ── Flow Canvas ───────────────────────────────────────────────────────────── + +function FlowCanvas({ mode }: { mode: EditorMode }) { + const isOnline = mode === "online"; + return ( +
+ {/* Grid */} +
+ + {/* Wires */} + + + + + + + + + {/* Node 1: Start Button */} + + + {/* Node 2: Delay T#2S */} + + + {/* Node 3: Part Counter — SELECTED */} + + + {/* Node 4: >= */} + + + {/* Node 5: Motor ON */} + + + {/* Node 6: Batch Done — IDLE in online, normal in edit */} + + + {/* MiniMap */} +
+
+
+
+
+
+
+
+
+
+ + {/* Zoom controls */} +
+ {["+", "85%", "\u2212", "\u2922"].map((label, i) => ( + + ))} +
+
+ ); +} + +// ── Node Inspector (Online — with Live Values) ───────────────────────────── + +function InspectorField({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
+ {label} +
+ {children} +
+ ); +} + +function ConnectionChip({ label, color }: { label: string; color: string }) { + return ( + + {label} + + ); +} + +function LiveValueRow({ name, value, type }: { name: string; value: string; type: string }) { + return ( +
+ {name} + +
+ ); +} + +function NodeInspectorPanel({ mode }: { mode: EditorMode }) { + return ( +