@aaqu/fromcubes-portal-react 0.1.0-alpha.22 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ [
2
+ {
3
+ "id": "fc-demo-flow",
4
+ "type": "tab",
5
+ "label": "Sensor Portal",
6
+ "disabled": false
7
+ },
8
+ {
9
+ "id": "ba6b163996fca9c1",
10
+ "type": "comment",
11
+ "z": "fc-demo-flow",
12
+ "name": "Sensor Portal Demo",
13
+ "info": "Live sensor gauge driven by msg.payload over WebSocket.\n\n## What it demonstrates\nA `portal-react` node that renders a circular gauge whose value reflects the last `msg.payload`\nbroadcast on the input wire.\n\n## How to run\n1. Import **Shared Components** first (provides Page, Header, Stat).\n2. Import this flow, deploy.\n3. Open `http://<host>:1880/fromcubes/sensors` in a browser.\n4. Click the **inject** node to push a payload.\n\n## Expected result\nThe gauge animates to the injected value. Multiple browser tabs stay in sync (broadcast).",
14
+ "x": 160,
15
+ "y": 60,
16
+ "wires": []
17
+ },
18
+ {
19
+ "id": "fc-inject",
20
+ "type": "inject",
21
+ "z": "fc-demo-flow",
22
+ "name": "Sensor tick",
23
+ "props": [
24
+ {
25
+ "p": "payload"
26
+ }
27
+ ],
28
+ "repeat": "2",
29
+ "payload": "{}",
30
+ "payloadType": "json",
31
+ "x": 160,
32
+ "y": 160,
33
+ "wires": [
34
+ [
35
+ "fc-rand"
36
+ ]
37
+ ]
38
+ },
39
+ {
40
+ "id": "fc-rand",
41
+ "type": "function",
42
+ "z": "fc-demo-flow",
43
+ "name": "Random sensors",
44
+ "func": "msg.payload = {\n temp: +(18 + Math.random() * 12).toFixed(1),\n humidity: Math.round(35 + Math.random() * 40),\n pressure: Math.round(995 + Math.random() * 35),\n ts: Date.now()\n};\nreturn msg;",
45
+ "outputs": 1,
46
+ "x": 360,
47
+ "y": 160,
48
+ "wires": [
49
+ [
50
+ "fc-portal"
51
+ ]
52
+ ]
53
+ },
54
+ {
55
+ "id": "fc-gauge-comp",
56
+ "type": "fc-portal-component",
57
+ "z": "fc-demo-flow",
58
+ "name": "Gauge",
59
+ "compName": "Gauge",
60
+ "compCode": "function Gauge({ value = 0, min = 0, max = 100, label = '', unit = '' }) {\n const pct = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));\n const color = pct > 80 ? 'text-red-400' : pct > 50 ? 'text-amber-400' : 'text-green-400';\n const bg = pct > 80 ? 'from-red-500' : pct > 50 ? 'from-amber-500' : 'from-green-500';\n return (\n <div className=\"flex flex-col items-center p-4\">\n <div className=\"text-xs text-zinc-500 uppercase tracking-wider mb-2\">{label}</div>\n <div className=\"relative w-28 h-28 rounded-full bg-zinc-900 flex items-center justify-center\">\n <svg className=\"absolute inset-0 w-full h-full -rotate-90\" viewBox=\"0 0 100 100\">\n <circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" strokeWidth=\"6\" className=\"stroke-zinc-800\" />\n <circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" strokeWidth=\"6\"\n className={`stroke-current ${color}`}\n strokeDasharray={`${pct * 2.64} 264`}\n strokeLinecap=\"round\" />\n </svg>\n <div className={`text-2xl font-bold ${color}`}>\n {Math.round(value)}<span className=\"text-sm ml-0.5\">{unit}</span>\n </div>\n </div>\n </div>\n );\n}",
61
+ "compInputs": "value,min,max,label,unit",
62
+ "compOutputs": "",
63
+ "x": 160,
64
+ "y": 280,
65
+ "wires": []
66
+ },
67
+ {
68
+ "id": "fc-portal",
69
+ "type": "portal-react",
70
+ "z": "fc-demo-flow",
71
+ "name": "Sensor Portal",
72
+ "subPath": "sensors",
73
+ "pageTitle": "Sensors",
74
+ "customHead": "",
75
+ "inputSchema": "[{\"name\":\"temp\",\"type\":\"number\"},{\"name\":\"humidity\",\"type\":\"number\"},{\"name\":\"pressure\",\"type\":\"number\"},{\"name\":\"ts\",\"type\":\"number\"}]",
76
+ "outputSchema": "[{\"name\":\"action\",\"type\":\"string\"}]",
77
+ "componentCode": "function App() {\n const { data, send } = useNodeRed();\n const s = data || { temp: 0, humidity: 0, pressure: 0 };\n\n return (\n <Page>\n <Header title=\"Sensor Portal\" subtitle=\"live sensor data\" />\n <div className=\"flex-1 flex flex-col items-center justify-center p-8\">\n <div className=\"flex gap-10 flex-wrap justify-center\">\n <Gauge value={s.temp} min={-10} max={50} label=\"Temperature\" unit=\"°C\" />\n <Gauge value={s.humidity} min={0} max={100} label=\"Humidity\" unit=\"%\" />\n <Gauge value={s.pressure} min={950} max={1050} label=\"Pressure\" unit=\"hPa\" />\n </div>\n <div className=\"mt-10 flex gap-3 items-center\">\n <Button onClick={() => send({ action: 'refresh' })}>Refresh</Button>\n <ValueBadge label=\"Last\" value={s.ts ? new Date(s.ts).toLocaleTimeString() : null} />\n </div>\n </div>\n </Page>\n );\n}",
78
+ "x": 560,
79
+ "y": 160,
80
+ "wires": [
81
+ [
82
+ "fc-debug"
83
+ ]
84
+ ]
85
+ },
86
+ {
87
+ "id": "fc-debug",
88
+ "type": "debug",
89
+ "z": "fc-demo-flow",
90
+ "name": "Portal output",
91
+ "active": true,
92
+ "tosidebar": true,
93
+ "x": 750,
94
+ "y": 160,
95
+ "wires": []
96
+ }
97
+ ]
@@ -0,0 +1,78 @@
1
+ [
2
+ {
3
+ "id": "fc-shared-flow",
4
+ "type": "tab",
5
+ "label": "Shared Components",
6
+ "disabled": false
7
+ },
8
+ {
9
+ "id": "bda6e8d95820e1fb",
10
+ "type": "comment",
11
+ "z": "fc-shared-flow",
12
+ "name": "Shared Components Library",
13
+ "info": "Reusable UI components used by other portal-react examples.\n\n## What it demonstrates\nA set of `fc-portal-component` nodes (Page, Header, Stat, Button, ValueBadge) that any portal can\nreference. No portal node is included — these components are the building blocks for examples 002+.\n\n## How to run\n1. Import this flow first.\n2. Deploy.\n3. Import any other example (Sensor Portal, Live Chart, …) — those flows reference these components by name.\n\n## Expected result\nFive `fc-portal-component` nodes appear in the workspace under the **fromcubes** category.\nTheir identifiers (`Page`, `Header`, `Stat`, `Button`, `ValueBadge`) become available globally for any\nportal-react node's JSX.",
14
+ "x": 160,
15
+ "y": 60,
16
+ "wires": []
17
+ },
18
+ {
19
+ "id": "fc-shared-comp-header",
20
+ "type": "fc-portal-component",
21
+ "z": "fc-shared-flow",
22
+ "compName": "Header",
23
+ "compCode": "function Header({ title = 'fromcubes', subtitle = '', children }) {\n return (\n <div className=\"flex items-center justify-between px-6 py-4\">\n <div>\n <h1 className=\"text-xl font-semibold text-zinc-100 tracking-tight\">{title}</h1>\n {subtitle && <p className=\"text-xs text-zinc-600 mt-0.5\">{subtitle}</p>}\n </div>\n <div className=\"flex items-center gap-2\">\n {children || (\n <>\n <span className=\"inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse\" />\n <span className=\"text-xs text-zinc-500\">streaming</span>\n </>\n )}\n </div>\n </div>\n );\n}",
24
+ "compInputs": "title,subtitle,children",
25
+ "compOutputs": "",
26
+ "x": 160,
27
+ "y": 160,
28
+ "wires": []
29
+ },
30
+ {
31
+ "id": "fc-shared-comp-stat",
32
+ "type": "fc-portal-component",
33
+ "z": "fc-shared-flow",
34
+ "compName": "Stat",
35
+ "compCode": "function Stat({ label, children }) {\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-zinc-500\">{label}</span>\n {children}\n </div>\n );\n}",
36
+ "compInputs": "label,children",
37
+ "compOutputs": "",
38
+ "x": 360,
39
+ "y": 160,
40
+ "wires": []
41
+ },
42
+ {
43
+ "id": "fc-shared-comp-button",
44
+ "type": "fc-portal-component",
45
+ "z": "fc-shared-flow",
46
+ "compName": "Button",
47
+ "compCode": "function Button({ onClick, children, variant = 'primary', className = '' }) {\n const base = 'px-4 py-2 rounded-lg font-medium transition-colors';\n const variants = {\n primary: 'bg-blue-600 text-white hover:bg-blue-500',\n secondary: 'bg-zinc-700 text-zinc-200 hover:bg-zinc-600',\n danger: 'bg-red-600 text-white hover:bg-red-500'\n };\n return (\n <button\n className={`${base} ${variants[variant] || variants.primary} ${className}`}\n onClick={onClick}\n >{children}</button>\n );\n}",
48
+ "compInputs": "onClick,children,variant,className",
49
+ "compOutputs": "",
50
+ "x": 560,
51
+ "y": 160,
52
+ "wires": []
53
+ },
54
+ {
55
+ "id": "fc-shared-comp-valuebadge",
56
+ "type": "fc-portal-component",
57
+ "z": "fc-shared-flow",
58
+ "compName": "ValueBadge",
59
+ "compCode": "function ValueBadge({ label, value, className = '' }) {\n if (value == null) return null;\n return (\n <span className={`text-zinc-600 text-xs ${className}`}>\n {label && <>{label}: </>}{value}\n </span>\n );\n}",
60
+ "compInputs": "label,value,className",
61
+ "compOutputs": "",
62
+ "x": 760,
63
+ "y": 160,
64
+ "wires": []
65
+ },
66
+ {
67
+ "id": "fc-shared-comp-page",
68
+ "type": "fc-portal-component",
69
+ "z": "fc-shared-flow",
70
+ "compName": "Page",
71
+ "compCode": "function Page({ children, className = '' }) {\n return (\n <div className={`min-h-screen bg-zinc-950 text-zinc-100 flex flex-col ${className}`}>\n {children}\n </div>\n );\n}",
72
+ "compInputs": "children,className",
73
+ "compOutputs": "",
74
+ "x": 160,
75
+ "y": 240,
76
+ "wires": []
77
+ }
78
+ ]
@@ -0,0 +1,122 @@
1
+ [
2
+ {
3
+ "id": "fc-three-flow",
4
+ "type": "tab",
5
+ "label": "Three.js Demo",
6
+ "disabled": false
7
+ },
8
+ {
9
+ "id": "7ad6c130eab72734",
10
+ "type": "comment",
11
+ "z": "fc-three-flow",
12
+ "name": "Three.js Scene Demo",
13
+ "info": "Rotating 3D cube rendered with Three.js + @react-three/fiber.\n\n## What it demonstrates\nMultiple library deps declared together: `three`, `@react-three/fiber`, `@react-three/drei`.\nShows that the React `alias` in esbuild keeps a single React instance across all peer deps\n(no \"Invalid hook call\" errors).\n\n## How to run\n1. Import this flow, deploy.\n2. Open `http://<host>:1880/fromcubes/threejs`.\n\n## Expected result\nA textured cube rotates in a WebGL canvas. Orbit controls work with mouse drag.",
14
+ "x": 160,
15
+ "y": 60,
16
+ "wires": []
17
+ },
18
+ {
19
+ "id": "fc-three-comp-box",
20
+ "type": "fc-portal-component",
21
+ "z": "fc-three-flow",
22
+ "compName": "Box",
23
+ "compCode": "import { useFrame } from '@react-three/fiber';\n\nfunction Box({ position, color = 'orange', onToggle }) {\n const ref = useRef();\n const [hovered, hover] = useState(false);\n const [clicked, click] = useState(false);\n\n useFrame((state, delta) => (ref.current.rotation.x += delta));\n\n return (\n <mesh\n position={position}\n ref={ref}\n scale={clicked ? 1.5 : 1}\n onClick={() => {\n click(!clicked);\n if (onToggle) onToggle(!clicked);\n }}\n onPointerOver={(e) => (e.stopPropagation(), hover(true))}\n onPointerOut={() => hover(false)}\n >\n <boxGeometry args={[1, 1, 1]} />\n <meshStandardMaterial color={hovered ? 'hotpink' : color} />\n </mesh>\n );\n}",
24
+ "compInputs": "position,color,onToggle",
25
+ "compOutputs": "",
26
+ "x": 160,
27
+ "y": 300,
28
+ "wires": []
29
+ },
30
+ {
31
+ "id": "fc-three-comp-statusbar",
32
+ "type": "fc-portal-component",
33
+ "z": "fc-three-flow",
34
+ "compName": "StatusBar",
35
+ "compCode": "function StatusBar({ leftColor, rightColor, clickCount }) {\n return (\n <div className=\"px-6 py-4 bg-zinc-900/50 border-t border-zinc-800/50 flex items-center gap-6 flex-wrap\">\n <Stat label=\"Left box\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{ backgroundColor: leftColor }} />\n </Stat>\n <Stat label=\"Right box\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{ backgroundColor: rightColor }} />\n </Stat>\n <Stat label=\"Clicks\">\n <span className=\"text-sm font-mono text-cyan-400\">{clickCount}</span>\n </Stat>\n </div>\n );\n}",
36
+ "compInputs": "leftColor,rightColor,clickCount",
37
+ "compOutputs": "",
38
+ "x": 360,
39
+ "y": 300,
40
+ "wires": []
41
+ },
42
+ {
43
+ "id": "fc-three-inject",
44
+ "type": "inject",
45
+ "z": "fc-three-flow",
46
+ "name": "Tick",
47
+ "props": [
48
+ {
49
+ "p": "payload"
50
+ }
51
+ ],
52
+ "repeat": "3",
53
+ "payload": "{}",
54
+ "payloadType": "json",
55
+ "x": 160,
56
+ "y": 160,
57
+ "wires": [
58
+ [
59
+ "fc-three-func"
60
+ ]
61
+ ]
62
+ },
63
+ {
64
+ "id": "fc-three-func",
65
+ "type": "function",
66
+ "z": "fc-three-flow",
67
+ "name": "Random colors",
68
+ "func": "const rnd = () => '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6,'0');\nmsg.payload = {\n leftColor: rnd(),\n rightColor: rnd()\n};\nreturn msg;",
69
+ "outputs": 1,
70
+ "x": 360,
71
+ "y": 160,
72
+ "wires": [
73
+ [
74
+ "fc-three-portal"
75
+ ]
76
+ ]
77
+ },
78
+ {
79
+ "id": "fc-three-portal",
80
+ "type": "portal-react",
81
+ "z": "fc-three-flow",
82
+ "name": "3D Portal",
83
+ "subPath": "threejs",
84
+ "pageTitle": "fromcubes 3D",
85
+ "customHead": "",
86
+ "portalAuth": false,
87
+ "showWsStatus": false,
88
+ "libs": [
89
+ {
90
+ "module": "three",
91
+ "var": "THREE"
92
+ },
93
+ {
94
+ "module": "@react-three/fiber",
95
+ "var": "R3F"
96
+ },
97
+ {
98
+ "module": "@react-three/drei",
99
+ "var": "Drei"
100
+ }
101
+ ],
102
+ "componentCode": "import { Canvas } from '@react-three/fiber';\nimport { OrbitControls } from '@react-three/drei';\n\nfunction App() {\n const { data, send } = useNodeRed();\n const colors = data || { leftColor: 'orange', rightColor: 'cyan' };\n const [clickCount, setClickCount] = useState(0);\n\n const handleToggle = useCallback((clicked) => {\n setClickCount(prev => prev + 1);\n send({ action: clicked ? 'clicked' : 'unclicked', clicks: clickCount + 1 }, '3d/interact');\n }, [send, clickCount]);\n\n return (\n <div className=\"h-screen bg-zinc-950 text-zinc-100 flex flex-col\">\n <Header title=\"fromcubes 3D\" subtitle=\"React Three Fiber + live Node-RED data\" />\n <div className=\"flex-1 overflow-hidden\">\n <Canvas style={{ width: '100%', height: '100%' }}>\n <ambientLight intensity={Math.PI / 2} />\n <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />\n <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />\n <Box position={[-1.2, 0, 0]} color={colors.leftColor} onToggle={handleToggle} />\n <Box position={[1.2, 0, 0]} color={colors.rightColor} onToggle={handleToggle} />\n <OrbitControls />\n </Canvas>\n </div>\n <StatusBar leftColor={colors.leftColor} rightColor={colors.rightColor} clickCount={clickCount} />\n </div>\n );\n}",
103
+ "x": 560,
104
+ "y": 160,
105
+ "wires": [
106
+ [
107
+ "fc-three-debug"
108
+ ]
109
+ ]
110
+ },
111
+ {
112
+ "id": "fc-three-debug",
113
+ "type": "debug",
114
+ "z": "fc-three-flow",
115
+ "name": "3D output",
116
+ "active": true,
117
+ "tosidebar": true,
118
+ "x": 750,
119
+ "y": 160,
120
+ "wires": []
121
+ }
122
+ ]
@@ -0,0 +1,96 @@
1
+ [
2
+ {
3
+ "id": "fc-util-flow",
4
+ "type": "tab",
5
+ "label": "Utility: useDebounce",
6
+ "disabled": false
7
+ },
8
+ {
9
+ "id": "5e0a035124844a91",
10
+ "type": "comment",
11
+ "z": "fc-util-flow",
12
+ "name": "Utility Hooks Demo",
13
+ "info": "Custom React hook + helper exposed via `fc-portal-utility` node.\n\n## What it demonstrates\nA single `fc-portal-utility` node declaring `useDebounce()` (custom hook) and `clamp()` (helper).\nThe companion `Bar` component and portal-react JSX consume them — illustrates how utility code is\ninjected at the top level of the bundle (no IIFE wrapper) so any component can call its symbols.\n\n## How to run\n1. Import this flow, deploy.\n2. Open `http://<host>:1880/fromcubes/debounce`.\n3. Click inject repeatedly.\n\n## Expected result\nRapid injects coalesce — the bar only updates 300 ms after the last burst. Values clamp to [0, 100].",
14
+ "x": 160,
15
+ "y": 60,
16
+ "wires": []
17
+ },
18
+ {
19
+ "id": "fc-util-tick",
20
+ "type": "inject",
21
+ "z": "fc-util-flow",
22
+ "name": "Fast tick (10Hz)",
23
+ "props": [
24
+ {
25
+ "p": "payload"
26
+ }
27
+ ],
28
+ "repeat": "0.1",
29
+ "payload": "",
30
+ "payloadType": "date",
31
+ "x": 160,
32
+ "y": 160,
33
+ "wires": [
34
+ [
35
+ "fc-util-noisy"
36
+ ]
37
+ ]
38
+ },
39
+ {
40
+ "id": "fc-util-noisy",
41
+ "type": "function",
42
+ "z": "fc-util-flow",
43
+ "name": "Noisy signal",
44
+ "func": "// Random walk between 0..120 with bursts of noise — exactly the sort of\n// fast-changing input where a debounced display is much more readable\n// than the raw value.\nconst prev = context.get('v') ?? 50;\nconst drift = (Math.random() - 0.5) * 8;\nconst noise = Math.random() < 0.15 ? (Math.random() - 0.5) * 30 : 0;\nconst v = Math.max(0, Math.min(120, prev + drift + noise));\ncontext.set('v', v);\nmsg.payload = { value: +v.toFixed(2), ts: Date.now() };\nreturn msg;",
45
+ "outputs": 1,
46
+ "x": 360,
47
+ "y": 160,
48
+ "wires": [
49
+ [
50
+ "fc-util-portal"
51
+ ]
52
+ ]
53
+ },
54
+ {
55
+ "id": "fc-util-helpers",
56
+ "type": "fc-portal-utility",
57
+ "z": "fc-util-flow",
58
+ "name": "math helpers + useDebounce",
59
+ "utilName": "mathHelpers",
60
+ "utilCode": "// Constants — bundled once, shared across portals that reference them.\nconst CLAMP_MIN = 0;\nconst CLAMP_MAX = 100;\n\n// Pure helper.\nfunction clamp(n, min, max) {\n return Math.max(min, Math.min(max, n));\n}\n\n// Custom React hook — name MUST start with `use` for React's rules of hooks.\n// This node groups a constant + a helper + a hook in one block; portals that\n// reference any of `clamp`, `useDebounce`, `CLAMP_MIN`, `CLAMP_MAX` pull in\n// the whole node.\nfunction useDebounce(value, ms = 300) {\n const [v, setV] = React.useState(value);\n React.useEffect(() => {\n const t = setTimeout(() => setV(value), ms);\n return () => clearTimeout(t);\n }, [value, ms]);\n return v;\n}",
61
+ "x": 160,
62
+ "y": 280,
63
+ "wires": []
64
+ },
65
+ {
66
+ "id": "fc-util-bar",
67
+ "type": "fc-portal-component",
68
+ "z": "fc-util-flow",
69
+ "name": "Bar",
70
+ "compName": "Bar",
71
+ "compCode": "function Bar({ value = 0, label = '' }) {\n // Component uses `clamp` from the utility node — selective inclusion picks\n // up the dependency through the components' code, not just the user JSX.\n const pct = clamp(value, CLAMP_MIN, CLAMP_MAX);\n const color = pct > 80 ? 'bg-red-500' : pct > 50 ? 'bg-amber-500' : 'bg-emerald-500';\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"w-24 text-xs text-zinc-400 uppercase tracking-wider\">{label}</div>\n <div className=\"flex-1 h-3 bg-zinc-800 rounded-full overflow-hidden\">\n <div className={`h-full ${color} transition-all duration-150`} style={{ width: pct + '%' }} />\n </div>\n <div className=\"w-14 text-right font-mono text-sm tabular-nums text-zinc-200\">\n {value.toFixed(1)}\n </div>\n </div>\n );\n}",
72
+ "compInputs": "value,label",
73
+ "compOutputs": "",
74
+ "x": 360,
75
+ "y": 280,
76
+ "wires": []
77
+ },
78
+ {
79
+ "id": "fc-util-portal",
80
+ "type": "portal-react",
81
+ "z": "fc-util-flow",
82
+ "name": "Debounce demo",
83
+ "subPath": "debounce",
84
+ "pageTitle": "useDebounce demo",
85
+ "customHead": "",
86
+ "componentCode": "function App() {\n const { data } = useNodeRed();\n const raw = data?.value ?? 0;\n\n // Live raw signal vs debounced views at three different windows.\n // `useDebounce` is declared in the fc-portal-utility node — no import.\n const slow100 = useDebounce(raw, 100);\n const slow400 = useDebounce(raw, 400);\n const slow1500 = useDebounce(raw, 1500);\n\n return (\n <div className=\"min-h-screen bg-zinc-950 text-zinc-100 p-8 font-sans\">\n <h1 className=\"text-2xl font-light text-amber-400 mb-2\">useDebounce</h1>\n <p className=\"text-sm text-zinc-500 mb-8\">\n Custom hook from <code className=\"text-amber-300\">fc-portal-utility</code>. Same noisy 10 Hz input fed through different debounce windows.\n </p>\n <div className=\"max-w-xl space-y-4\">\n <Bar value={raw} label=\"raw\" />\n <Bar value={slow100} label=\"100 ms\" />\n <Bar value={slow400} label=\"400 ms\" />\n <Bar value={slow1500} label=\"1500 ms\" />\n </div>\n </div>\n );\n}",
87
+ "libs": [],
88
+ "portalAuth": false,
89
+ "showWsStatus": false,
90
+ "x": 580,
91
+ "y": 160,
92
+ "wires": [
93
+ []
94
+ ]
95
+ }
96
+ ]
@@ -0,0 +1,114 @@
1
+ [
2
+ {
3
+ "id": "fc-webgpu-flow",
4
+ "type": "tab",
5
+ "label": "WebGPU TSL Demo",
6
+ "disabled": false
7
+ },
8
+ {
9
+ "id": "8098c4fd7a54a6b0",
10
+ "type": "comment",
11
+ "z": "fc-webgpu-flow",
12
+ "name": "WebGPU TSL Shader Demo",
13
+ "info": "WebGPU-backed Three.js scene using the TSL (Three Shading Language) node graph API.\n\n## What it demonstrates\nModern `three` WebGPU renderer + TSL. Demonstrates that the bundler tolerates importing the\n`three/webgpu` subpath and that TSL helpers (`time`, `Fn`, …) work in user JSX.\n\n## How to run\n1. Import this flow, deploy (auto-installs `three`).\n2. Open `http://<host>:1880/fromcubes/webgpu` in a WebGPU-capable browser (Chrome ≥113, Safari ≥18).\n\n## Expected result\nAnimated shader effect. Browsers without WebGPU fall back to an error message in the canvas.",
14
+ "x": 160,
15
+ "y": 60,
16
+ "wires": []
17
+ },
18
+ {
19
+ "id": "fc-webgpu-comp-canvas",
20
+ "type": "fc-portal-component",
21
+ "z": "fc-webgpu-flow",
22
+ "compName": "TSLCanvas",
23
+ "compCode": "import { AmbientLight, Color, Mesh, MeshStandardNodeMaterial, PerspectiveCamera, PointLight, Scene, Timer, TorusKnotGeometry, WebGPURenderer } from 'three/webgpu';\nimport { uniform, sin, cos, time, uv, vec3, mix, float, positionLocal, normalLocal } from 'three/tsl';\n\nfunction TSLCanvas({ params }) {\n const mountRef = useRef(null);\n const stateRef = useRef(null);\n\n useEffect(() => {\n const el = mountRef.current;\n const w = el.clientWidth;\n const h = el.clientHeight;\n\n const scene = new Scene();\n const camera = new PerspectiveCamera(50, w / h, 0.1, 100);\n camera.position.z = 4;\n\n const renderer = new WebGPURenderer({ antialias: true });\n renderer.setSize(w, h);\n renderer.setPixelRatio(window.devicePixelRatio);\n el.appendChild(renderer.domElement);\n\n scene.add(new AmbientLight(0xffffff, 0.5));\n const point = new PointLight(0xffffff, 1.5, 100);\n point.position.set(5, 5, 5);\n scene.add(point);\n\n const colorAU = uniform(new Color(0x22d3ee));\n const colorBU = uniform(new Color(0xa78bfa));\n\n const t = time;\n const uvNode = uv();\n const wave = sin(uvNode.x.mul(6.28).add(t)).mul(0.5).add(0.5);\n const pulse = cos(t.mul(0.7)).mul(0.5).add(0.5);\n const blend = mix(vec3(colorAU), vec3(colorBU), wave.mul(pulse));\n\n const mat = new MeshStandardNodeMaterial();\n mat.colorNode = blend;\n mat.roughnessNode = float(0.3);\n mat.metalnessNode = float(0.7);\n\n const displaceAmount = sin(positionLocal.y.mul(4.0).add(t)).mul(0.08);\n mat.positionNode = positionLocal.add(normalLocal.mul(displaceAmount));\n\n const geo = new TorusKnotGeometry(1, 0.35, 128, 32);\n const mesh = new Mesh(geo, mat);\n scene.add(mesh);\n\n const targetA = new Color(0x22d3ee);\n const targetB = new Color(0xa78bfa);\n const st = { mesh, scene, camera, renderer, colorAU, colorBU, targetA, targetB };\n stateRef.current = st;\n\n let raf;\n const timer = new Timer();\n function animate() {\n raf = requestAnimationFrame(animate);\n timer.update();\n const dt = timer.getDelta();\n mesh.rotation.x += 0.3 * dt;\n mesh.rotation.y += 0.6 * dt;\n st.colorAU.value.lerp(st.targetA, Math.min(1, 2.5 * dt));\n st.colorBU.value.lerp(st.targetB, Math.min(1, 2.5 * dt));\n renderer.render(scene, camera);\n }\n\n renderer.init().then(() => animate());\n\n const onResize = () => {\n const w2 = el.clientWidth;\n const h2 = el.clientHeight;\n camera.aspect = w2 / h2;\n camera.updateProjectionMatrix();\n renderer.setSize(w2, h2);\n };\n window.addEventListener('resize', onResize);\n\n return () => {\n cancelAnimationFrame(raf);\n window.removeEventListener('resize', onResize);\n renderer.dispose();\n geo.dispose();\n mat.dispose();\n el.removeChild(renderer.domElement);\n };\n }, []);\n\n useEffect(() => {\n const s = stateRef.current;\n if (!s) return;\n s.targetA.set(params.colorA);\n s.targetB.set(params.colorB);\n }, [params.colorA, params.colorB]);\n\n return <div ref={mountRef} className=\"flex-1\" />;\n}",
24
+ "compInputs": "params",
25
+ "compOutputs": "",
26
+ "x": 160,
27
+ "y": 300,
28
+ "wires": []
29
+ },
30
+ {
31
+ "id": "fc-webgpu-comp-statusbar",
32
+ "type": "fc-portal-component",
33
+ "z": "fc-webgpu-flow",
34
+ "compName": "TSLStatusBar",
35
+ "compCode": "function TSLStatusBar({ params }) {\n return (\n <div className=\"px-6 py-4 bg-zinc-900/50 border-t border-zinc-800/50 flex items-center gap-6 flex-wrap\">\n <Stat label=\"Color A\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{backgroundColor: params.colorA}} />\n </Stat>\n <Stat label=\"Color B\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{backgroundColor: params.colorB}} />\n </Stat>\n <Stat label=\"Backend\">\n <span className=\"text-xs font-mono px-2 py-0.5 rounded bg-violet-500/20 text-violet-300\">WebGPU / TSL</span>\n </Stat>\n </div>\n );\n}",
36
+ "compInputs": "params",
37
+ "compOutputs": "",
38
+ "x": 360,
39
+ "y": 300,
40
+ "wires": []
41
+ },
42
+ {
43
+ "id": "fc-webgpu-inject",
44
+ "type": "inject",
45
+ "z": "fc-webgpu-flow",
46
+ "name": "Tick",
47
+ "props": [
48
+ {
49
+ "p": "payload"
50
+ }
51
+ ],
52
+ "repeat": "3",
53
+ "payload": "{}",
54
+ "payloadType": "json",
55
+ "x": 160,
56
+ "y": 160,
57
+ "wires": [
58
+ [
59
+ "fc-webgpu-func"
60
+ ]
61
+ ]
62
+ },
63
+ {
64
+ "id": "fc-webgpu-func",
65
+ "type": "function",
66
+ "z": "fc-webgpu-flow",
67
+ "name": "Random TSL params",
68
+ "func": "const pairs = [\n ['#22d3ee','#a78bfa'],\n ['#a78bfa','#f97316'],\n ['#34d399','#e879f9'],\n ['#f97316','#60a5fa'],\n ['#f87171','#34d399'],\n ['#e879f9','#22d3ee'],\n ['#fbbf24','#f87171']\n];\nconst p = pairs[Math.floor(Math.random() * pairs.length)];\nmsg.payload = { colorA: p[0], colorB: p[1] };\nreturn msg;",
69
+ "outputs": 1,
70
+ "x": 360,
71
+ "y": 160,
72
+ "wires": [
73
+ [
74
+ "fc-webgpu-portal"
75
+ ]
76
+ ]
77
+ },
78
+ {
79
+ "id": "fc-webgpu-portal",
80
+ "type": "portal-react",
81
+ "z": "fc-webgpu-flow",
82
+ "name": "WebGPU Portal",
83
+ "subPath": "webgpu",
84
+ "pageTitle": "fromcubes WebGPU",
85
+ "customHead": "",
86
+ "portalAuth": false,
87
+ "showWsStatus": false,
88
+ "libs": [
89
+ {
90
+ "module": "three",
91
+ "var": "THREE"
92
+ }
93
+ ],
94
+ "componentCode": "function App() {\n const { data } = useNodeRed();\n const params = data || { colorA: '#22d3ee', colorB: '#a78bfa' };\n\n return (\n <Page>\n <Header title=\"fromcubes WebGPU\" subtitle=\"Three.js TSL + live Node-RED data\" />\n <TSLCanvas params={params} />\n <TSLStatusBar params={params} />\n </Page>\n );\n}",
95
+ "x": 560,
96
+ "y": 160,
97
+ "wires": [
98
+ [
99
+ "fc-webgpu-debug"
100
+ ]
101
+ ]
102
+ },
103
+ {
104
+ "id": "fc-webgpu-debug",
105
+ "type": "debug",
106
+ "z": "fc-webgpu-flow",
107
+ "name": "WebGPU output",
108
+ "active": true,
109
+ "tosidebar": true,
110
+ "x": 750,
111
+ "y": 160,
112
+ "wires": []
113
+ }
114
+ ]