From f155c68efe833cf010e5e82ea83fc4a10e72090e Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:13:38 +0200 Subject: [PATCH 1/6] ui/scene: fix animateToPosition --- pkgs/clan-app/ui/src/scene/cubes.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index 3104279f2..a81431e4b 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -251,7 +251,10 @@ export function CubeScene(props: { // Reactive cubes memo - this recalculates whenever data changes const cubes = createMemo(() => { console.log("Calculating cubes..."); - const currentIds = Object.keys(unwrap(props.sceneStore())); + const sceneData = props.sceneStore(); // keep it reactive + if (!sceneData) return []; + + const currentIds = Object.keys(sceneData); console.log("Current IDs:", currentIds); let cameraTarget = [0, 0, 0] as [number, number, number]; @@ -302,6 +305,7 @@ export function CubeScene(props: { if (progress < 1) { requestAnimationFrame(animate); + requestRenderIfNotRequested(); } } @@ -923,8 +927,10 @@ export function CubeScene(props: { onClick={() => { if (positionMode() === "grid") { setPositionMode("circle"); + grid.visible = false; } else { setPositionMode("grid"); + grid.visible = true; } }} /> From 71b69c1010fd69d28dfa6b1915fb1bfa2d1be9b3 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:17:38 +0200 Subject: [PATCH 2/6] ui/scene: add promise based create machine callback" --- pkgs/clan-app/ui/src/scene/cubes.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index a81431e4b..cc80dfaab 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -66,7 +66,7 @@ function keyFromPos(pos: [number, number]): string { export function CubeScene(props: { cubesQuery: MachinesQueryResult; - onCreate?: (id: string) => Promise; + onCreate: () => Promise<{ id: string }>; sceneStore: Accessor; setMachinePos: (machineId: string, pos: [number, number]) => void; isLoading: boolean; @@ -635,14 +635,25 @@ export function CubeScene(props: { // - Creates a new cube in "create" mode const onClick = (event: MouseEvent) => { if (worldMode() === "create") { - setWorldMode("view"); + props + .onCreate() + .then(({ id }) => { + //Successfully created machine + const pos = cursorPosition(); + if (!pos) { + console.warn("No position set for new cube"); + return; + } + props.setMachinePos(id, pos); + }) + .catch((error) => { + console.error("Error creating cube:", error); + }) + .finally(() => { + if (initBase) initBase.visible = false; - // res.result.then(() => { - // props.cubesQuery.refetch(); - - // positionMap.set("sara", pos); - // addCube("sara"); - // }); + setWorldMode("view"); + }); } const rect = renderer.domElement.getBoundingClientRect(); From 2d404254dafba963c0066fe44fe642b38e732117 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:18:05 +0200 Subject: [PATCH 3/6] ui/scene: fix initBase visibility --- pkgs/clan-app/ui/src/scene/cubes.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index cc80dfaab..9de64d00f 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -603,6 +603,7 @@ export function CubeScene(props: { CREATE_BASE_COLOR, CREATE_BASE_EMISSIVE, ); + initBase.visible = false; scene.add(initBase); @@ -843,6 +844,8 @@ export function CubeScene(props: { } } }); + + requestRenderIfNotRequested(); }); createEffect( @@ -871,8 +874,10 @@ export function CubeScene(props: { const pos = nextGridPos(); if (!initBase) return; - initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]); - + if (initBase.visible === false && inside) { + initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]); + initBase.visible = true; + } requestRenderIfNotRequested(); }; @@ -884,6 +889,8 @@ export function CubeScene(props: { if (worldMode() !== "create") return; if (!initBase) return; + initBase.visible = true; + const rect = renderer.domElement.getBoundingClientRect(); const mouse = new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, From d01342aa7969e0034755f9761de2564501855fa9 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:18:56 +0200 Subject: [PATCH 4/6] components/modal: add missing properties {mount, class} --- pkgs/clan-app/ui/src/components/Modal/Modal.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx index dc64d9b94..6310d9b14 100644 --- a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx +++ b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx @@ -3,6 +3,7 @@ import { Dialog as KDialog } from "@kobalte/core/dialog"; import "./Modal.css"; import { Typography } from "../Typography/Typography"; import Icon from "../Icon/Icon"; +import cx from "classnames"; export interface ModalContext { close(): void; @@ -13,6 +14,8 @@ export interface ModalProps { title: string; onClose: () => void; children: (ctx: ModalContext) => JSX.Element; + mount?: Node; + class?: string; } export const Modal = (props: ModalProps) => { @@ -20,8 +23,8 @@ export const Modal = (props: ModalProps) => { return ( - - + +
{props.title} From ac96d67f09408d24b49ad4ec0a51eb76b9858e56 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:19:19 +0200 Subject: [PATCH 5/6] components/modal: fix missing onClose call --- pkgs/clan-app/ui/src/components/Modal/Modal.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx index 6310d9b14..a4618c369 100644 --- a/pkgs/clan-app/ui/src/components/Modal/Modal.tsx +++ b/pkgs/clan-app/ui/src/components/Modal/Modal.tsx @@ -29,12 +29,22 @@ export const Modal = (props: ModalProps) => { {props.title} - setOpen(false)}> + { + setOpen(false); + props.onClose(); + }} + >
- {props.children({ close: () => setOpen(false) })} + {props.children({ + close: () => { + setOpen(false); + props.onClose(); + }, + })}
From 5a63eeed4e4258f4fa3cd56e467ca540281ff8b1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 19 Jul 2025 18:19:37 +0200 Subject: [PATCH 6/6] ui/scene: mock create machine modal for testing --- pkgs/clan-app/ui/src/routes/Clan/Clan.css | 9 ++ pkgs/clan-app/ui/src/routes/Clan/Clan.tsx | 104 +++++++++++++++++++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.css b/pkgs/clan-app/ui/src/routes/Clan/Clan.css index 977797521..d74a2c313 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.css +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.css @@ -2,3 +2,12 @@ opacity: 0; transition: opacity 0.5s ease; } + +.create-backdrop { + @apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50; + -webkit-backdrop-filter: blur(4px); +} + +.create-modal { + @apply min-w-96; +} diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index d9383dcff..cd47110a4 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -1,5 +1,5 @@ import { RouteSectionProps } from "@solidjs/router"; -import { Component, JSX } from "solid-js"; +import { Component, JSX, Show, createSignal } from "solid-js"; import { useClanURI } from "@/src/hooks/clan"; import { CubeScene } from "@/src/scene/cubes"; import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries"; @@ -10,6 +10,9 @@ import { Button } from "@/src/components/Button/Button"; import { Splash } from "@/src/scene/splash"; import cx from "classnames"; import "./Clan.css"; +import { Modal } from "@/src/components/Modal/Modal"; +import { TextInput } from "@/src/components/Form/TextInput"; +import { createForm, FieldValues, reset } from "@modular-forms/solid"; export const Clan: Component = (props) => { return ( @@ -27,17 +30,88 @@ export const Clan: Component = (props) => { ); }; +interface CreateFormValues extends FieldValues { + name: string; +} +interface MockProps { + onClose: () => void; + onSubmit: (formValues: CreateFormValues) => void; +} +const MockCreateMachine = (props: MockProps) => { + let container: Node; + + const [form, { Form, Field, FieldArray }] = createForm(); + + return ( +
(container = el)} class="create-backdrop"> + { + reset(form); + props.onClose(); + }} + class="create-modal" + title="Create Machine" + > + {() => ( +
+ + {(field, props) => ( + <> + + + )} + + +
+ + +
+
+ )} +
+
+ ); +}; + const ClanSceneController = () => { const clanURI = useClanURI({ force: true }); - const onCreate = async (id: string) => { + const [dialogHandlers, setDialogHandlers] = createSignal<{ + resolve: ({ id }: { id: string }) => void; + reject: (err: unknown) => void; + } | null>(null); + + const onCreate = async (): Promise<{ id: string }> => { + return new Promise((resolve, reject) => { + setShowModal(true); + setDialogHandlers({ resolve, reject }); + }); + }; + + const sendCreate = async (values: CreateFormValues) => { const api = callApi("create_machine", { opts: { clan_dir: { identifier: clanURI, }, machine: { - name: id, + name: values.name, }, }, }); @@ -49,14 +123,34 @@ const ClanSceneController = () => { // Important: rejects the promise throw new Error(res.errors[0].message); } - return; + return { id: values.name }; }; + const [showModal, setShowModal] = createSignal(false); + return ( {({ query }) => { return ( <> + + { + setShowModal(false); + dialogHandlers()?.reject(new Error("User cancelled")); + }} + onSubmit={async (values) => { + try { + const result = await sendCreate(values); + dialogHandlers()?.resolve(result); + setShowModal(false); + } catch (err) { + dialogHandlers()?.reject(err); + setShowModal(false); + } + }} + /> +
{ produce((s) => { for (const machineId in s.sceneData[clanURI]) { // Reset the position of each machine to [0, 0] + s.sceneData[clanURI] = {}; // Clear the entire object // delete s.sceneData[clanURI][machineId]; } }), @@ -90,6 +185,7 @@ const ClanSceneController = () => {
+