Compare commits
21 Commits
fix-module
...
revers-upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e17d9ec0f | ||
|
|
87ea942399 | ||
|
|
39a032a285 | ||
|
|
a06940e981 | ||
|
|
4aebfadc8a | ||
|
|
f45f26994e | ||
|
|
c777a1a2b9 | ||
|
|
36fe7822f7 | ||
|
|
0ccf3310f9 | ||
|
|
a8d6552caa | ||
|
|
a131448dcf | ||
|
|
14a52dbc2e | ||
|
|
565391bd8c | ||
|
|
9bffa2a774 | ||
|
|
e42a07423e | ||
|
|
c5178ac16a | ||
|
|
33791e06cd | ||
|
|
c7e3bf624e | ||
|
|
ba027c2239 | ||
|
|
25fdabee29 | ||
|
|
de69c63ee3 |
12
devFlake/flake.lock
generated
12
devFlake/flake.lock
generated
@@ -84,11 +84,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-dev": {
|
"nixpkgs-dev": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756578978,
|
"lastModified": 1756662818,
|
||||||
"narHash": "sha256-dLgwMLIMyHlSeIDsoT2OcZBkuruIbjhIAv1sGANwtes=",
|
"narHash": "sha256-Opggp4xiucQ5gBceZ6OT2vWAZOjQb3qULv39scGZ9Nw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a85a50bef870537a9705f64ed75e54d1f4bf9c23",
|
"rev": "2e6aeede9cb4896693434684bb0002ab2c0cfc09",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -165,11 +165,11 @@
|
|||||||
"nixpkgs": []
|
"nixpkgs": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755934250,
|
"lastModified": 1756662192,
|
||||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
"narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
"rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
20
flake.lock
generated
20
flake.lock
generated
@@ -13,11 +13,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756091210,
|
"lastModified": 1756695982,
|
||||||
"narHash": "sha256-oEUEAZnLbNHi8ti4jY8x10yWcIkYoFc5XD+2hjmOS04=",
|
"narHash": "sha256-dyLhOSDzxZtRgi5aj/OuaZJUsuvo+8sZ9CU/qieZ15c=",
|
||||||
"rev": "eb831bca21476fa8f6df26cb39e076842634700d",
|
"rev": "cc8f26e7e6c2dc985526ba59b286ae5a83168cdb",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/eb831bca21476fa8f6df26cb39e076842634700d.tar.gz"
|
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/cc8f26e7e6c2dc985526ba59b286ae5a83168cdb.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -99,11 +99,11 @@
|
|||||||
},
|
},
|
||||||
"nixos-facter-modules": {
|
"nixos-facter-modules": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756291602,
|
"lastModified": 1756491981,
|
||||||
"narHash": "sha256-FYhiArSzcx60OwoH3JBp5Ho1D5HEwmZx6WoquauDv3g=",
|
"narHash": "sha256-lXyDAWPw/UngVtQfgQ8/nrubs2r+waGEYIba5UX62+k=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixos-facter-modules",
|
"repo": "nixos-facter-modules",
|
||||||
"rev": "5c37cee817c94f50710ab11c25de572bc3604bd5",
|
"rev": "c1b29520945d3e148cd96618c8a0d1f850965d8c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -181,11 +181,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755934250,
|
"lastModified": 1756662192,
|
||||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
"narHash": "sha256-F1oFfV51AE259I85av+MAia221XwMHCOtZCMcZLK2Jk=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
"rev": "1aabc6c05ccbcbf4a635fb7a90400e44282f61c4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ let
|
|||||||
url = "https://github.com/eigilnikolajsen/commit-mono/raw/0b3b192f035cdc8d1ea8ffb5463cc23d73d0b89f/src/fonts/fontlab/CommitMonoV143-VF.woff2";
|
url = "https://github.com/eigilnikolajsen/commit-mono/raw/0b3b192f035cdc8d1ea8ffb5463cc23d73d0b89f/src/fonts/fontlab/CommitMonoV143-VF.woff2";
|
||||||
hash = "sha256-80LKbD8ll+bA/NhLPz7WTTzlvbbQrxnRkNZFpVixzyk=";
|
hash = "sha256-80LKbD8ll+bA/NhLPz7WTTzlvbbQrxnRkNZFpVixzyk=";
|
||||||
};
|
};
|
||||||
|
commitMono_ttf = fetchurl {
|
||||||
|
url = "https://github.com/eigilnikolajsen/commit-mono/raw/0b3b192f035cdc8d1ea8ffb5463cc23d73d0b89f/src/fonts/fontlab/CommitMonoV143-VF.ttf";
|
||||||
|
hash = "sha256-mN6akBFjp2mBLDzy8bhtY6mKnO1nINdHqmZSaIQHw08=";
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
runCommand "" { } ''
|
runCommand "" { } ''
|
||||||
@@ -62,4 +66,5 @@ runCommand "" { } ''
|
|||||||
cp ${archivoSemi.semiBold} $out/ArchivoSemiCondensed-SemiBold.woff2
|
cp ${archivoSemi.semiBold} $out/ArchivoSemiCondensed-SemiBold.woff2
|
||||||
|
|
||||||
cp ${commitMono} $out/CommitMonoV143-VF.woff2
|
cp ${commitMono} $out/CommitMonoV143-VF.woff2
|
||||||
|
cp ${commitMono_ttf} $out/CommitMonoV143-VF.ttf
|
||||||
''
|
''
|
||||||
|
|||||||
55
pkgs/clan-app/ui/package-lock.json
generated
55
pkgs/clan-app/ui/package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"solid-js": "^1.9.7",
|
"solid-js": "^1.9.7",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"three": "^0.176.0",
|
"three": "^0.176.0",
|
||||||
|
"troika-three-text": "^0.52.4",
|
||||||
"valibot": "^1.1.0"
|
"valibot": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3807,6 +3808,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -7528,6 +7538,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
@@ -8655,6 +8674,36 @@
|
|||||||
"tree-kill": "cli.js"
|
"tree-kill": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/troika-three-text": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bidi-js": "^1.0.2",
|
||||||
|
"troika-three-utils": "^0.52.4",
|
||||||
|
"troika-worker-utils": "^0.52.0",
|
||||||
|
"webgl-sdf-generator": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-three-utils": {
|
||||||
|
"version": "0.52.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||||
|
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.125.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/troika-worker-utils": {
|
||||||
|
"version": "0.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||||
|
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
@@ -9268,6 +9317,12 @@
|
|||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webgl-sdf-generator": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"solid-js": "^1.9.7",
|
"solid-js": "^1.9.7",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"three": "^0.176.0",
|
"three": "^0.176.0",
|
||||||
|
"troika-three-text": "^0.52.4",
|
||||||
"valibot": "^1.1.0"
|
"valibot": "^1.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
width: 113px;
|
||||||
|
padding: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--clr-border-def-2, #d8e8eb);
|
||||||
|
background: var(--clr-bg-def-1, #fff);
|
||||||
|
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
max-height: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-def-3;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
&[aria-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
pkgs/clan-app/ui/src/components/ContextMenu/ContextMenu.tsx
Normal file
61
pkgs/clan-app/ui/src/components/ContextMenu/ContextMenu.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { onCleanup, onMount } from "solid-js";
|
||||||
|
import styles from "./ContextMenu.module.css";
|
||||||
|
import { Typography } from "../Typography/Typography";
|
||||||
|
|
||||||
|
export const Menu = (props: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
onSelect: (option: "move") => void;
|
||||||
|
close: () => void;
|
||||||
|
intersect: string[];
|
||||||
|
}) => {
|
||||||
|
let ref: HTMLUListElement;
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (!ref.contains(e.target as Node)) {
|
||||||
|
props.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() =>
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside),
|
||||||
|
);
|
||||||
|
const currentMachine = () => props.intersect.at(0) || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
ref={(el) => (ref = el)}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: `${props.y}px`,
|
||||||
|
left: `${props.x}px`,
|
||||||
|
"z-index": 1000,
|
||||||
|
"pointer-events": "auto",
|
||||||
|
}}
|
||||||
|
class={styles.list}
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class={styles.item}
|
||||||
|
aria-disabled={!currentMachine()}
|
||||||
|
onClick={() => {
|
||||||
|
console.log("Move clicked", currentMachine());
|
||||||
|
props.onSelect("move");
|
||||||
|
props.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
hierarchy="label"
|
||||||
|
size="s"
|
||||||
|
weight="bold"
|
||||||
|
color={currentMachine() ? "primary" : "quaternary"}
|
||||||
|
>
|
||||||
|
Move
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,8 +8,8 @@ import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
|||||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||||
import { SidebarProps } from "./Sidebar";
|
import { SidebarProps } from "./Sidebar";
|
||||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
|
||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
|
import { useClanContext } from "@/src/routes/Clan/Clan";
|
||||||
|
|
||||||
interface MachineProps {
|
interface MachineProps {
|
||||||
clanURI: string;
|
clanURI: string;
|
||||||
@@ -59,10 +59,7 @@ const MachineRoute = (props: MachineProps) => {
|
|||||||
export const SidebarBody = (props: SidebarProps) => {
|
export const SidebarBody = (props: SidebarProps) => {
|
||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
|
|
||||||
const ctx = useContext(ClanContext);
|
const ctx = useClanContext();
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("ClanContext not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionLabels = (props.staticSections || []).map(
|
const sectionLabels = (props.staticSections || []).map(
|
||||||
(section) => section.title,
|
(section) => section.title,
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import Icon from "@/src/components/Icon/Icon";
|
|||||||
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import { createSignal, For, Show, Suspense, useContext } from "solid-js";
|
import { createSignal, For, Show, Suspense } from "solid-js";
|
||||||
import { navigateToOnboarding } from "@/src/hooks/clan";
|
import { navigateToOnboarding } from "@/src/hooks/clan";
|
||||||
import { setActiveClanURI } from "@/src/stores/clan";
|
import { setActiveClanURI } from "@/src/stores/clan";
|
||||||
import { Button } from "../Button/Button";
|
import { Button } from "../Button/Button";
|
||||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
|
||||||
import { ClanSettingsModal } from "@/src/modals/ClanSettingsModal/ClanSettingsModal";
|
import { ClanSettingsModal } from "@/src/modals/ClanSettingsModal/ClanSettingsModal";
|
||||||
|
import { useClanContext } from "@/src/routes/Clan/Clan";
|
||||||
|
|
||||||
export const SidebarHeader = () => {
|
export const SidebarHeader = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -17,11 +17,7 @@ export const SidebarHeader = () => {
|
|||||||
const [showSettings, setShowSettings] = createSignal(false);
|
const [showSettings, setShowSettings] = createSignal(false);
|
||||||
|
|
||||||
// get information about the current active clan
|
// get information about the current active clan
|
||||||
const ctx = useContext(ClanContext);
|
const ctx = useClanContext();
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("SidebarContext not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const clanChar = () =>
|
const clanChar = () =>
|
||||||
ctx?.activeClanQuery?.data?.details.name.charAt(0).toUpperCase();
|
ctx?.activeClanQuery?.data?.details.name.charAt(0).toUpperCase();
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export const useMachineStateQuery = (clanURI: string, machineName: string) => {
|
|||||||
const client = useApiClient();
|
const client = useApiClient();
|
||||||
return useQuery<MachineState>(() => ({
|
return useQuery<MachineState>(() => ({
|
||||||
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName, "state"],
|
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName, "state"],
|
||||||
|
staleTime: 60_000, // 1 minute stale time
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const apiCall = client.fetch("get_machine_state", {
|
const apiCall = client.fetch("get_machine_state", {
|
||||||
machine: {
|
machine: {
|
||||||
@@ -455,14 +456,12 @@ export const useMachineGenerators = (
|
|||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const call = client.fetch("get_generators", {
|
const call = client.fetch("get_generators", {
|
||||||
machines: [
|
machine: {
|
||||||
{
|
name: machineName,
|
||||||
name: machineName,
|
flake: {
|
||||||
flake: {
|
identifier: clanUri,
|
||||||
identifier: clanUri,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
full_closure: true, // TODO: Make this configurable
|
full_closure: true, // TODO: Make this configurable
|
||||||
// TODO: Make this configurable
|
// TODO: Make this configurable
|
||||||
include_previous_values: true,
|
include_previous_values: true,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
Show,
|
Show,
|
||||||
Signal,
|
|
||||||
useContext,
|
useContext,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
|
buildClanPath,
|
||||||
buildMachinePath,
|
buildMachinePath,
|
||||||
maybeUseMachineName,
|
maybeUseMachineName,
|
||||||
useClanURI,
|
useClanURI,
|
||||||
@@ -55,57 +55,38 @@ interface ClanContextProps {
|
|||||||
setShowAddMachine(value: boolean): void;
|
setShowAddMachine(value: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultClanContext implements ClanContextProps {
|
function createClanContext(
|
||||||
public readonly clanURI: string;
|
clanURI: string,
|
||||||
|
machinesQuery: MachinesQueryResult,
|
||||||
|
activeClanQuery: UseQueryResult<ClanDetails>,
|
||||||
|
otherClanQueries: UseQueryResult<ClanDetails>[],
|
||||||
|
) {
|
||||||
|
const [showAddMachine, setShowAddMachine] = createSignal(false);
|
||||||
|
const allClansQueries = [activeClanQuery, ...otherClanQueries];
|
||||||
|
const allQueries = [machinesQuery, ...allClansQueries];
|
||||||
|
|
||||||
public readonly activeClanQuery: UseQueryResult<ClanDetails>;
|
return {
|
||||||
public readonly otherClanQueries: UseQueryResult<ClanDetails>[];
|
clanURI,
|
||||||
public readonly allClansQueries: UseQueryResult<ClanDetails>[];
|
machinesQuery,
|
||||||
|
activeClanQuery,
|
||||||
public readonly machinesQuery: MachinesQueryResult;
|
otherClanQueries,
|
||||||
|
allClansQueries,
|
||||||
allQueries: UseQueryResult[];
|
isLoading: () => allQueries.some((q) => q.isLoading),
|
||||||
|
isError: () => activeClanQuery.isError,
|
||||||
showAddMachineSignal: Signal<boolean>;
|
showAddMachine,
|
||||||
|
setShowAddMachine,
|
||||||
constructor(
|
};
|
||||||
clanURI: string,
|
|
||||||
machinesQuery: MachinesQueryResult,
|
|
||||||
activeClanQuery: UseQueryResult<ClanDetails>,
|
|
||||||
otherClanQueries: UseQueryResult<ClanDetails>[],
|
|
||||||
) {
|
|
||||||
this.clanURI = clanURI;
|
|
||||||
this.machinesQuery = machinesQuery;
|
|
||||||
|
|
||||||
this.activeClanQuery = activeClanQuery;
|
|
||||||
this.otherClanQueries = otherClanQueries;
|
|
||||||
this.allClansQueries = [activeClanQuery, ...otherClanQueries];
|
|
||||||
|
|
||||||
this.allQueries = [machinesQuery, activeClanQuery, ...otherClanQueries];
|
|
||||||
|
|
||||||
this.showAddMachineSignal = createSignal(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading(): boolean {
|
|
||||||
return this.allQueries.some((q) => q.isLoading);
|
|
||||||
}
|
|
||||||
|
|
||||||
isError(): boolean {
|
|
||||||
return this.activeClanQuery.isError;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowAddMachine(value: boolean) {
|
|
||||||
const [_, setShow] = this.showAddMachineSignal;
|
|
||||||
setShow(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
showAddMachine(): boolean {
|
|
||||||
const [show, _] = this.showAddMachineSignal;
|
|
||||||
return show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClanContext = createContext<ClanContextProps>();
|
const ClanContext = createContext<ClanContextProps>();
|
||||||
|
|
||||||
|
export const useClanContext = () => {
|
||||||
|
const ctx = useContext(ClanContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("ClanContext not found");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
@@ -124,17 +105,15 @@ export const Clan: Component<RouteSectionProps> = (props) => {
|
|||||||
|
|
||||||
const machinesQuery = useMachinesQuery(clanURI);
|
const machinesQuery = useMachinesQuery(clanURI);
|
||||||
|
|
||||||
|
const ctx = createClanContext(
|
||||||
|
clanURI,
|
||||||
|
machinesQuery,
|
||||||
|
activeClanQuery,
|
||||||
|
otherClanQueries,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClanContext.Provider
|
<ClanContext.Provider value={ctx}>
|
||||||
value={
|
|
||||||
new DefaultClanContext(
|
|
||||||
clanURI,
|
|
||||||
machinesQuery,
|
|
||||||
activeClanQuery,
|
|
||||||
otherClanQueries,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class={cx(styles.sidebarContainer, {
|
class={cx(styles.sidebarContainer, {
|
||||||
[styles.machineSelected]: useMachineName(),
|
[styles.machineSelected]: useMachineName(),
|
||||||
@@ -149,10 +128,7 @@ export const Clan: Component<RouteSectionProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ClanSceneController = (props: RouteSectionProps) => {
|
const ClanSceneController = (props: RouteSectionProps) => {
|
||||||
const ctx = useContext(ClanContext);
|
const ctx = useClanContext();
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("ClanContext not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -197,6 +173,8 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
const selected = ids.values().next().value;
|
const selected = ids.values().next().value;
|
||||||
if (selected) {
|
if (selected) {
|
||||||
navigate(buildMachinePath(ctx.clanURI, selected));
|
navigate(buildMachinePath(ctx.clanURI, selected));
|
||||||
|
} else {
|
||||||
|
navigate(buildClanPath(ctx.clanURI));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class MachineManager {
|
|||||||
machinesQueryResult: MachinesQueryResult,
|
machinesQueryResult: MachinesQueryResult,
|
||||||
selectedIds: Accessor<Set<string>>,
|
selectedIds: Accessor<Set<string>>,
|
||||||
setMachinePos: (id: string, position: [number, number] | null) => void,
|
setMachinePos: (id: string, position: [number, number] | null) => void,
|
||||||
|
camera: THREE.Camera,
|
||||||
) {
|
) {
|
||||||
this.machinePositionsSignal = machinePositionsSignal;
|
this.machinePositionsSignal = machinePositionsSignal;
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export class MachineManager {
|
|||||||
id,
|
id,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
highlightGroups,
|
highlightGroups,
|
||||||
|
camera,
|
||||||
);
|
);
|
||||||
this.machines.set(id, repr);
|
this.machines.set(id, repr);
|
||||||
scene.add(repr.group);
|
scene.add(repr.group);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { ObjectRegistry } from "./ObjectRegistry";
|
|||||||
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
||||||
import { Accessor, createEffect, createRoot, on } from "solid-js";
|
import { Accessor, createEffect, createRoot, on } from "solid-js";
|
||||||
import { renderLoop } from "./RenderLoop";
|
import { renderLoop } from "./RenderLoop";
|
||||||
|
// @ts-expect-error: No types for troika-three-text
|
||||||
|
import { Text } from "troika-three-text";
|
||||||
|
import ttf from "../../.fonts/CommitMonoV143-VF.ttf";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const BASE_SIZE = 0.9;
|
const BASE_SIZE = 0.9;
|
||||||
@@ -28,6 +31,7 @@ export class MachineRepr {
|
|||||||
private baseMesh: THREE.Mesh;
|
private baseMesh: THREE.Mesh;
|
||||||
private geometry: THREE.BoxGeometry;
|
private geometry: THREE.BoxGeometry;
|
||||||
private material: THREE.MeshPhongMaterial;
|
private material: THREE.MeshPhongMaterial;
|
||||||
|
private camera: THREE.Camera;
|
||||||
|
|
||||||
private disposeRoot: () => void;
|
private disposeRoot: () => void;
|
||||||
|
|
||||||
@@ -38,8 +42,10 @@ export class MachineRepr {
|
|||||||
id: string,
|
id: string,
|
||||||
selectedSignal: Accessor<Set<string>>,
|
selectedSignal: Accessor<Set<string>>,
|
||||||
highlightGroups: Record<string, Set<string>>, // Reactive store
|
highlightGroups: Record<string, Set<string>>, // Reactive store
|
||||||
|
camera: THREE.Camera,
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.camera = camera;
|
||||||
this.geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
|
this.geometry = new THREE.BoxGeometry(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE);
|
||||||
this.material = new THREE.MeshPhongMaterial({
|
this.material = new THREE.MeshPhongMaterial({
|
||||||
color: CUBE_COLOR,
|
color: CUBE_COLOR,
|
||||||
@@ -62,7 +68,6 @@ export class MachineRepr {
|
|||||||
this.baseMesh.name = "base";
|
this.baseMesh.name = "base";
|
||||||
|
|
||||||
const label = this.createLabel(id);
|
const label = this.createLabel(id);
|
||||||
this.cubeMesh.add(label);
|
|
||||||
|
|
||||||
const shadowPlaneMaterial = new THREE.MeshStandardMaterial({
|
const shadowPlaneMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: BASE_COLOR, // any color you like
|
color: BASE_COLOR, // any color you like
|
||||||
@@ -82,6 +87,7 @@ export class MachineRepr {
|
|||||||
shadowPlane.position.set(0, BASE_HEIGHT + 0.0001, 0);
|
shadowPlane.position.set(0, BASE_HEIGHT + 0.0001, 0);
|
||||||
|
|
||||||
this.group = new THREE.Group();
|
this.group = new THREE.Group();
|
||||||
|
this.group.add(label);
|
||||||
this.group.add(this.cubeMesh);
|
this.group.add(this.cubeMesh);
|
||||||
this.group.add(this.baseMesh);
|
this.group.add(this.baseMesh);
|
||||||
this.group.add(shadowPlane);
|
this.group.add(shadowPlane);
|
||||||
@@ -161,12 +167,27 @@ export class MachineRepr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createLabel(id: string) {
|
private createLabel(id: string) {
|
||||||
const div = document.createElement("div");
|
const text = new Text();
|
||||||
div.className = "machine-label";
|
text.text = id;
|
||||||
div.textContent = id;
|
text.font = ttf;
|
||||||
const label = new CSS2DObject(div);
|
// text.font = ".fonts/CommitMonoV143-VF.woff2"; // <-- normal web font, not JSON
|
||||||
label.position.set(0, CUBE_SIZE + 0.1, 0);
|
text.fontSize = 0.15; // relative to your cube size
|
||||||
return label;
|
text.color = 0x000000; // any THREE.Color
|
||||||
|
text.anchorX = "center"; // horizontal centering
|
||||||
|
text.anchorY = "bottom"; // baseline aligns to cube top
|
||||||
|
text.position.set(0, CUBE_SIZE + 0.05, 0);
|
||||||
|
|
||||||
|
// If you want it to always face camera:
|
||||||
|
text.userData.isLabel = true;
|
||||||
|
text.outlineWidth = 0.005;
|
||||||
|
text.outlineColor = 0x333333;
|
||||||
|
text.quaternion.copy(this.camera.quaternion);
|
||||||
|
|
||||||
|
// Re-render on text changes
|
||||||
|
text.sync(() => {
|
||||||
|
renderLoop.requestRender();
|
||||||
|
});
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(scene: THREE.Scene) {
|
dispose(scene: THREE.Scene) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Scene, Camera, WebGLRenderer } from "three";
|
import { Scene, Camera, WebGLRenderer } from "three";
|
||||||
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
|
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
|
||||||
import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
||||||
|
import * as THREE from "three";
|
||||||
/**
|
/**
|
||||||
* Private class to manage the render loop
|
* Private class to manage the render loop
|
||||||
* @internal
|
* @internal
|
||||||
@@ -93,6 +93,18 @@ class RenderLoop {
|
|||||||
|
|
||||||
this.renderer.render(this.bgScene, this.bgCamera);
|
this.renderer.render(this.bgScene, this.bgCamera);
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
|
||||||
|
this.scene.traverse((obj) => {
|
||||||
|
if (obj.userData.isLabel) {
|
||||||
|
(obj as THREE.Mesh).quaternion.copy(this.camera.quaternion);
|
||||||
|
}
|
||||||
|
// if (obj.userData.isLabel) {
|
||||||
|
// const camPos = new THREE.Vector3();
|
||||||
|
// this.camera.getWorldPosition(camPos);
|
||||||
|
// obj.lookAt(new THREE.Vector3(camPos.x, obj.position.y, camPos.z));
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
this.labelRenderer.render(this.scene, this.camera);
|
this.labelRenderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
onMount,
|
onMount,
|
||||||
on,
|
on,
|
||||||
JSX,
|
JSX,
|
||||||
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import "./cubes.css";
|
import "./cubes.css";
|
||||||
|
|
||||||
@@ -22,6 +23,29 @@ import { renderLoop } from "./RenderLoop";
|
|||||||
import { ObjectRegistry } from "./ObjectRegistry";
|
import { ObjectRegistry } from "./ObjectRegistry";
|
||||||
import { MachineManager } from "./MachineManager";
|
import { MachineManager } from "./MachineManager";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
import { Menu } from "../components/ContextMenu/ContextMenu";
|
||||||
|
import { clearHighlight, setHighlightGroups } from "./highlightStore";
|
||||||
|
|
||||||
|
function intersectMachines(
|
||||||
|
event: MouseEvent,
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
camera: THREE.Camera,
|
||||||
|
machineManager: MachineManager,
|
||||||
|
raycaster: THREE.Raycaster,
|
||||||
|
): string[] {
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
const mouse = new THREE.Vector2(
|
||||||
|
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
|
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
||||||
|
);
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const intersects = raycaster.intersectObjects(
|
||||||
|
Array.from(machineManager.machines.values().map((m) => m.group)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return intersects.map((i) => i.object.userData.id);
|
||||||
|
}
|
||||||
|
|
||||||
function garbageCollectGroup(group: THREE.Group) {
|
function garbageCollectGroup(group: THREE.Group) {
|
||||||
for (const child of group.children) {
|
for (const child of group.children) {
|
||||||
@@ -64,7 +88,7 @@ export function useMachineClick() {
|
|||||||
|
|
||||||
/*Gloabl signal*/
|
/*Gloabl signal*/
|
||||||
const [worldMode, setWorldMode] = createSignal<
|
const [worldMode, setWorldMode] = createSignal<
|
||||||
"default" | "select" | "service" | "create"
|
"default" | "select" | "service" | "create" | "move"
|
||||||
>("select");
|
>("select");
|
||||||
export { worldMode, setWorldMode };
|
export { worldMode, setWorldMode };
|
||||||
|
|
||||||
@@ -88,7 +112,7 @@ export function CubeScene(props: {
|
|||||||
let controls: MapControls;
|
let controls: MapControls;
|
||||||
// Raycaster for clicking
|
// Raycaster for clicking
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
let initBase: THREE.Mesh | undefined;
|
let actionBase: THREE.Mesh | undefined;
|
||||||
|
|
||||||
// Create background scene
|
// Create background scene
|
||||||
const bgScene = new THREE.Scene();
|
const bgScene = new THREE.Scene();
|
||||||
@@ -111,6 +135,10 @@ export function CubeScene(props: {
|
|||||||
position: { x: 0, y: 0, z: 0 },
|
position: { x: 0, y: 0, z: 0 },
|
||||||
spherical: { radius: 0, theta: 0, phi: 0 },
|
spherical: { radius: 0, theta: 0, phi: 0 },
|
||||||
});
|
});
|
||||||
|
// Context menu state
|
||||||
|
const [contextOpen, setContextOpen] = createSignal(false);
|
||||||
|
const [menuPos, setMenuPos] = createSignal<{ x: number; y: number }>();
|
||||||
|
const [menuIntersection, setMenuIntersection] = createSignal<string[]>([]);
|
||||||
|
|
||||||
// Grid configuration
|
// Grid configuration
|
||||||
const GRID_SIZE = 1;
|
const GRID_SIZE = 1;
|
||||||
@@ -126,8 +154,10 @@ export function CubeScene(props: {
|
|||||||
const BASE_COLOR = 0xecfdff;
|
const BASE_COLOR = 0xecfdff;
|
||||||
const BASE_EMISSIVE = 0x0c0c0c;
|
const BASE_EMISSIVE = 0x0c0c0c;
|
||||||
|
|
||||||
const CREATE_BASE_COLOR = 0x636363;
|
const ACTION_BASE_COLOR = 0x636363;
|
||||||
|
|
||||||
const CREATE_BASE_EMISSIVE = 0xc5fad7;
|
const CREATE_BASE_EMISSIVE = 0xc5fad7;
|
||||||
|
const MOVE_BASE_EMISSIVE = 0xb2d7ff;
|
||||||
|
|
||||||
function createCubeBase(
|
function createCubeBase(
|
||||||
cube_pos: [number, number, number],
|
cube_pos: [number, number, number],
|
||||||
@@ -148,12 +178,6 @@ export function CubeScene(props: {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelection(id: string) {
|
|
||||||
const next = new Set<string>();
|
|
||||||
next.add(id);
|
|
||||||
props.onSelect(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialCameraPosition = { x: 20, y: 20, z: 20 };
|
const initialCameraPosition = { x: 20, y: 20, z: 20 };
|
||||||
const initialSphericalCameraPosition = new THREE.Spherical();
|
const initialSphericalCameraPosition = new THREE.Spherical();
|
||||||
initialSphericalCameraPosition.setFromVector3(
|
initialSphericalCameraPosition.setFromVector3(
|
||||||
@@ -350,15 +374,15 @@ export function CubeScene(props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Important create CubeBase depends on sharedBaseGeometry
|
// Important create CubeBase depends on sharedBaseGeometry
|
||||||
initBase = createCubeBase(
|
actionBase = createCubeBase(
|
||||||
[1, BASE_HEIGHT / 2, 1],
|
[1, BASE_HEIGHT / 2, 1],
|
||||||
1,
|
1,
|
||||||
CREATE_BASE_COLOR,
|
ACTION_BASE_COLOR,
|
||||||
CREATE_BASE_EMISSIVE,
|
CREATE_BASE_EMISSIVE,
|
||||||
);
|
);
|
||||||
initBase.visible = false;
|
actionBase.visible = false;
|
||||||
|
|
||||||
scene.add(initBase);
|
scene.add(actionBase);
|
||||||
|
|
||||||
// const spherical = new THREE.Spherical();
|
// const spherical = new THREE.Spherical();
|
||||||
// spherical.setFromVector3(camera.position);
|
// spherical.setFromVector3(camera.position);
|
||||||
@@ -387,9 +411,9 @@ export function CubeScene(props: {
|
|||||||
createEffect(
|
createEffect(
|
||||||
on(worldMode, (mode) => {
|
on(worldMode, (mode) => {
|
||||||
if (mode === "create") {
|
if (mode === "create") {
|
||||||
initBase!.visible = true;
|
actionBase!.visible = true;
|
||||||
} else {
|
} else {
|
||||||
initBase!.visible = false;
|
actionBase!.visible = false;
|
||||||
}
|
}
|
||||||
renderLoop.requestRender();
|
renderLoop.requestRender();
|
||||||
}),
|
}),
|
||||||
@@ -404,6 +428,7 @@ export function CubeScene(props: {
|
|||||||
props.cubesQuery,
|
props.cubesQuery,
|
||||||
props.selectedIds,
|
props.selectedIds,
|
||||||
props.setMachinePos,
|
props.setMachinePos,
|
||||||
|
camera,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click handler:
|
// Click handler:
|
||||||
@@ -426,11 +451,21 @@ export function CubeScene(props: {
|
|||||||
console.error("Error creating cube:", error);
|
console.error("Error creating cube:", error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (initBase) initBase.visible = false;
|
if (actionBase) actionBase.visible = false;
|
||||||
|
|
||||||
setWorldMode("default");
|
setWorldMode("default");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (worldMode() === "move") {
|
||||||
|
console.log("sanpped");
|
||||||
|
const currId = menuIntersection().at(0);
|
||||||
|
const pos = cursorPosition();
|
||||||
|
if (!currId || !pos) return;
|
||||||
|
|
||||||
|
props.setMachinePos(currId, pos);
|
||||||
|
setWorldMode("select");
|
||||||
|
clearHighlight("move");
|
||||||
|
}
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
const mouse = new THREE.Vector2(
|
const mouse = new THREE.Vector2(
|
||||||
@@ -447,13 +482,13 @@ export function CubeScene(props: {
|
|||||||
console.log("Clicked on cube:", intersects);
|
console.log("Clicked on cube:", intersects);
|
||||||
const id = intersects[0].object.userData.id;
|
const id = intersects[0].object.userData.id;
|
||||||
|
|
||||||
if (worldMode() === "select") toggleSelection(id);
|
if (worldMode() === "select") props.onSelect(new Set<string>([id]));
|
||||||
|
|
||||||
emitMachineClick(id); // notify subscribers
|
emitMachineClick(id); // notify subscribers
|
||||||
} else {
|
} else {
|
||||||
emitMachineClick(null);
|
emitMachineClick(null);
|
||||||
|
|
||||||
props.onSelect(new Set<string>()); // Clear selection if clicked outside cubes
|
if (worldMode() === "select") props.onSelect(new Set<string>());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -484,18 +519,28 @@ export function CubeScene(props: {
|
|||||||
renderLoop.requestRender();
|
renderLoop.requestRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (e.button === 2) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const intersection = intersectMachines(
|
||||||
|
e,
|
||||||
|
renderer,
|
||||||
|
camera,
|
||||||
|
machineManager,
|
||||||
|
raycaster,
|
||||||
|
);
|
||||||
|
if (!intersection.length) return;
|
||||||
|
setMenuIntersection(intersection);
|
||||||
|
setMenuPos({ x: e.clientX, y: e.clientY });
|
||||||
|
setContextOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("mousedown", handleMouseDown);
|
||||||
renderer.domElement.addEventListener("mousemove", onMouseMove);
|
renderer.domElement.addEventListener("mousemove", onMouseMove);
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
// For debugging,
|
|
||||||
// TODO: Remove in production
|
|
||||||
window.addEventListener(
|
|
||||||
"contextmenu",
|
|
||||||
(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
{ capture: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
renderLoop.requestRender();
|
renderLoop.requestRender();
|
||||||
@@ -522,12 +567,12 @@ export function CubeScene(props: {
|
|||||||
renderer.domElement.removeEventListener("mousemove", onMouseMove);
|
renderer.domElement.removeEventListener("mousemove", onMouseMove);
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
|
|
||||||
if (initBase) {
|
if (actionBase) {
|
||||||
initBase.geometry.dispose();
|
actionBase.geometry.dispose();
|
||||||
if (Array.isArray(initBase.material)) {
|
if (Array.isArray(actionBase.material)) {
|
||||||
initBase.material.forEach((material) => material.dispose());
|
actionBase.material.forEach((material) => material.dispose());
|
||||||
} else {
|
} else {
|
||||||
initBase.material.dispose();
|
actionBase.material.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,10 +588,18 @@ export function CubeScene(props: {
|
|||||||
renderLoop.requestRender();
|
renderLoop.requestRender();
|
||||||
};
|
};
|
||||||
const onMouseMove = (event: MouseEvent) => {
|
const onMouseMove = (event: MouseEvent) => {
|
||||||
if (worldMode() !== "create") return;
|
if (!(worldMode() === "create" || worldMode() === "move")) return;
|
||||||
if (!initBase) return;
|
if (!actionBase) return;
|
||||||
|
|
||||||
initBase.visible = true;
|
console.log("Mouse move in create/move mode");
|
||||||
|
|
||||||
|
actionBase.visible = true;
|
||||||
|
(actionBase.material as THREE.MeshPhongMaterial).emissive.set(
|
||||||
|
worldMode() === "create" ? CREATE_BASE_EMISSIVE : MOVE_BASE_EMISSIVE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate mouse position in normalized device coordinates
|
||||||
|
// (-1 to +1) for both components
|
||||||
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
const mouse = new THREE.Vector2(
|
const mouse = new THREE.Vector2(
|
||||||
@@ -577,21 +630,37 @@ export function CubeScene(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Math.abs(initBase.position.x - snapped.x) > 0.01 ||
|
Math.abs(actionBase.position.x - snapped.x) > 0.01 ||
|
||||||
Math.abs(initBase.position.z - snapped.z) > 0.01
|
Math.abs(actionBase.position.z - snapped.z) > 0.01
|
||||||
) {
|
) {
|
||||||
// Only request render if the position actually changed
|
// Only request render if the position actually changed
|
||||||
initBase.position.set(snapped.x, 0, snapped.z);
|
actionBase.position.set(snapped.x, 0, snapped.z);
|
||||||
setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation
|
setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation
|
||||||
renderLoop.requestRender();
|
renderLoop.requestRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleMenuSelect = (mode: "move") => {
|
||||||
|
setWorldMode(mode);
|
||||||
|
setHighlightGroups({ move: new Set(menuIntersection()) });
|
||||||
|
console.log("Menu selected, new World mode", worldMode());
|
||||||
|
};
|
||||||
|
|
||||||
const machinesQuery = useMachinesQuery(props.clanURI);
|
const machinesQuery = useMachinesQuery(props.clanURI);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Show when={contextOpen()}>
|
||||||
|
<Portal mount={document.body}>
|
||||||
|
<Menu
|
||||||
|
onSelect={handleMenuSelect}
|
||||||
|
intersect={menuIntersection()}
|
||||||
|
x={menuPos()!.x - 10}
|
||||||
|
y={menuPos()!.y - 10}
|
||||||
|
close={() => setContextOpen(false)}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"cubes-scene-container",
|
"cubes-scene-container",
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ export const StepTags = (props: { onDone: () => void }) => {
|
|||||||
store.onCreated(store.general.name);
|
store.onCreated(store.general.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Done creating machine");
|
|
||||||
props.onDone();
|
props.onDone();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,8 @@ type ModuleItem = ServiceModules["modules"][number];
|
|||||||
|
|
||||||
interface Module {
|
interface Module {
|
||||||
value: string;
|
value: string;
|
||||||
input?: string | null;
|
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
|
||||||
raw: ModuleItem;
|
raw: ModuleItem;
|
||||||
instances: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectService = () => {
|
const SelectService = () => {
|
||||||
@@ -70,11 +67,8 @@ const SelectService = () => {
|
|||||||
setModuleOptions(
|
setModuleOptions(
|
||||||
serviceModulesQuery.data.modules.map((currService) => ({
|
serviceModulesQuery.data.modules.map((currService) => ({
|
||||||
value: `${currService.usage_ref.name}:${currService.usage_ref.input}`,
|
value: `${currService.usage_ref.name}:${currService.usage_ref.input}`,
|
||||||
label: currService.info.manifest.name,
|
label: currService.usage_ref.name,
|
||||||
description: currService.info.manifest.description,
|
|
||||||
input: currService.usage_ref.input,
|
|
||||||
raw: currService,
|
raw: currService,
|
||||||
instances: currService.instance_refs,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,14 +94,14 @@ const SelectService = () => {
|
|||||||
// For now:
|
// For now:
|
||||||
// Create a new instance, if there are no instances yet
|
// Create a new instance, if there are no instances yet
|
||||||
// Update the first instance, if there is one
|
// Update the first instance, if there is one
|
||||||
if (module.instances.length === 0) {
|
if (module.raw.instance_refs.length === 0) {
|
||||||
set("action", "create");
|
set("action", "create");
|
||||||
} else {
|
} else {
|
||||||
if (!serviceInstancesQuery.data) return;
|
if (!serviceInstancesQuery.data) return;
|
||||||
if (!machinesQuery.data) return;
|
if (!machinesQuery.data) return;
|
||||||
set("action", "update");
|
set("action", "update");
|
||||||
|
|
||||||
const instanceName = module.instances[0];
|
const instanceName = module.raw.instance_refs[0];
|
||||||
const instance = serviceInstancesQuery.data[instanceName];
|
const instance = serviceInstancesQuery.data[instanceName];
|
||||||
console.log("Editing existing instance", module);
|
console.log("Editing existing instance", module);
|
||||||
|
|
||||||
@@ -157,7 +151,7 @@ const SelectService = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
<Combobox.ItemLabel class="flex gap-1.5">
|
<Combobox.ItemLabel class="flex gap-1.5">
|
||||||
<Show when={item.instances.length > 0}>
|
<Show when={item.raw.instance_refs.length > 0}>
|
||||||
<div class="flex items-center rounded bg-[#76FFA4] px-1 py-0.5">
|
<div class="flex items-center rounded bg-[#76FFA4] px-1 py-0.5">
|
||||||
<Typography hierarchy="label" weight="bold" size="xxs">
|
<Typography hierarchy="label" weight="bold" size="xxs">
|
||||||
Added
|
Added
|
||||||
@@ -176,12 +170,12 @@ const SelectService = () => {
|
|||||||
inverted
|
inverted
|
||||||
class="flex justify-between"
|
class="flex justify-between"
|
||||||
>
|
>
|
||||||
<span class="inline-block max-w-48 truncate align-middle">
|
<span class="inline-block max-w-80 truncate align-middle">
|
||||||
{item.description}
|
{item.raw.info.manifest.description}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-block max-w-12 truncate align-middle">
|
<span class="inline-block max-w-32 truncate align-middle">
|
||||||
<Show when={!item.raw.native} fallback="by clan-core">
|
<Show when={!item.raw.native} fallback="by clan-core">
|
||||||
by {item.input}
|
by {item.raw.usage_ref.input}
|
||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -862,7 +862,7 @@ def test_api_set_prompts(
|
|||||||
|
|
||||||
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||||
generators = get_generators(
|
generators = get_generators(
|
||||||
machines=[machine],
|
machine=machine,
|
||||||
full_closure=True,
|
full_closure=True,
|
||||||
include_previous_values=True,
|
include_previous_values=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def vars_status(
|
|||||||
unfixed_secret_vars = []
|
unfixed_secret_vars = []
|
||||||
invalid_generators = []
|
invalid_generators = []
|
||||||
|
|
||||||
generators = Generator.get_machine_generators([machine.name], machine.flake)
|
generators = Generator.get_machine_generators(machine.name, machine.flake)
|
||||||
if generator_name:
|
if generator_name:
|
||||||
for generator in generators:
|
for generator in generators:
|
||||||
if generator_name == generator.name:
|
if generator_name == generator.name:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def get_machine_vars(machine: Machine) -> list[Var]:
|
|||||||
|
|
||||||
all_vars = []
|
all_vars = []
|
||||||
|
|
||||||
generators = get_generators(machines=[machine], full_closure=True)
|
generators = get_generators(machine=machine, full_closure=True)
|
||||||
for generator in generators:
|
for generator in generators:
|
||||||
for var in generator.files:
|
for var in generator.files:
|
||||||
if var.secret:
|
if var.secret:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class SecretStore(StoreBase):
|
|||||||
# no need to generate keys if we don't manage secrets
|
# no need to generate keys if we don't manage secrets
|
||||||
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
vars_generators = Generator.get_machine_generators([machine], self.flake)
|
vars_generators = Generator.get_machine_generators(machine, self.flake)
|
||||||
if not vars_generators:
|
if not vars_generators:
|
||||||
return
|
return
|
||||||
has_secrets = False
|
has_secrets = False
|
||||||
@@ -143,7 +143,7 @@ class SecretStore(StoreBase):
|
|||||||
if generators is None:
|
if generators is None:
|
||||||
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
generators = Generator.get_machine_generators([machine], self.flake)
|
generators = Generator.get_machine_generators(machine, self.flake)
|
||||||
file_found = False
|
file_found = False
|
||||||
outdated = []
|
outdated = []
|
||||||
for generator in generators:
|
for generator in generators:
|
||||||
@@ -220,7 +220,7 @@ class SecretStore(StoreBase):
|
|||||||
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
|
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
|
||||||
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
vars_generators = Generator.get_machine_generators([machine], self.flake)
|
vars_generators = Generator.get_machine_generators(machine, self.flake)
|
||||||
if "users" in phases or "services" in phases:
|
if "users" in phases or "services" in phases:
|
||||||
key_name = f"{machine}-age.key"
|
key_name = f"{machine}-age.key"
|
||||||
if not has_secret(sops_secrets_folder(self.flake.path) / key_name):
|
if not has_secret(sops_secrets_folder(self.flake.path) / key_name):
|
||||||
@@ -356,7 +356,7 @@ class SecretStore(StoreBase):
|
|||||||
if generators is None:
|
if generators is None:
|
||||||
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
from clan_cli.vars.generator import Generator # noqa: PLC0415
|
||||||
|
|
||||||
generators = Generator.get_machine_generators([machine], self.flake)
|
generators = Generator.get_machine_generators(machine, self.flake)
|
||||||
file_found = False
|
file_found = False
|
||||||
for generator in generators:
|
for generator in generators:
|
||||||
for file in generator.files:
|
for file in generator.files:
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def test_clan_create_api(
|
|||||||
# Invalidate cache because of new inventory
|
# Invalidate cache because of new inventory
|
||||||
clan_dir_flake.invalidate_cache()
|
clan_dir_flake.invalidate_cache()
|
||||||
|
|
||||||
generators = get_generators(machines=[machine], full_closure=True)
|
generators = get_generators(machine=machine, full_closure=True)
|
||||||
collected_prompt_values = {}
|
collected_prompt_values = {}
|
||||||
for generator in generators:
|
for generator in generators:
|
||||||
prompt_values = {}
|
prompt_values = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user