Merge pull request 'ui/cubes: reactive wiring, use orthographic camera' (#4468) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4468
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-nav__title,
|
.md-nav__title,
|
||||||
.md-nav__item.md-nav__item--section>label>span {
|
.md-nav__item.md-nav__item--section > label > span {
|
||||||
color: var(--md-typeset-a-color);
|
color: var(--md-typeset-a-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
version: "0.5"
|
|
||||||
|
|
||||||
processes:
|
|
||||||
# App Dev
|
|
||||||
|
|
||||||
clan-app-ui:
|
|
||||||
namespace: "app"
|
|
||||||
command: |
|
|
||||||
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
|
|
||||||
npm install
|
|
||||||
vite
|
|
||||||
ready_log_line: "VITE"
|
|
||||||
|
|
||||||
clan-app:
|
|
||||||
namespace: "app"
|
|
||||||
command: |
|
|
||||||
cd $(git rev-parse --show-toplevel)/pkgs/clan-app
|
|
||||||
./bin/clan-app --debug --content-uri http://localhost:3000
|
|
||||||
depends_on:
|
|
||||||
clan-app-ui:
|
|
||||||
condition: "process_log_ready"
|
|
||||||
is_foreground: true
|
|
||||||
ready_log_line: "Debug mode enabled"
|
|
||||||
|
|
||||||
# Storybook Dev
|
|
||||||
|
|
||||||
storybook:
|
|
||||||
namespace: "storybook"
|
|
||||||
command: |
|
|
||||||
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
|
|
||||||
npm run storybook-dev -- --ci
|
|
||||||
ready_log_line: "started"
|
|
||||||
|
|
||||||
luakit:
|
|
||||||
namespace: "storybook"
|
|
||||||
command: "luakit http://localhost:6006"
|
|
||||||
depends_on:
|
|
||||||
storybook:
|
|
||||||
condition: "process_log_ready"
|
|
||||||
@@ -63,3 +63,11 @@ export const useMachineID = (): string => {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
return machineIDParam(params);
|
return machineIDParam(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const maybeUseMachineID = (): string | null => {
|
||||||
|
const params = useParams();
|
||||||
|
if (params.machineID === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return machineIDParam(params);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import { RouteSectionProps } from "@solidjs/router";
|
import { RouteSectionProps } from "@solidjs/router";
|
||||||
import { Component, JSX, Show, createSignal, onMount } from "solid-js";
|
import {
|
||||||
import { useClanURI } from "@/src/hooks/clan";
|
Component,
|
||||||
|
JSX,
|
||||||
|
Show,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
on,
|
||||||
|
onMount,
|
||||||
|
} from "solid-js";
|
||||||
|
import {
|
||||||
|
buildMachinePath,
|
||||||
|
maybeUseMachineID,
|
||||||
|
useClanURI,
|
||||||
|
} from "@/src/hooks/clan";
|
||||||
import { CubeScene } from "@/src/scene/cubes";
|
import { CubeScene } from "@/src/scene/cubes";
|
||||||
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
||||||
import { callApi } from "@/src/hooks/api";
|
import { callApi } from "@/src/hooks/api";
|
||||||
@@ -14,6 +27,7 @@ import { Modal } from "@/src/components/Modal/Modal";
|
|||||||
import { TextInput } from "@/src/components/Form/TextInput";
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
||||||
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
|
||||||
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -64,7 +78,7 @@ const MockCreateMachine = (props: MockProps) => {
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div class="flex w-full items-center justify-end gap-4">
|
<div class="mt-4 flex w-full items-center justify-end gap-4">
|
||||||
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -86,6 +100,7 @@ const MockCreateMachine = (props: MockProps) => {
|
|||||||
|
|
||||||
const ClanSceneController = (props: RouteSectionProps) => {
|
const ClanSceneController = (props: RouteSectionProps) => {
|
||||||
const clanURI = useClanURI();
|
const clanURI = useClanURI();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
||||||
resolve: ({ id }: { id: string }) => void;
|
resolve: ({ id }: { id: string }) => void;
|
||||||
@@ -130,6 +145,32 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const onMachineSelect = (ids: Set<string>) => {
|
||||||
|
// Get the first selected ID and navigate to its machine details
|
||||||
|
const selected = ids.values().next().value;
|
||||||
|
if (selected) {
|
||||||
|
navigate(buildMachinePath(clanURI, selected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const machine = createMemo(() => maybeUseMachineID());
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(machine, (machineId) => {
|
||||||
|
if (machineId) {
|
||||||
|
setSelectedIds(() => {
|
||||||
|
const res = new Set<string>();
|
||||||
|
res.add(machineId);
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set<string>());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneDataProvider clanURI={clanURI}>
|
<SceneDataProvider clanURI={clanURI}>
|
||||||
{({ query }) => {
|
{({ query }) => {
|
||||||
@@ -190,6 +231,8 @@ const ClanSceneController = (props: RouteSectionProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CubeScene
|
<CubeScene
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelect={onMachineSelect}
|
||||||
isLoading={query.isLoading}
|
isLoading={query.isLoading}
|
||||||
cubesQuery={query}
|
cubesQuery={query}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function garbageCollectGroup(group: THREE.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFloorPosition(
|
function getFloorPosition(
|
||||||
camera: THREE.PerspectiveCamera,
|
camera: THREE.Camera,
|
||||||
floor: THREE.Object3D,
|
floor: THREE.Object3D,
|
||||||
): [number, number, number] {
|
): [number, number, number] {
|
||||||
const cameraPosition = camera.position.clone();
|
const cameraPosition = camera.position.clone();
|
||||||
@@ -67,13 +67,15 @@ function keyFromPos(pos: [number, number]): string {
|
|||||||
export function CubeScene(props: {
|
export function CubeScene(props: {
|
||||||
cubesQuery: MachinesQueryResult;
|
cubesQuery: MachinesQueryResult;
|
||||||
onCreate: () => Promise<{ id: string }>;
|
onCreate: () => Promise<{ id: string }>;
|
||||||
|
selectedIds: Accessor<Set<string>>;
|
||||||
|
onSelect: (v: Set<string>) => void;
|
||||||
sceneStore: Accessor<SceneData>;
|
sceneStore: Accessor<SceneData>;
|
||||||
setMachinePos: (machineId: string, pos: [number, number]) => void;
|
setMachinePos: (machineId: string, pos: [number, number]) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) {
|
}) {
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let scene: THREE.Scene;
|
let scene: THREE.Scene;
|
||||||
let camera: THREE.PerspectiveCamera;
|
let camera: THREE.OrthographicCamera;
|
||||||
let renderer: THREE.WebGLRenderer;
|
let renderer: THREE.WebGLRenderer;
|
||||||
let floor: THREE.Mesh;
|
let floor: THREE.Mesh;
|
||||||
let controls: MapControls;
|
let controls: MapControls;
|
||||||
@@ -108,7 +110,6 @@ export function CubeScene(props: {
|
|||||||
const [worldMode, setWorldMode] = createSignal<"view" | "create">("view");
|
const [worldMode, setWorldMode] = createSignal<"view" | "create">("view");
|
||||||
|
|
||||||
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
|
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
|
||||||
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const [cameraInfo, setCameraInfo] = createSignal({
|
const [cameraInfo, setCameraInfo] = createSignal({
|
||||||
position: { x: 0, y: 0, z: 0 },
|
position: { x: 0, y: 0, z: 0 },
|
||||||
@@ -117,8 +118,6 @@ export function CubeScene(props: {
|
|||||||
|
|
||||||
// Animation configuration
|
// Animation configuration
|
||||||
const ANIMATION_DURATION = 800; // milliseconds
|
const ANIMATION_DURATION = 800; // milliseconds
|
||||||
const DELETE_ANIMATION_DURATION = 400; // milliseconds
|
|
||||||
const CREATE_ANIMATION_DURATION = 600; // milliseconds
|
|
||||||
|
|
||||||
// Grid configuration
|
// Grid configuration
|
||||||
const GRID_SIZE = 2;
|
const GRID_SIZE = 2;
|
||||||
@@ -146,7 +145,6 @@ export function CubeScene(props: {
|
|||||||
const CREATE_BASE_EMISSIVE = 0xc5fad7;
|
const CREATE_BASE_EMISSIVE = 0xc5fad7;
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log("Direct query data hook");
|
|
||||||
// Update when API updates.
|
// Update when API updates.
|
||||||
if (props.cubesQuery.data) {
|
if (props.cubesQuery.data) {
|
||||||
const actualMachines = Object.keys(props.cubesQuery.data);
|
const actualMachines = Object.keys(props.cubesQuery.data);
|
||||||
@@ -189,7 +187,7 @@ export function CubeScene(props: {
|
|||||||
console.warn("Not animating!");
|
console.warn("Not animating!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Rendering scene...", initBase?.position);
|
console.log("Rendering scene...", camera.toJSON());
|
||||||
|
|
||||||
needsRender = false;
|
needsRender = false;
|
||||||
|
|
||||||
@@ -217,23 +215,53 @@ export function CubeScene(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextGridPos(): [number, number] {
|
function nextGridPos(): [number, number] {
|
||||||
// Scales up to 10*10 grid = 100 positions
|
let x = 0,
|
||||||
// TODO: Make this more scalable and nicer
|
z = 0;
|
||||||
const maxRows = 10; // or dynamic limit if needed
|
let layer = 1;
|
||||||
const maxCols = 10;
|
|
||||||
|
|
||||||
for (let y = 0; y < maxRows; y++) {
|
while (layer < 100) {
|
||||||
for (let x = 0; x < maxCols; x++) {
|
// right
|
||||||
const pos = [x * CUBE_SPACING, y * CUBE_SPACING] as [number, number];
|
for (let i = 0; i < layer; i++) {
|
||||||
|
const pos = [x * CUBE_SPACING, z * CUBE_SPACING] as [number, number];
|
||||||
const key = keyFromPos(pos);
|
const key = keyFromPos(pos);
|
||||||
|
|
||||||
if (!occupiedPositions.has(key)) {
|
if (!occupiedPositions.has(key)) {
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
x += 1;
|
||||||
}
|
}
|
||||||
|
// down
|
||||||
|
for (let i = 0; i < layer; i++) {
|
||||||
|
const pos = [x * CUBE_SPACING, z * CUBE_SPACING] as [number, number];
|
||||||
|
const key = keyFromPos(pos);
|
||||||
|
if (!occupiedPositions.has(key)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
z += 1;
|
||||||
|
}
|
||||||
|
layer++;
|
||||||
|
// left
|
||||||
|
for (let i = 0; i < layer; i++) {
|
||||||
|
const pos = [x * CUBE_SPACING, z * CUBE_SPACING] as [number, number];
|
||||||
|
const key = keyFromPos(pos);
|
||||||
|
if (!occupiedPositions.has(key)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
x -= 1;
|
||||||
|
}
|
||||||
|
// up
|
||||||
|
for (let i = 0; i < layer; i++) {
|
||||||
|
const pos = [x * CUBE_SPACING, z * CUBE_SPACING] as [number, number];
|
||||||
|
const key = keyFromPos(pos);
|
||||||
|
if (!occupiedPositions.has(key)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
z -= 1;
|
||||||
|
}
|
||||||
|
layer++;
|
||||||
}
|
}
|
||||||
|
console.warn("No free grid positions available, returning [0, 0]");
|
||||||
throw new Error("No free grid positions available.");
|
// Fallback if no position was found
|
||||||
|
return [0, 0] as [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circle IDEA:
|
// Circle IDEA:
|
||||||
@@ -331,49 +359,10 @@ export function CubeScene(props: {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelectedCubes(selectedSet: Set<string>) {
|
|
||||||
if (selectedSet.size === 0) return;
|
|
||||||
|
|
||||||
console.log("Deleting cubes:", selectedSet);
|
|
||||||
|
|
||||||
// Start delete animations
|
|
||||||
selectedSet.forEach((id) => {
|
|
||||||
const group = groupMap.get(id);
|
|
||||||
|
|
||||||
if (group) {
|
|
||||||
groupMap.delete(id); // Remove from group map
|
|
||||||
|
|
||||||
const base = group.children.find((child) => child.name === "base");
|
|
||||||
const cube = group.children.find((child) => child.name === "cube");
|
|
||||||
|
|
||||||
if (!base || !cube) {
|
|
||||||
console.warn(`DELETE: Base mesh not found for id: ${id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
setSelectedIds(new Set<string>()); // Clear selection after deletion
|
|
||||||
|
|
||||||
garbageCollectGroup(group); // Clean up geometries and materials
|
|
||||||
scene.remove(group); // Remove from scene
|
|
||||||
groupMap.delete(id); // Remove from group map
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`DELETE: Group not found for id: ${id}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelection(id: string) {
|
function toggleSelection(id: string) {
|
||||||
setSelectedIds((curr) => {
|
const next = new Set<string>();
|
||||||
const next = new Set(curr);
|
next.add(id);
|
||||||
if (next.has(id)) {
|
props.onSelect(next);
|
||||||
next.delete(id);
|
|
||||||
} else {
|
|
||||||
next.add(id);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMeshColors(
|
function updateMeshColors(
|
||||||
@@ -450,7 +439,7 @@ export function CubeScene(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialCameraPosition = { x: 2.8, y: 4, z: -2 };
|
const initialCameraPosition = { x: 20, y: 20, z: 20 };
|
||||||
const initialSphericalCameraPosition = new THREE.Spherical();
|
const initialSphericalCameraPosition = new THREE.Spherical();
|
||||||
initialSphericalCameraPosition.setFromVector3(
|
initialSphericalCameraPosition.setFromVector3(
|
||||||
new THREE.Vector3(
|
new THREE.Vector3(
|
||||||
@@ -465,7 +454,6 @@ export function CubeScene(props: {
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Scene setup
|
// Scene setup
|
||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
scene.fog = new THREE.Fog(0xffffff, 10, 50); //
|
|
||||||
// Transparent background
|
// Transparent background
|
||||||
scene.background = null;
|
scene.background = null;
|
||||||
|
|
||||||
@@ -506,17 +494,30 @@ export function CubeScene(props: {
|
|||||||
bgScene.add(bgMesh);
|
bgScene.add(bgMesh);
|
||||||
|
|
||||||
// Camera setup
|
// Camera setup
|
||||||
camera = new THREE.PerspectiveCamera(
|
// /container!.clientWidth / container!.clientHeight,
|
||||||
75,
|
const aspect = window.innerWidth / window.innerHeight;
|
||||||
container!.clientWidth / container!.clientHeight,
|
const d = 20;
|
||||||
0.1,
|
const zoom = 2.5;
|
||||||
|
camera = new THREE.OrthographicCamera(
|
||||||
|
(-d * aspect) / zoom,
|
||||||
|
(d * aspect) / zoom,
|
||||||
|
d / zoom,
|
||||||
|
-d / zoom,
|
||||||
|
0.001,
|
||||||
1000,
|
1000,
|
||||||
);
|
);
|
||||||
|
camera.zoom = zoom;
|
||||||
|
|
||||||
camera.position.setFromSpherical(initialSphericalCameraPosition);
|
camera.position.setFromSpherical(initialSphericalCameraPosition);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
// Renderer setup
|
// Renderer setup
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
logarithmicDepthBuffer: true,
|
||||||
|
});
|
||||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
@@ -527,25 +528,36 @@ export function CubeScene(props: {
|
|||||||
// Enable the context menu,
|
// Enable the context menu,
|
||||||
// TODO: disable in production
|
// TODO: disable in production
|
||||||
controls.mouseButtons.RIGHT = null;
|
controls.mouseButtons.RIGHT = null;
|
||||||
controls.addEventListener("change", requestRenderIfNotRequested);
|
controls.minZoom = 1.2;
|
||||||
|
controls.maxZoom = 3.5;
|
||||||
|
controls.addEventListener("change", () => {
|
||||||
|
const aspect = container.clientWidth / container.clientHeight;
|
||||||
|
const zoom = camera.zoom;
|
||||||
|
camera.left = (-d * aspect) / zoom;
|
||||||
|
camera.right = (d * aspect) / zoom;
|
||||||
|
camera.top = d / zoom;
|
||||||
|
camera.bottom = -d / zoom;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
requestRenderIfNotRequested();
|
||||||
|
});
|
||||||
|
|
||||||
// Lighting
|
// Lighting
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||||
|
|
||||||
// scene.add(new THREE.DirectionalLightHelper(directionalLight));
|
// scene.add(new THREE.DirectionalLightHelper(directionalLight));
|
||||||
// scene.add(new THREE.CameraHelper(directionalLight.shadow.camera));
|
// scene.add(new THREE.CameraHelper(directionalLight.shadow.camera));
|
||||||
// scene.add(new THREE.CameraHelper(camera));
|
// scene.add(new THREE.CameraHelper(camera));
|
||||||
const lightPos = new THREE.Spherical(
|
const lightPos = new THREE.Spherical(
|
||||||
100,
|
1000,
|
||||||
initialSphericalCameraPosition.phi,
|
initialSphericalCameraPosition.phi - Math.PI / 8,
|
||||||
initialSphericalCameraPosition.theta - Math.PI / 2,
|
initialSphericalCameraPosition.theta - Math.PI / 2,
|
||||||
);
|
);
|
||||||
directionalLight.position.setFromSpherical(lightPos);
|
directionalLight.position.setFromSpherical(lightPos);
|
||||||
directionalLight.target.position.set(0, 0, 0); // Point light at the center
|
directionalLight.target.position.set(0, 0, 0); // Point light at the center
|
||||||
|
|
||||||
// initialSphericalCameraPosition
|
// initialSphericalCameraPosition
|
||||||
directionalLight.castShadow = true;
|
directionalLight.castShadow = true;
|
||||||
|
|
||||||
@@ -555,10 +567,10 @@ export function CubeScene(props: {
|
|||||||
directionalLight.shadow.camera.top = 30;
|
directionalLight.shadow.camera.top = 30;
|
||||||
directionalLight.shadow.camera.bottom = -30;
|
directionalLight.shadow.camera.bottom = -30;
|
||||||
directionalLight.shadow.camera.near = 0.1;
|
directionalLight.shadow.camera.near = 0.1;
|
||||||
directionalLight.shadow.camera.far = 200;
|
directionalLight.shadow.camera.far = 2000;
|
||||||
directionalLight.shadow.mapSize.width = 4096; // Higher resolution for sharper shadows
|
directionalLight.shadow.mapSize.width = 4096; // Higher resolution for sharper shadows
|
||||||
directionalLight.shadow.mapSize.height = 4096;
|
directionalLight.shadow.mapSize.height = 4096;
|
||||||
directionalLight.shadow.radius = 1; // Hard shadows (low radius)
|
directionalLight.shadow.radius = 0; // Hard shadows (low radius)
|
||||||
directionalLight.shadow.blurSamples = 4; // Fewer samples for harder edges
|
directionalLight.shadow.blurSamples = 4; // Fewer samples for harder edges
|
||||||
scene.add(directionalLight);
|
scene.add(directionalLight);
|
||||||
scene.add(directionalLight.target);
|
scene.add(directionalLight.target);
|
||||||
@@ -631,6 +643,17 @@ export function CubeScene(props: {
|
|||||||
// Initial camera info update
|
// Initial camera info update
|
||||||
updateCameraInfo();
|
updateCameraInfo();
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(worldMode, (mode) => {
|
||||||
|
if (mode === "create") {
|
||||||
|
initBase!.visible = true;
|
||||||
|
} else {
|
||||||
|
initBase!.visible = false;
|
||||||
|
}
|
||||||
|
requestRenderIfNotRequested();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Click handler:
|
// Click handler:
|
||||||
// - Select/deselects a cube in "view" mode
|
// - Select/deselects a cube in "view" mode
|
||||||
// - Creates a new cube in "create" mode
|
// - Creates a new cube in "create" mode
|
||||||
@@ -672,7 +695,7 @@ export function CubeScene(props: {
|
|||||||
const id = intersects[0].object.userData.id;
|
const id = intersects[0].object.userData.id;
|
||||||
toggleSelection(id);
|
toggleSelection(id);
|
||||||
} else {
|
} else {
|
||||||
setSelectedIds(new Set<string>()); // Clear selection if clicked outside cubes
|
props.onSelect(new Set<string>()); // Clear selection if clicked outside cubes
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -684,8 +707,14 @@ export function CubeScene(props: {
|
|||||||
|
|
||||||
// Handle window resize
|
// Handle window resize
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
camera.aspect = container.clientWidth / container.clientHeight;
|
const aspect = container.clientWidth / container.clientHeight;
|
||||||
|
const zoom = camera.zoom;
|
||||||
|
camera.left = (-d * aspect) / zoom;
|
||||||
|
camera.right = (d * aspect) / zoom;
|
||||||
|
camera.top = d / zoom;
|
||||||
|
camera.bottom = -d / zoom;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
|
||||||
// Update background shader resolution
|
// Update background shader resolution
|
||||||
@@ -740,18 +769,6 @@ export function CubeScene(props: {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Move into css
|
|
||||||
// createEffect(
|
|
||||||
// on(positionMode, (mode) => {
|
|
||||||
// console.log("Position mode changed:", mode);
|
|
||||||
// if (mode === "circle") {
|
|
||||||
// grid.visible = false; // Hide grid when in circle mode
|
|
||||||
// } else if (mode === "grid") {
|
|
||||||
// grid.visible = true; // Show grid when in grid mode
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
function createCube(
|
function createCube(
|
||||||
gridPosition: [number, number],
|
gridPosition: [number, number],
|
||||||
userData: { id: string },
|
userData: { id: string },
|
||||||
@@ -787,7 +804,6 @@ export function CubeScene(props: {
|
|||||||
// Effect to manage cube meshes - this runs whenever cubes() changes
|
// Effect to manage cube meshes - this runs whenever cubes() changes
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const currentCubes = cubes();
|
const currentCubes = cubes();
|
||||||
console.log("Current cubes:", currentCubes);
|
|
||||||
|
|
||||||
const existing = new Set(groupMap.keys());
|
const existing = new Set(groupMap.keys());
|
||||||
|
|
||||||
@@ -808,7 +824,6 @@ export function CubeScene(props: {
|
|||||||
scene.add(group);
|
scene.add(group);
|
||||||
groupMap.set(cube.id, group);
|
groupMap.set(cube.id, group);
|
||||||
} else {
|
} else {
|
||||||
console.log("Updating existing cube:", cube.id);
|
|
||||||
// Only animate position if not being deleted
|
// Only animate position if not being deleted
|
||||||
const targetPosition = cube.targetPosition || cube.position;
|
const targetPosition = cube.targetPosition || cube.position;
|
||||||
const currentPosition = existingGroup.position.toArray() as [
|
const currentPosition = existingGroup.position.toArray() as [
|
||||||
@@ -849,7 +864,7 @@ export function CubeScene(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(selectedIds, (curr, prev) => {
|
on(props.selectedIds, (curr, prev) => {
|
||||||
console.log("Selected cubes:", curr);
|
console.log("Selected cubes:", curr);
|
||||||
// Update colors of selected cubes
|
// Update colors of selected cubes
|
||||||
updateMeshColors(curr, prev);
|
updateMeshColors(curr, prev);
|
||||||
@@ -933,8 +948,7 @@ export function CubeScene(props: {
|
|||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
name="new-machine"
|
name="new-machine"
|
||||||
icon="NewMachine"
|
icon="NewMachine"
|
||||||
onMouseEnter={onHover(true)}
|
disabled={positionMode() === "circle"}
|
||||||
onMouseLeave={onHover(false)}
|
|
||||||
onClick={onAddClick}
|
onClick={onAddClick}
|
||||||
selected={worldMode() === "create"}
|
selected={worldMode() === "create"}
|
||||||
/>
|
/>
|
||||||
@@ -945,6 +959,7 @@ export function CubeScene(props: {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (positionMode() === "grid") {
|
if (positionMode() === "grid") {
|
||||||
setPositionMode("circle");
|
setPositionMode("circle");
|
||||||
|
setWorldMode("view");
|
||||||
grid.visible = false;
|
grid.visible = false;
|
||||||
} else {
|
} else {
|
||||||
setPositionMode("grid");
|
setPositionMode("grid");
|
||||||
@@ -952,11 +967,7 @@ export function CubeScene(props: {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton name="delete" icon="Trash" />
|
||||||
name="delete"
|
|
||||||
icon="Trash"
|
|
||||||
onClick={() => deleteSelectedCubes(selectedIds())}
|
|
||||||
/>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user