diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 42d83701..bc0be654 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,37 +1,31 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { - "name": "PDS MetaCity Studio", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "dockerComposeFile": "docker-compose.yml", - "service": "devcontainer", - "workspaceFolder": "/workspace", - // Features to add to the dev container. More info: https://cont ainers.dev/features. - "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "20" - }, - "ghcr.io/devcontainers/features/python:1": { - "version": "3.12" - }, - "ghcr.io/devcontainers/features/docker-in-docker:2": {} - }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 3000, - "minio:9090", - "postgres:5432" - ], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install", - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "esbenp.prettier-vscode" - ] - } - } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} \ No newline at end of file + "name": "Metacity Studio", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspace", + // Features to add to the dev container. More info: https://cont ainers.dev/features. + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 3e23c4a3..512c2821 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,34 +1,4 @@ -volumes: - postgres: - services: devcontainer: build: . command: sleep infinity - environment: - DB_HOST: postgres - DB_USERNAME: pds - DB_PASSWORD: pds-pass - DB_DATABASE: pds - MINIO_ENDPOINT: minio - MINIO_ACCESS_KEY: pds - MINIO_SECRET_KEY: pdspdspds - - postgres: - image: postgres:16 - shm_size: 2g - environment: - POSTGRES_USER: pds - POSTGRES_PASSWORD: pds-pass - POSTGRES_DB: pds - PGDATA: /data/postgres - volumes: - - postgres:/data/postgres - - minio: - image: minio/minio - command: minio server --console-address ":9090" - environment: - MINIO_VOLUMES: /data - MINIO_ROOT_USER: pds - MINIO_ROOT_PASSWORD: pdspdspds diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9b8d5147 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/.github/workflows/build-converter.yml b/.github/workflows/build-converter.yml deleted file mode 100644 index ca1f6d81..00000000 --- a/.github/workflows/build-converter.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build Coordinates Converter - -on: - workflow_call: - -jobs: - build: - name: Build and push Docker image - runs-on: ubuntu-latest - - steps: - - name: Checkout the updated source code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to container registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build image and push it to the container registry - uses: docker/build-push-action@v5 - with: - push: true - context: ./coordinates_converter - tags: "ghcr.io/metacitytools/coordinates-converter:latest" - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/test-converter.yml b/.github/workflows/test-converter.yml deleted file mode 100644 index 977ccc04..00000000 --- a/.github/workflows/test-converter.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Test Coordinates Converter - -on: - pull_request: - types: [opened, reopened, synchronize] - paths: - - "coordinates_converter/**" - - ".github/workflows/test-converter.yml" - workflow_call: - -jobs: - test: - name: Run tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - - name: Install dependencies - run: | - cd coordinates_converter - pip install -r requirements.txt - - - name: Run tests - run: | - cd coordinates_converter/tests - pytest -rA diff --git a/.github/workflows/test-studio.yml b/.github/workflows/test-studio.yml index 9435ffc6..ea5dcb91 100644 --- a/.github/workflows/test-studio.yml +++ b/.github/workflows/test-studio.yml @@ -12,34 +12,6 @@ jobs: test: name: Run tests runs-on: ubuntu-latest - - services: - postgres: - image: postgres:16 - ports: - - 5432:5432 - env: - POSTGRES_USER: pds - POSTGRES_PASSWORD: pds-pass - POSTGRES_DB: pds - minio: - image: minio/minio:edge-cicd - options: --health-cmd "curl -s http://localhost:9000/minio/health/live" - ports: - - 9000:9000 - env: - MINIO_ROOT_USER: pds - MINIO_ROOT_PASSWORD: pdspdspds - - env: - DB_HOST: localhost - DB_USERNAME: pds - DB_PASSWORD: pds-pass - DB_DATABASE: pds - MINIO_ENDPOINT: localhost - MINIO_ACCESS_KEY: pds - MINIO_SECRET_KEY: pdspdspds - steps: - uses: actions/checkout@v4 @@ -49,10 +21,7 @@ jobs: node-version: 20 - name: Install dependencies - run: cd studio && npm ci - - - name: Run migrations dependencies - run: cd studio && npm run migrations:run + run: npm ci - - name: Run tests - run: cd studio && npm test + - name: Try building the app + run: npm run build diff --git a/.gitignore b/.gitignore index 30bc1627..ce9b6d65 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -/node_modules \ No newline at end of file +.env +!.env.example +.DS_Store +.react-router +build +node_modules +*.tsbuildinfo diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..207bf937 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/Dockerfile.bun b/Dockerfile.bun new file mode 100644 index 00000000..973038e8 --- /dev/null +++ b/Dockerfile.bun @@ -0,0 +1,25 @@ +FROM oven/bun:1 AS dependencies-env +COPY . /app + +FROM dependencies-env AS development-dependencies-env +COPY ./package.json bun.lockb /app/ +WORKDIR /app +RUN bun i --frozen-lockfile + +FROM dependencies-env AS production-dependencies-env +COPY ./package.json bun.lockb /app/ +WORKDIR /app +RUN bun i --production + +FROM dependencies-env AS build-env +COPY ./package.json bun.lockb /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN bun run build + +FROM dependencies-env +COPY ./package.json bun.lockb /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["bun", "run", "start"] \ No newline at end of file diff --git a/Dockerfile.pnpm b/Dockerfile.pnpm new file mode 100644 index 00000000..57916afc --- /dev/null +++ b/Dockerfile.pnpm @@ -0,0 +1,26 @@ +FROM node:20-alpine AS dependencies-env +RUN npm i -g pnpm +COPY . /app + +FROM dependencies-env AS development-dependencies-env +COPY ./package.json pnpm-lock.yaml /app/ +WORKDIR /app +RUN pnpm i --frozen-lockfile + +FROM dependencies-env AS production-dependencies-env +COPY ./package.json pnpm-lock.yaml /app/ +WORKDIR /app +RUN pnpm i --prod --frozen-lockfile + +FROM dependencies-env AS build-env +COPY ./package.json pnpm-lock.yaml /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN pnpm build + +FROM dependencies-env +COPY ./package.json pnpm-lock.yaml /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["pnpm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 1b37f373..202341f6 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,98 @@ -# 🏑 [Metacity Studio](https://studio.metacity.cc) +# Welcome to React Router! -![Screenshot](./docs/studio.png) +A modern, production-ready template for building full-stack React applications using React Router. -Metacity Studio is an online tool for integrating and visualization of spatial data. +## Features -Running at [studio.metacity.cc](https://studio.metacity.cc) +- πŸš€ Server-side rendering +- ⚑️ Hot Module Replacement (HMR) +- πŸ“¦ Asset bundling and optimization +- πŸ”„ Data loading and mutations +- πŸ”’ TypeScript by default +- πŸŽ‰ TailwindCSS for styling +- πŸ“– [React Router docs](https://reactrouter.com/) -## [Studio](./studio) +## Getting Started -Studio is the frontend and backend of the Metacity Studio application. +### Installation -See the [README in the `studio` directory](./studio/README.md) for more information. +Install the dependencies: -## Supporting services +```bash +npm install +``` -Supporting services can be run using the [docker-compose.yml](./docker-compose.yml) file in the root of the repository. +### Development -### [Coordinates converter](coordinates_converter) +Start the development server with HMR: -Python service to convert models between different coordinate systems. +```bash +npm run dev +``` -See the [README in the `coordinates_converter`](./coordinates_converter/README.md) directory for more information. +Your application will be available at `http://localhost:5173`. -## Development +## Building for Production -### Devcontainer +Create a production build: -This repository is set up to be used with the Visual Studio Code Remote - Containers extension. This allows you to develop in a containerized environment with all the necessary dependencies installed. +```bash +npm run build +``` -The devcontainer includes: -* Node.js 20 -* Python 3.12 -* Docker-in-Docker \ No newline at end of file +## Deployment + +### Docker Deployment + +This template includes three Dockerfiles optimized for different package managers: + +- `Dockerfile` - for npm +- `Dockerfile.pnpm` - for pnpm +- `Dockerfile.bun` - for bun + +To build and run using Docker: + +```bash +# For npm +docker build -t my-app . + +# For pnpm +docker build -f Dockerfile.pnpm -t my-app . + +# For bun +docker build -f Dockerfile.bun -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +β”œβ”€β”€ package.json +β”œβ”€β”€ package-lock.json (or pnpm-lock.yaml, or bun.lockb) +β”œβ”€β”€ build/ +β”‚ β”œβ”€β”€ client/ # Static assets +β”‚ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❀️ using React Router. diff --git a/app/app.css b/app/app.css new file mode 100644 index 00000000..b160883b --- /dev/null +++ b/app/app.css @@ -0,0 +1,46 @@ +/* Base CSS Reset */ +html, +body { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + font-size: 100%; /* Adjust this if needed */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + line-height: 1.5; + min-height: 100vh; +} + +input, +button, +textarea, +select { + font: inherit; /* Use the same font as the rest of the app */ +} + +a { + text-decoration: none; + color: inherit; +} + +ul, +ol { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +:root { + --border-radius: 4px; /* Example base radius for customization */ + --transition-duration: 0.3s; /* Example transition duration */ +} diff --git a/studio/core/components/ContentContainer.tsx b/app/core/components/ContentContainer.tsx similarity index 100% rename from studio/core/components/ContentContainer.tsx rename to app/core/components/ContentContainer.tsx diff --git a/studio/core/components/Empty.tsx b/app/core/components/Empty.tsx similarity index 100% rename from studio/core/components/Empty.tsx rename to app/core/components/Empty.tsx diff --git a/app/core/components/Header.tsx b/app/core/components/Header.tsx new file mode 100644 index 00000000..be490139 --- /dev/null +++ b/app/core/components/Header.tsx @@ -0,0 +1,27 @@ +import { Flex, View } from "@adobe/react-spectrum"; +import { useUser } from "@core/hooks/useUser"; + +type HeaderProps = { + children?: React.ReactNode; +}; + +export default function Header(props: HeaderProps) { + const { children } = props; + const user = useUser(); + + return ( + + + {children} + + + ); +} diff --git a/studio/core/components/PositioningContainer.tsx b/app/core/components/PositioningContainer.tsx similarity index 100% rename from studio/core/components/PositioningContainer.tsx rename to app/core/components/PositioningContainer.tsx diff --git a/app/core/components/SidebarHeader.tsx b/app/core/components/SidebarHeader.tsx new file mode 100644 index 00000000..bc4b7a32 --- /dev/null +++ b/app/core/components/SidebarHeader.tsx @@ -0,0 +1,20 @@ +import { Flex, Heading, View } from "@adobe/react-spectrum"; +import { ReactNode } from "react"; + +type SidebarHeaderProps = { + title: string; + children?: ReactNode; +}; + +export default function SidebarHeader({ children, title }: SidebarHeaderProps) { + return ( + + + + {title} + + {children} + + + ); +} diff --git a/app/core/defaults.ts b/app/core/defaults.ts new file mode 100644 index 00000000..e7132a4f --- /dev/null +++ b/app/core/defaults.ts @@ -0,0 +1,5 @@ +import { SpectrumToastOptions } from "@react-spectrum/toast"; + +export const toasterOptions: SpectrumToastOptions = { + timeout: 5000, +}; diff --git a/studio/core/hooks/useColorScheme.ts b/app/core/hooks/useColorScheme.ts similarity index 100% rename from studio/core/hooks/useColorScheme.ts rename to app/core/hooks/useColorScheme.ts diff --git a/studio/core/hooks/useMutation.ts b/app/core/hooks/useMutation.ts similarity index 100% rename from studio/core/hooks/useMutation.ts rename to app/core/hooks/useMutation.ts diff --git a/studio/core/hooks/useQuery.ts b/app/core/hooks/useQuery.ts similarity index 100% rename from studio/core/hooks/useQuery.ts rename to app/core/hooks/useQuery.ts diff --git a/app/core/hooks/useUser.ts b/app/core/hooks/useUser.ts new file mode 100644 index 00000000..6f1cccff --- /dev/null +++ b/app/core/hooks/useUser.ts @@ -0,0 +1,3 @@ +export const useUser = () => { + return null; +}; diff --git a/studio/core/icons/CubeEmpty.tsx b/app/core/icons/CubeEmpty.tsx similarity index 100% rename from studio/core/icons/CubeEmpty.tsx rename to app/core/icons/CubeEmpty.tsx diff --git a/studio/core/icons/CubeLeft.tsx b/app/core/icons/CubeLeft.tsx similarity index 100% rename from studio/core/icons/CubeLeft.tsx rename to app/core/icons/CubeLeft.tsx diff --git a/studio/core/icons/CubeRight.tsx b/app/core/icons/CubeRight.tsx similarity index 100% rename from studio/core/icons/CubeRight.tsx rename to app/core/icons/CubeRight.tsx diff --git a/studio/core/icons/CubeTop.tsx b/app/core/icons/CubeTop.tsx similarity index 100% rename from studio/core/icons/CubeTop.tsx rename to app/core/icons/CubeTop.tsx diff --git a/studio/core/icons/Icon.tsx b/app/core/icons/Icon.tsx similarity index 100% rename from studio/core/icons/Icon.tsx rename to app/core/icons/Icon.tsx diff --git a/studio/core/icons/MdiArrowRightBold.tsx b/app/core/icons/MdiArrowRightBold.tsx similarity index 100% rename from studio/core/icons/MdiArrowRightBold.tsx rename to app/core/icons/MdiArrowRightBold.tsx diff --git a/studio/core/icons/MdiArrowSplitVertical.tsx b/app/core/icons/MdiArrowSplitVertical.tsx similarity index 100% rename from studio/core/icons/MdiArrowSplitVertical.tsx rename to app/core/icons/MdiArrowSplitVertical.tsx diff --git a/studio/core/icons/MdiCube.tsx b/app/core/icons/MdiCube.tsx similarity index 100% rename from studio/core/icons/MdiCube.tsx rename to app/core/icons/MdiCube.tsx diff --git a/studio/core/icons/MdiExport.tsx b/app/core/icons/MdiExport.tsx similarity index 100% rename from studio/core/icons/MdiExport.tsx rename to app/core/icons/MdiExport.tsx diff --git a/studio/core/icons/MdiEye.tsx b/app/core/icons/MdiEye.tsx similarity index 100% rename from studio/core/icons/MdiEye.tsx rename to app/core/icons/MdiEye.tsx diff --git a/studio/core/icons/MdiEyeOff.tsx b/app/core/icons/MdiEyeOff.tsx similarity index 100% rename from studio/core/icons/MdiEyeOff.tsx rename to app/core/icons/MdiEyeOff.tsx diff --git a/studio/core/icons/MdiMoonWaningCrecnet.tsx b/app/core/icons/MdiMoonWaningCrecnet.tsx similarity index 100% rename from studio/core/icons/MdiMoonWaningCrecnet.tsx rename to app/core/icons/MdiMoonWaningCrecnet.tsx diff --git a/studio/core/icons/MdiPalette.tsx b/app/core/icons/MdiPalette.tsx similarity index 100% rename from studio/core/icons/MdiPalette.tsx rename to app/core/icons/MdiPalette.tsx diff --git a/studio/core/icons/MdiRename.tsx b/app/core/icons/MdiRename.tsx similarity index 100% rename from studio/core/icons/MdiRename.tsx rename to app/core/icons/MdiRename.tsx diff --git a/studio/core/icons/MdiRulerSquare.tsx b/app/core/icons/MdiRulerSquare.tsx similarity index 100% rename from studio/core/icons/MdiRulerSquare.tsx rename to app/core/icons/MdiRulerSquare.tsx diff --git a/studio/core/icons/MdiSelectAll.tsx b/app/core/icons/MdiSelectAll.tsx similarity index 100% rename from studio/core/icons/MdiSelectAll.tsx rename to app/core/icons/MdiSelectAll.tsx diff --git a/studio/core/icons/MdiSelectRemove.tsx b/app/core/icons/MdiSelectRemove.tsx similarity index 100% rename from studio/core/icons/MdiSelectRemove.tsx rename to app/core/icons/MdiSelectRemove.tsx diff --git a/studio/core/icons/MdiSortAscending.tsx b/app/core/icons/MdiSortAscending.tsx similarity index 100% rename from studio/core/icons/MdiSortAscending.tsx rename to app/core/icons/MdiSortAscending.tsx diff --git a/studio/core/icons/MdiTable.tsx b/app/core/icons/MdiTable.tsx similarity index 100% rename from studio/core/icons/MdiTable.tsx rename to app/core/icons/MdiTable.tsx diff --git a/studio/core/icons/MdiTrash.tsx b/app/core/icons/MdiTrash.tsx similarity index 100% rename from studio/core/icons/MdiTrash.tsx rename to app/core/icons/MdiTrash.tsx diff --git a/studio/core/icons/MdiWhiteBalanceSunny.tsx b/app/core/icons/MdiWhiteBalanceSunny.tsx similarity index 100% rename from studio/core/icons/MdiWhiteBalanceSunny.tsx rename to app/core/icons/MdiWhiteBalanceSunny.tsx diff --git a/studio/core/icons/TriangleEmpty.tsx b/app/core/icons/TriangleEmpty.tsx similarity index 100% rename from studio/core/icons/TriangleEmpty.tsx rename to app/core/icons/TriangleEmpty.tsx diff --git a/studio/core/icons/TriangleFull.tsx b/app/core/icons/TriangleFull.tsx similarity index 100% rename from studio/core/icons/TriangleFull.tsx rename to app/core/icons/TriangleFull.tsx diff --git a/studio/core/icons/TriangleFullFilled.tsx b/app/core/icons/TriangleFullFilled.tsx similarity index 100% rename from studio/core/icons/TriangleFullFilled.tsx rename to app/core/icons/TriangleFullFilled.tsx diff --git a/app/core/providers/ClientProvider.tsx b/app/core/providers/ClientProvider.tsx new file mode 100644 index 00000000..214e545a --- /dev/null +++ b/app/core/providers/ClientProvider.tsx @@ -0,0 +1,32 @@ +import { defaultTheme, Provider } from "@adobe/react-spectrum"; +import { createContext, Dispatch, SetStateAction, useState } from "react"; + +type ClientProvidersProps = Readonly<{ + children: React.ReactNode; +}>; + +type CoreContextProps = { + setColorScheme: Dispatch>; +}; + +export const CoreContext = createContext({} as CoreContextProps); + +export function ClientProviders(props: ClientProvidersProps) { + const { children } = props; + + //get OS default color scheme + let colorScheme: "light" | "dark" = "light"; + if (typeof window !== "undefined") { + colorScheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + + const [colorSchemeState, setColorSchemeState] = useState<"light" | "dark">(colorScheme); + + return ( + + + {children} + + + ); +} diff --git a/app/entry.client.tsx b/app/entry.client.tsx new file mode 100644 index 00000000..08ab4ec3 --- /dev/null +++ b/app/entry.client.tsx @@ -0,0 +1,12 @@ +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/studio/features/bananagl/bananagl.ts b/app/features/bananagl/bananagl.ts similarity index 100% rename from studio/features/bananagl/bananagl.ts rename to app/features/bananagl/bananagl.ts diff --git a/old/studio/src/bananagl/camera/camera.ts b/app/features/bananagl/camera/camera.ts similarity index 100% rename from old/studio/src/bananagl/camera/camera.ts rename to app/features/bananagl/camera/camera.ts diff --git a/studio/features/bananagl/camera/cameraInterface.ts b/app/features/bananagl/camera/cameraInterface.ts similarity index 100% rename from studio/features/bananagl/camera/cameraInterface.ts rename to app/features/bananagl/camera/cameraInterface.ts diff --git a/studio/features/bananagl/camera/cameraLock.ts b/app/features/bananagl/camera/cameraLock.ts similarity index 100% rename from studio/features/bananagl/camera/cameraLock.ts rename to app/features/bananagl/camera/cameraLock.ts diff --git a/studio/features/bananagl/camera/cameraView.ts b/app/features/bananagl/camera/cameraView.ts similarity index 100% rename from studio/features/bananagl/camera/cameraView.ts rename to app/features/bananagl/camera/cameraView.ts diff --git a/old/studio/src/bananagl/models/attribute.ts b/app/features/bananagl/models/attribute.ts similarity index 100% rename from old/studio/src/bananagl/models/attribute.ts rename to app/features/bananagl/models/attribute.ts diff --git a/studio/features/bananagl/models/attributes.ts b/app/features/bananagl/models/attributes.ts similarity index 100% rename from studio/features/bananagl/models/attributes.ts rename to app/features/bananagl/models/attributes.ts diff --git a/app/features/bananagl/models/buffer.ts b/app/features/bananagl/models/buffer.ts new file mode 100644 index 00000000..922bc2b1 --- /dev/null +++ b/app/features/bananagl/models/buffer.ts @@ -0,0 +1,149 @@ +import { mat2, mat3, mat4, vec2, vec3, vec4 } from "gl-matrix"; + +import { TypedArray } from "@bananagl/shaders/shader"; + +export function cloneTypedArrayWithSize(arr: TypedArray, size: number) { + const newArr = new (arr.constructor as any)(size); + return newArr; +} + +interface ConstructableTypedArray { + new (buffer: ArrayBuffer): TypedArray; +} + +export class Buffer { + buffer: WebGLBuffer | null = null; + private partsToUpdate_: [number, number][] = []; + constructor(public data: TypedArray) {} + + protected setup(gl: WebGL2RenderingContext) { + const buffer = gl.createBuffer(); + if (!buffer) throw new Error("Failed to create buffer"); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, this.data, gl.STATIC_DRAW); + this.buffer = buffer; + } + + bind(gl: WebGL2RenderingContext): asserts this is { buffer: WebGLBuffer } { + if (!this.buffer) this.setup(gl); + else gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + } + + toUpdate(start?: number, end?: number) { + if (start === undefined) start = 0; + if (end === undefined) end = this.data.length; + this.partsToUpdate_.push([start, end]); + } + + get needsUpdate() { + return this.partsToUpdate_.length > 0; + } + + get active() { + return this.buffer !== null; + } + + update(gl: WebGL2RenderingContext) { + if (!this.buffer) throw new Error("Buffer not setup"); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + for (const [start, end] of this.partsToUpdate_) { + gl.bufferSubData(gl.ARRAY_BUFFER, start * this.data.BYTES_PER_ELEMENT, this.data.subarray(start, end)); + } + this.partsToUpdate_.length = 0; + } + + getDataType(gl: WebGL2RenderingContext) { + if (this.data instanceof Float32Array) return gl.FLOAT; + else if (this.data instanceof Uint8Array) return gl.UNSIGNED_BYTE; + else if (this.data instanceof Int16Array) return gl.SHORT; + else if (this.data instanceof Int32Array) return gl.INT; + else if (this.data instanceof Int8Array) return gl.BYTE; + else throw new Error("Unsupported data type"); + } + + getView(typeConstructor: ConstructableTypedArray) { + return new typeConstructor(this.data.buffer as ArrayBuffer); + } + + get BYTES_PER_ELEMENT() { + return this.data.BYTES_PER_ELEMENT; + } + + get bytesAllocated() { + return this.data.length * this.data.BYTES_PER_ELEMENT; + } + + applyMatrix(matrix: mat2 | mat3 | mat4, size: number) { + if (matrix.length === 4 && size === 2) { + this.applyMatrix2(matrix); + } else if (matrix.length === 9 && size === 3) { + this.applyMatrix3(matrix); + } else if (matrix.length === 16 && (size === 4 || size === 3)) { + this.applyMatrix4(matrix, size); + } else { + throw new Error(`Invalid combination of matrix (${matrix.length}) and element (${size}) size.`); + } + } + + swap(index1: number, index2: number, swapArr: TypedArray) { + swapArr.set(this.data.subarray(index1, index1 + swapArr.length)); + this.data.set(this.data.subarray(index2, index2 + swapArr.length), index1); + this.data.set(swapArr, index2); + } + + private applyMatrix2(matrix: mat2) { + for (let i = 0; i < this.data.length; i += 2) { + vec2.transformMat2(this.data.subarray(i, i + 2) as vec2, this.data.subarray(i, i + 2) as vec2, matrix); + } + } + + private applyMatrix3(matrix: mat3) { + for (let i = 0; i < this.data.length; i += 3) { + vec3.transformMat3(this.data.subarray(i, i + 3) as vec3, this.data.subarray(i, i + 3) as vec3, matrix); + } + } + + private applyMatrix4(matrix: mat4, size: number) { + for (let i = 0; i < this.data.length; i += size) { + if (size === 3) { + vec3.transformMat4(this.data.subarray(i, i + 3) as vec3, this.data.subarray(i, i + 3) as vec3, matrix); + } else if (size === 4) { + vec4.transformMat4(this.data.subarray(i, i + 4) as vec4, this.data.subarray(i, i + 4) as vec4, matrix); + } + } + } + + dispose(gl: WebGL2RenderingContext) { + if (this.buffer) gl.deleteBuffer(this.buffer); + this.buffer = null; + this.data = null as any; + } +} + +export class ElementBuffer extends Buffer { + buffer: WebGLBuffer | null = null; + constructor(public data: Uint16Array | Uint32Array) { + super(data); + } + + protected setup(gl: WebGL2RenderingContext) { + const buffer = gl.createBuffer(); + if (!buffer) throw new Error("Failed to create buffer"); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.data, gl.STATIC_DRAW); + this.buffer = buffer; + } + + bind(gl: WebGL2RenderingContext): asserts this is { buffer: WebGLBuffer } { + if (!this.buffer) this.setup(gl); + else gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffer); + } + + get BYTES_PER_ELEMENT() { + return this.data.BYTES_PER_ELEMENT; + } + + get bytesAllocated() { + return this.data.length * this.data.BYTES_PER_ELEMENT; + } +} diff --git a/old/studio/src/bananagl/models/model.ts b/app/features/bananagl/models/model.ts similarity index 100% rename from old/studio/src/bananagl/models/model.ts rename to app/features/bananagl/models/model.ts diff --git a/studio/features/bananagl/models/pickable.ts b/app/features/bananagl/models/pickable.ts similarity index 100% rename from studio/features/bananagl/models/pickable.ts rename to app/features/bananagl/models/pickable.ts diff --git a/old/studio/src/bananagl/models/renderable.ts b/app/features/bananagl/models/renderable.ts similarity index 100% rename from old/studio/src/bananagl/models/renderable.ts rename to app/features/bananagl/models/renderable.ts diff --git a/old/studio/src/bananagl/models/selectable.ts b/app/features/bananagl/models/selectable.ts similarity index 100% rename from old/studio/src/bananagl/models/selectable.ts rename to app/features/bananagl/models/selectable.ts diff --git a/old/studio/src/bananagl/picking/bbox.ts b/app/features/bananagl/picking/bbox.ts similarity index 100% rename from old/studio/src/bananagl/picking/bbox.ts rename to app/features/bananagl/picking/bbox.ts diff --git a/old/studio/src/bananagl/picking/bvh.ts b/app/features/bananagl/picking/bvh.ts similarity index 100% rename from old/studio/src/bananagl/picking/bvh.ts rename to app/features/bananagl/picking/bvh.ts diff --git a/studio/features/bananagl/picking/picker.ts b/app/features/bananagl/picking/picker.ts similarity index 100% rename from studio/features/bananagl/picking/picker.ts rename to app/features/bananagl/picking/picker.ts diff --git a/old/studio/src/bananagl/picking/ray.ts b/app/features/bananagl/picking/ray.ts similarity index 100% rename from old/studio/src/bananagl/picking/ray.ts rename to app/features/bananagl/picking/ray.ts diff --git a/old/studio/src/bananagl/picking/rect.ts b/app/features/bananagl/picking/rect.ts similarity index 100% rename from old/studio/src/bananagl/picking/rect.ts rename to app/features/bananagl/picking/rect.ts diff --git a/studio/features/bananagl/picking/triangles/build.ts b/app/features/bananagl/picking/triangles/build.ts similarity index 80% rename from studio/features/bananagl/picking/triangles/build.ts rename to app/features/bananagl/picking/triangles/build.ts index 9fe0b78a..7ccd1055 100644 --- a/studio/features/bananagl/picking/triangles/build.ts +++ b/app/features/bananagl/picking/triangles/build.ts @@ -1,19 +1,15 @@ import { Attribute } from "@bananagl/bananagl"; import { BVHNode } from "../bvh"; -import { - BuilderOutput, - fromTransferable, - getTrasferables, - reconstructBBoxes, - toTransferable, -} from "./transform"; +import { BuilderOutput, fromTransferable, getTrasferables, reconstructBBoxes, toTransferable } from "./transform"; export async function buildBVHInWorker(position: Attribute, attr: Attribute[]) { const data = toTransferable(position, attr); const transferables = getTrasferables(data); return new Promise((resolve) => { - const worker = new Worker(new URL("./build.worker.ts", import.meta.url)); + const worker = new Worker(new URL("./build.worker.ts", import.meta.url), { + type: "module", + }); worker.onmessage = (e) => { const data = e.data as BuilderOutput; fromTransferable(position, attr, data.data); diff --git a/app/features/bananagl/picking/triangles/build.worker.ts b/app/features/bananagl/picking/triangles/build.worker.ts new file mode 100644 index 00000000..8b741413 --- /dev/null +++ b/app/features/bananagl/picking/triangles/build.worker.ts @@ -0,0 +1,34 @@ +import { Attribute } from "@bananagl/models/attribute"; +import { Buffer } from "@bananagl/models/buffer"; + +import { TriangleBVHBuilder } from "./builder"; +import { BuilderOutput, TriangleBuildInput, getTrasferables, toTransferable } from "./transform"; + +self.onmessage = (e) => { + const data = e.data as TriangleBuildInput; + const { position, attrs } = toAttributes(data); + const builder = new TriangleBVHBuilder(position, attrs); + const returnedAttrs = toTransferable(position, attrs); + const transferables = getTrasferables(returnedAttrs); + const result: BuilderOutput = { + data: returnedAttrs, + rootNode: builder.root, + }; + (self as any).postMessage(result, transferables); +}; + +function toAttributes(data: TriangleBuildInput) { + return { + position: new Attribute( + data.position.name, + new Buffer(data.position.buffer.data), + data.position.size, + data.position.normalized, + data.position.stride, + data.position.offset + ), + attrs: data.attrs.map( + (a) => new Attribute(a.name, new Buffer(a.buffer.data), a.size, a.normalized, a.stride, a.offset) + ), + }; +} diff --git a/old/studio/src/bananagl/picking/triangles/builder.ts b/app/features/bananagl/picking/triangles/builder.ts similarity index 100% rename from old/studio/src/bananagl/picking/triangles/builder.ts rename to app/features/bananagl/picking/triangles/builder.ts diff --git a/old/studio/src/bananagl/picking/triangles/bvh.triangle.ts b/app/features/bananagl/picking/triangles/bvh.triangle.ts similarity index 100% rename from old/studio/src/bananagl/picking/triangles/bvh.triangle.ts rename to app/features/bananagl/picking/triangles/bvh.triangle.ts diff --git a/old/studio/src/bananagl/picking/triangles/transform.ts b/app/features/bananagl/picking/triangles/transform.ts similarity index 100% rename from old/studio/src/bananagl/picking/triangles/transform.ts rename to app/features/bananagl/picking/triangles/transform.ts diff --git a/studio/features/bananagl/renderer/pass.ts b/app/features/bananagl/renderer/pass.ts similarity index 100% rename from studio/features/bananagl/renderer/pass.ts rename to app/features/bananagl/renderer/pass.ts diff --git a/studio/features/bananagl/renderer/renderer.ts b/app/features/bananagl/renderer/renderer.ts similarity index 100% rename from studio/features/bananagl/renderer/renderer.ts rename to app/features/bananagl/renderer/renderer.ts diff --git a/old/studio/src/bananagl/renderer/setup.ts b/app/features/bananagl/renderer/setup.ts similarity index 100% rename from old/studio/src/bananagl/renderer/setup.ts rename to app/features/bananagl/renderer/setup.ts diff --git a/old/studio/src/bananagl/scene/scene.ts b/app/features/bananagl/scene/scene.ts similarity index 100% rename from old/studio/src/bananagl/scene/scene.ts rename to app/features/bananagl/scene/scene.ts diff --git a/old/studio/src/bananagl/shaders/clone.ts b/app/features/bananagl/shaders/clone.ts similarity index 100% rename from old/studio/src/bananagl/shaders/clone.ts rename to app/features/bananagl/shaders/clone.ts diff --git a/old/studio/src/bananagl/shaders/errors.ts b/app/features/bananagl/shaders/errors.ts similarity index 100% rename from old/studio/src/bananagl/shaders/errors.ts rename to app/features/bananagl/shaders/errors.ts diff --git a/old/studio/src/bananagl/shaders/shader.ts b/app/features/bananagl/shaders/shader.ts similarity index 100% rename from old/studio/src/bananagl/shaders/shader.ts rename to app/features/bananagl/shaders/shader.ts diff --git a/old/studio/src/bananagl/shaders/uniforms.ts b/app/features/bananagl/shaders/uniforms.ts similarity index 100% rename from old/studio/src/bananagl/shaders/uniforms.ts rename to app/features/bananagl/shaders/uniforms.ts diff --git a/old/studio/src/bananagl/utils/profiler.ts b/app/features/bananagl/utils/profiler.ts similarity index 100% rename from old/studio/src/bananagl/utils/profiler.ts rename to app/features/bananagl/utils/profiler.ts diff --git a/studio/features/bananagl/window/controls.ts b/app/features/bananagl/window/controls.ts similarity index 100% rename from studio/features/bananagl/window/controls.ts rename to app/features/bananagl/window/controls.ts diff --git a/studio/features/bananagl/window/controls/calls.ts b/app/features/bananagl/window/controls/calls.ts similarity index 100% rename from studio/features/bananagl/window/controls/calls.ts rename to app/features/bananagl/window/controls/calls.ts diff --git a/studio/features/bananagl/window/controls/keyboard.ts b/app/features/bananagl/window/controls/keyboard.ts similarity index 100% rename from studio/features/bananagl/window/controls/keyboard.ts rename to app/features/bananagl/window/controls/keyboard.ts diff --git a/studio/features/bananagl/window/controls/mouse.ts b/app/features/bananagl/window/controls/mouse.ts similarity index 100% rename from studio/features/bananagl/window/controls/mouse.ts rename to app/features/bananagl/window/controls/mouse.ts diff --git a/studio/features/bananagl/window/controls/selection.ts b/app/features/bananagl/window/controls/selection.ts similarity index 100% rename from studio/features/bananagl/window/controls/selection.ts rename to app/features/bananagl/window/controls/selection.ts diff --git a/old/studio/src/bananagl/window/view.ts b/app/features/bananagl/window/view.ts similarity index 100% rename from old/studio/src/bananagl/window/view.ts rename to app/features/bananagl/window/view.ts diff --git a/studio/features/bananagl/window/window.ts b/app/features/bananagl/window/window.ts similarity index 100% rename from studio/features/bananagl/window/window.ts rename to app/features/bananagl/window/window.ts diff --git a/studio/features/editor-metadata/components/EditorMetadataColumns.tsx b/app/features/data-columns/components/EditorColumns.tsx similarity index 73% rename from studio/features/editor-metadata/components/EditorMetadataColumns.tsx rename to app/features/data-columns/components/EditorColumns.tsx index 343011d8..db0c17a9 100644 --- a/studio/features/editor-metadata/components/EditorMetadataColumns.tsx +++ b/app/features/data-columns/components/EditorColumns.tsx @@ -1,13 +1,9 @@ -"use client"; - import { ActionBar, ActionBarContainer, - ActionButton, ActionGroup, AlertDialog, DialogContainer, - DialogTrigger, Flex, Item, Key, @@ -18,43 +14,40 @@ import { TooltipTrigger, View, } from "@adobe/react-spectrum"; - +import { NoData } from "@core/components/Empty"; import { PositioningContainer } from "@core/components/PositioningContainer"; +import SidebarHeader from "@core/components/SidebarHeader"; import { MdiRename } from "@core/icons/MdiRename"; import { MdiTrash } from "@core/icons/MdiTrash"; -import { useEditorContext } from "@features/editor/hooks/useEditorContext"; +import useMetadataContext from "@features/metadata/hooks/useMetadataContext"; +import useMetadataEdits from "@features/metadata/hooks/useMetadataEdits"; import { useCallback, useState } from "react"; -import useMetadataContext from "../hooks/useMetadataContext"; -import useMetadataEdits from "../hooks/useMetadataEdits"; -import AddColumnDialog from "./EditorMetadataAddColumnDialog"; -import { RenameColumnDialog } from "./EditorMetadataRenameColumnDialog"; +import { RenameColumnDialog } from "./EditorRenameColumnDialog"; -export default function EditorMetadataColumns() { - const { selection } = useEditorContext(); +export default function EditorColumns() { + return ( + + + + + + ); +} + +function EditorColumnsList() { const { columns } = useMetadataContext(); const [columnsToDelete, setColumnsToDelete] = useState([]); const [columnToRename, setColumnToRename] = useState(); - const { assignValue, deleteColumns, renameColumn } = useMetadataEdits(); - - const handleCreateColumn = useCallback( - ( - columnName: string, - defaultValue: string | number, - type: "string" | "number", - ) => { - assignValue(defaultValue, columnName, type); - }, - [assignValue], - ); + const { deleteColumns, renameColumn } = useMetadataEdits(); const handleRenameColumn = useCallback( (newColumnName: string) => { if (!columnToRename) return; renameColumn(columnToRename, newColumnName); }, - [renameColumn, columnToRename], + [renameColumn, columnToRename] ); const handleDeleteColumns = useCallback(() => { @@ -83,39 +76,25 @@ export default function EditorMetadataColumns() { break; } }, - [selectedKeys], + [selectedKeys] ); return ( - - - - - - Add Column - - {(close) => ( - - )} - - - - + + + } onSelectionChange={(keys) => { if (keys === "all") { setSelectedKeys(columns.map((item) => item.key)); @@ -126,11 +105,14 @@ export default function EditorMetadataColumns() { > {(item) => ( - {item.key} - handleItemAction(key, item.key)} + + {item.key} + + handleItemAction(key, item.key)}> @@ -195,8 +177,7 @@ export default function EditorMetadataColumns() { fontSize: "0.9rem", }} > - This action cannot be undone. All values assigned to the columns - will be removed. + This action cannot be undone. All values assigned to the columns will be removed. )} diff --git a/studio/features/editor-metadata/components/EditorMetadataRenameColumnDialog.tsx b/app/features/data-columns/components/EditorRenameColumnDialog.tsx similarity index 100% rename from studio/features/editor-metadata/components/EditorMetadataRenameColumnDialog.tsx rename to app/features/data-columns/components/EditorRenameColumnDialog.tsx diff --git a/studio/features/editor/components/Canvas/Canvas.tsx b/app/features/editor/components/Canvas/Canvas.tsx similarity index 99% rename from studio/features/editor/components/Canvas/Canvas.tsx rename to app/features/editor/components/Canvas/Canvas.tsx index 7c3b6dc8..c6d03115 100644 --- a/studio/features/editor/components/Canvas/Canvas.tsx +++ b/app/features/editor/components/Canvas/Canvas.tsx @@ -1,5 +1,3 @@ -"use client"; - import * as GL from "@bananagl/bananagl"; import { EditorModel } from "@editor/data/EditorModel"; import { useRenderer } from "@editor/hooks/useRender"; diff --git a/studio/features/editor/components/Canvas/CanvasWrapper.tsx b/app/features/editor/components/Canvas/CanvasWrapper.tsx similarity index 92% rename from studio/features/editor/components/Canvas/CanvasWrapper.tsx rename to app/features/editor/components/Canvas/CanvasWrapper.tsx index bc192346..67e2bcee 100644 --- a/studio/features/editor/components/Canvas/CanvasWrapper.tsx +++ b/app/features/editor/components/Canvas/CanvasWrapper.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useProvider } from "@adobe/react-spectrum"; import * as GL from "@bananagl/bananagl"; import { GridModel } from "@editor/data/GridModel"; @@ -70,11 +68,5 @@ export function CanvasWrapper() { setTooltip(null); }; - return ( - - ); + return ; } diff --git a/studio/features/editor/components/Canvas/TooltipOverlay.tsx b/app/features/editor/components/Canvas/TooltipOverlay.tsx similarity index 100% rename from studio/features/editor/components/Canvas/TooltipOverlay.tsx rename to app/features/editor/components/Canvas/TooltipOverlay.tsx diff --git a/studio/features/editor/components/Editor.css b/app/features/editor/components/Editor.css similarity index 100% rename from studio/features/editor/components/Editor.css rename to app/features/editor/components/Editor.css diff --git a/studio/features/editor/components/Editor.tsx b/app/features/editor/components/Editor.tsx similarity index 53% rename from studio/features/editor/components/Editor.tsx rename to app/features/editor/components/Editor.tsx index 6cff9b2e..8015eddc 100644 --- a/studio/features/editor/components/Editor.tsx +++ b/app/features/editor/components/Editor.tsx @@ -1,13 +1,4 @@ -"use client"; - -import { - Grid, - Item, - TabList, - TabPanels, - Tabs, - View, -} from "@adobe/react-spectrum"; +import { Item, TabList, TabPanels, Tabs, View } from "@adobe/react-spectrum"; import { ToastContainer } from "@react-spectrum/toast"; //import Brush from "@spectrum-icons/workflow/Brush"; import { PositioningContainer } from "@core/components/PositioningContainer"; @@ -15,70 +6,42 @@ import { MdiCube } from "@core/icons/MdiCube"; import { MdiExport } from "@core/icons/MdiExport"; import { MdiPalette } from "@core/icons/MdiPalette"; import { MdiTable } from "@core/icons/MdiTable"; -import EditorExports from "@features/editor-exports/components/EditorExports"; -import EditorColumns from "@features/editor-metadata/components/EditorColumns"; -import EditorStyle from "@features/editor-metadata/components/EditorStyle"; -import useMetadataModelStyle from "@features/editor-metadata/hooks/useMetadataModelStyle"; -import EditorModels from "@features/editor-models/components/EditorModels"; -import ActiveColumnToolbar from "@features/editor-toolbar/components/ActiveColumnToolbar"; -import CameraViewToolbar from "@features/editor-toolbar/components/CameraViewToolbar"; -import ColorSchemeToolbar from "@features/editor-toolbar/components/ColorSchemeToolbar"; -import ProjectionToolbar from "@features/editor-toolbar/components/ProjectionToolbar"; -import SelectionToolbar from "@features/editor-toolbar/components/SelectionToolbar"; +import { TriangleEmpty } from "@core/icons/TriangleEmpty"; +import EditorColumns from "@features/data-columns/components/EditorColumns"; +import EditorExports from "@features/exports/components/EditorExports"; +import useMetadataModelStyle from "@features/metadata/hooks/useMetadataModelStyle"; +import EditorMetadataColor from "@features/model-color/components/EditorMetadataColor"; +import EditorMetadataModels from "@features/model-shader/components/EditorMetadataModels"; +import EditorModels from "@features/model/components/EditorModels"; import { Allotment } from "allotment"; import "allotment/dist/style.css"; import { CanvasWrapper } from "./Canvas/CanvasWrapper"; import { TooltipOverlay } from "./Canvas/TooltipOverlay"; import "./Editor.css"; -type EditorProps = SidePanelProps; - -export default function Editor(props: EditorProps) { +export default function Editor() { useMetadataModelStyle(); return ( - + - - - - - - - - - ); } -type SidePanelProps = { - projectId: number; -}; - -function SidePanel(props: SidePanelProps) { +function SidePanel() { return ( - + + + + @@ -109,16 +75,19 @@ function SidePanel(props: SidePanelProps) { > - + - + - + + + + - + diff --git a/app/features/editor/components/EditorHeader.tsx b/app/features/editor/components/EditorHeader.tsx new file mode 100644 index 00000000..4aff9614 --- /dev/null +++ b/app/features/editor/components/EditorHeader.tsx @@ -0,0 +1,69 @@ +import { + ActionButton, + Content, + Dialog, + DialogContainer, + DialogTrigger, + Flex, + ProgressCircle, + Text, +} from "@adobe/react-spectrum"; +import Header from "@core/components/Header"; +import ActiveColumnToolbar from "@features/editor/components/Header/ActiveColumnToolbar"; +import CameraViewToolbar from "@features/editor/components/Header/CameraViewToolbar"; +import ColorSchemeToolbar from "@features/editor/components/Header/ColorSchemeToolbar"; +import ProjectionToolbar from "@features/editor/components/Header/ProjectionToolbar"; +import SelectionToolbar from "@features/editor/components/Header/SelectionToolbar"; +import AddColumnDialog from "@features/metadata/components/EditorMetadataAddColumnDialog"; +import useMetadataAssignValue from "@features/metadata/hooks/useMetadataAssignValue"; +import { useCallback, useState } from "react"; +import { useEditorContext } from "../hooks/useEditorContext"; +import AddModelMenu from "./Header/AddModelMenu"; + +export default function EditorHeader() { + const [isSavingDialogOpen, setIsSavingDialogOpen] = useState(false); + const { selection } = useEditorContext(); + const assignValue = useMetadataAssignValue(); + + const handleCreateColumn = useCallback( + (columnName: string, defaultValue: string | number, type: "string" | "number") => { + assignValue(defaultValue, columnName, type); + }, + [assignValue] + ); + + return ( + <> +
+ + + + + Add Data Column to Selection + + {(close) => } + + + + + + + + + +
+ {}}> + {isSavingDialogOpen && ( + + + + + Saving project data + + + + )} + + + ); +} diff --git a/app/features/editor/components/EditorRoot.tsx b/app/features/editor/components/EditorRoot.tsx new file mode 100644 index 00000000..4be8fc6a --- /dev/null +++ b/app/features/editor/components/EditorRoot.tsx @@ -0,0 +1,22 @@ +import { Flex, View } from "@adobe/react-spectrum"; +import Editor from "@features/editor/components/Editor"; +import EditorHeader from "@features/editor/components/EditorHeader"; +import { EditorProvider } from "@features/editor/providers/EditorProvider"; +import { MetadataProvider } from "@features/metadata/providers/MetadataProvider"; + +export default function EditorRoot() { + return ( + + + + + + + + + + + + + ); +} diff --git a/app/features/editor/components/Header/ActiveColumnToolbar.tsx b/app/features/editor/components/Header/ActiveColumnToolbar.tsx new file mode 100644 index 00000000..c7407c98 --- /dev/null +++ b/app/features/editor/components/Header/ActiveColumnToolbar.tsx @@ -0,0 +1,25 @@ +import { ComboBox, Item } from "@adobe/react-spectrum"; +import { useEditorContext } from "@features/editor/hooks/useEditorContext"; +import useMetadataContext from "@features/metadata/hooks/useMetadataContext"; + +export default function ActiveColumnToolbar() { + const { columns } = useMetadataContext(); + const { activeMetadataColumn, setActiveMetadataColumn } = useEditorContext(); + + return ( + setActiveMetadataColumn(key?.toString() || "")} + selectedKey={activeMetadataColumn} + > + {(item) => {item.key}} + + ); +} diff --git a/app/features/editor/components/Header/AddModelMenu.tsx b/app/features/editor/components/Header/AddModelMenu.tsx new file mode 100644 index 00000000..f975e70e --- /dev/null +++ b/app/features/editor/components/Header/AddModelMenu.tsx @@ -0,0 +1,81 @@ +import { + ActionButton, + Content, + Dialog, + DialogContainer, + Flex, + Item, + Key, + Menu, + MenuTrigger, + ProgressCircle, + Text, +} from "@adobe/react-spectrum"; +import { MdiCube } from "@core/icons/MdiCube"; +import { MdiTable } from "@core/icons/MdiTable"; +import { useCallback, useState } from "react"; +import useModelImport from "../../hooks/useModelImport"; + +export default function AddModelMenu() { + const [isLoadingDialogOpen, setIsLoadingDialogOpen] = useState(false); + const handleModelLoad = useModelImport(); + + const handleAction = useCallback( + async (action: Key) => { + setIsLoadingDialogOpen(true); + switch (action) { + case "shp": + await handleModelLoad([".shp", ".shx", ".dbf", ".prj", ".cpg"]); + break; + case "gltf": + await handleModelLoad([".gltf", ".glb"]); + break; + case "metacity": + await handleModelLoad([".metacity"]); + break; + //TODO handle CSV import + } + setIsLoadingDialogOpen(false); + }, + [handleModelLoad] + ); + + return ( + <> + + Import + + + + Import Shapefile + + + + Import GLTF + + + + Import Metacity + + + + Import CSV + + + + + {}}> + {isLoadingDialogOpen && ( + + + + + Loading model data + + + + )} + + + ); +} diff --git a/studio/features/editor-toolbar/components/CameraViewToolbar.tsx b/app/features/editor/components/Header/CameraViewToolbar.tsx similarity index 83% rename from studio/features/editor-toolbar/components/CameraViewToolbar.tsx rename to app/features/editor/components/Header/CameraViewToolbar.tsx index be0abd84..3f8eb3f3 100644 --- a/studio/features/editor-toolbar/components/CameraViewToolbar.tsx +++ b/app/features/editor/components/Header/CameraViewToolbar.tsx @@ -1,11 +1,4 @@ -import { - ActionGroup, - Item, - Selection, - Tooltip, - TooltipTrigger, - View, -} from "@adobe/react-spectrum"; +import { ActionGroup, Item, Selection, Tooltip, TooltipTrigger, View } from "@adobe/react-spectrum"; import { CameraView } from "@bananagl/bananagl"; import { CubeEmpty } from "@core/icons/CubeEmpty"; import { CubeLeft } from "@core/icons/CubeLeft"; @@ -24,23 +17,15 @@ export default function CameraViewToolbar() { if (keys === "all") return; //get first key - const viewMode = - (keys.values().next().value as CameraView) ?? CameraView.Free; + const viewMode = (keys.values().next().value as CameraView) ?? CameraView.Free; setViewMode(viewMode); }, - [setViewMode], + [setViewMode] ); return ( - + { + setColorScheme(colorScheme === "dark" ? "light" : "dark"); + }, [colorScheme, setColorScheme]); + + return ( + + + + {colorScheme === "dark" ? : } + {colorScheme === "dark" ? "Switch to light theme" : "Switch to dark theme"} + + + + ); +} diff --git a/studio/features/editor-toolbar/components/ProjectionToolbar.tsx b/app/features/editor/components/Header/ProjectionToolbar.tsx similarity index 75% rename from studio/features/editor-toolbar/components/ProjectionToolbar.tsx rename to app/features/editor/components/Header/ProjectionToolbar.tsx index e0918dde..67c3c459 100644 --- a/studio/features/editor-toolbar/components/ProjectionToolbar.tsx +++ b/app/features/editor/components/Header/ProjectionToolbar.tsx @@ -1,11 +1,4 @@ -import { - ActionGroup, - Item, - Selection, - Tooltip, - TooltipTrigger, - View, -} from "@adobe/react-spectrum"; +import { ActionGroup, Item, Selection, Tooltip, TooltipTrigger, View } from "@adobe/react-spectrum"; import { ProjectionType } from "@bananagl/bananagl"; import { useEditorContext } from "@features/editor/hooks/useEditorContext"; import { useCallback } from "react"; @@ -21,24 +14,15 @@ export default function ProjectionToolbar() { if (keys === "all") return; //get first key - const key = - (keys.values().next().value as ProjectionType) ?? - ProjectionType.ORTHOGRAPHIC; + const key = (keys.values().next().value as ProjectionType) ?? ProjectionType.ORTHOGRAPHIC; setProjection(key); }, - [setProjection], + [setProjection] ); return ( - + + diff --git a/studio/features/editor/data/EditorModel.ts b/app/features/editor/data/EditorModel.ts similarity index 88% rename from studio/features/editor/data/EditorModel.ts rename to app/features/editor/data/EditorModel.ts index 968a33e3..c173a152 100644 --- a/studio/features/editor/data/EditorModel.ts +++ b/app/features/editor/data/EditorModel.ts @@ -2,12 +2,7 @@ import { vec3 } from "gl-matrix"; import * as GL from "@bananagl/bananagl"; -import { - GeometryMode, - ModelData, - ModelMetadataRecords, - PrimitiveType, -} from "./types"; +import { GeometryMode, ModelData, ModelMetadataRecords, PrimitiveType } from "./types"; export interface EditorModelData extends ModelData { position?: vec3; @@ -28,7 +23,7 @@ export class EditorModel extends GL.Pickable implements GL.Selectable { public noEdgesShader?: GL.Shader; public metadata: ModelMetadataRecords = {}; - public uuid = self.crypto.randomUUID(); + public uuid: string = self.crypto.randomUUID(); constructor() { super(); @@ -91,16 +86,14 @@ export class EditorModel extends GL.Pickable implements GL.Selectable { highlight(submodelIDs: Set) { if (this.disposed) return; - if (this.lastHighlight.size > 0) - this.selectSubmodels(this.lastHighlight, 0, "highlighted"); + if (this.lastHighlight.size > 0) this.selectSubmodels(this.lastHighlight, 0, "highlighted"); this.selectSubmodels(submodelIDs, 60, "highlighted"); this.lastHighlight = submodelIDs; } dehighlight() { if (this.disposed) return; - if (this.lastHighlight.size > 0) - this.selectSubmodels(this.lastHighlight, 0, "highlighted"); + if (this.lastHighlight.size > 0) this.selectSubmodels(this.lastHighlight, 0, "highlighted"); this.lastHighlight.clear(); } @@ -148,11 +141,7 @@ export class EditorModel extends GL.Pickable implements GL.Selectable { color.buffer.toUpdate(); } - private selectSubmodels( - submodelIDs: Set, - s: number, - bufferName: "selected" | "highlighted" = "selected", - ) { + private selectSubmodels(submodelIDs: Set, s: number, bufferName: "selected" | "highlighted" = "selected") { if (this.disposed) return; if (submodelIDs.size === 0) return; const selected = this.attributes.getAttribute(bufferName); @@ -173,8 +162,7 @@ export class EditorModel extends GL.Pickable implements GL.Selectable { if (this.disposed) return; const selected = this.attributes.getAttribute("selected"); if (!selected) return; - for (let i = 0; i < selected.buffer.data.length; i++) - selected.buffer.data[i] = 0; + for (let i = 0; i < selected.buffer.data.length; i++) selected.buffer.data[i] = 0; selected.buffer.toUpdate(); } } diff --git a/studio/features/editor/data/EditorModelShader.ts b/app/features/editor/data/EditorModelShader.ts similarity index 96% rename from studio/features/editor/data/EditorModelShader.ts rename to app/features/editor/data/EditorModelShader.ts index acb5f6ce..ecbdc38f 100644 --- a/studio/features/editor/data/EditorModelShader.ts +++ b/app/features/editor/data/EditorModelShader.ts @@ -148,11 +148,7 @@ void main() { } `; -export const wireframeShader = new GL.Shader( - wvertexShader, - wfragmentShader, - true, -); +export const wireframeShader = new GL.Shader(wvertexShader, wfragmentShader, true); const svertexShader = ` in vec3 normal; @@ -185,7 +181,7 @@ void main() { float factor = max(factorA, factorB); //the weight of the oColor = vec3(factor) * 0.1 + vec3(0.8); - oColor *= mix(vec3(1.0), mix(vec3(0.5), vec3(1.0), smoothstep(uZMin, uZMax, transformed.z)), uUseShading); + //oColor *= mix(vec3(1.0), mix(vec3(0.5), vec3(1.0), smoothstep(uZMin, uZMax, transformed.z)), uUseShading); oColor *= mix(color, vec3(1.0, 0.705, 0.196), selected); oColor *= mix(color, vec3(1.0, 0.705, 0.196), highlighted); diff --git a/old/studio/src/data/GridModel.ts b/app/features/editor/data/GridModel.ts similarity index 100% rename from old/studio/src/data/GridModel.ts rename to app/features/editor/data/GridModel.ts diff --git a/old/studio/src/data/GridModelShader.ts b/app/features/editor/data/GridModelShader.ts similarity index 100% rename from old/studio/src/data/GridModelShader.ts rename to app/features/editor/data/GridModelShader.ts diff --git a/studio/features/editor/data/types.ts b/app/features/editor/data/types.ts similarity index 100% rename from studio/features/editor/data/types.ts rename to app/features/editor/data/types.ts diff --git a/studio/features/editor/hooks/useActiveView.ts b/app/features/editor/hooks/useActiveView.ts similarity index 100% rename from studio/features/editor/hooks/useActiveView.ts rename to app/features/editor/hooks/useActiveView.ts diff --git a/studio/features/editor/hooks/useEditorContext.ts b/app/features/editor/hooks/useEditorContext.ts similarity index 100% rename from studio/features/editor/hooks/useEditorContext.ts rename to app/features/editor/hooks/useEditorContext.ts diff --git a/studio/features/editor/hooks/useExportModels.ts b/app/features/editor/hooks/useExportModels.ts similarity index 100% rename from studio/features/editor/hooks/useExportModels.ts rename to app/features/editor/hooks/useExportModels.ts diff --git a/studio/features/editor/hooks/useImportModels.ts b/app/features/editor/hooks/useImportModels.ts similarity index 100% rename from studio/features/editor/hooks/useImportModels.ts rename to app/features/editor/hooks/useImportModels.ts diff --git a/app/features/editor/hooks/useModelImport.ts b/app/features/editor/hooks/useModelImport.ts new file mode 100644 index 00000000..1f78e2ab --- /dev/null +++ b/app/features/editor/hooks/useModelImport.ts @@ -0,0 +1,54 @@ +import { toasterOptions } from "@core/defaults"; +import { useImportModels } from "@features/editor/hooks/useImportModels"; +import { load } from "@features/editor/utils/formats/loader"; +import { ToastQueue } from "@react-spectrum/toast"; +import { useCallback } from "react"; + +export default function useModelImport() { + const importModels = useImportModels(); + + const handleSubmit = useCallback( + async (files: FileList | null) => { + if (!files) return; + const modelList = Array.from(files); + if (modelList.length === 0) { + ToastQueue.info("No models selected", toasterOptions); + return; + } + const fileMap = new Map(); + // Add models to project + for (const file of modelList) { + fileMap.set(file.name, file); + } + const data = await load(fileMap); + await importModels(data); + }, + [importModels] + ); + + return useCallback( + (acceptedFormats: string[]) => + new Promise((resolve, reject) => { + const input = document.createElement("input"); + document.body.appendChild(input); + input.type = "file"; + input.accept = acceptedFormats.join(","); + input.multiple = true; + + input.addEventListener("change", async () => { + await handleSubmit(input.files); + input.remove(); + resolve(); + }); + + input.addEventListener("cancel", () => { + console.log("cancel"); + input.remove(); + resolve(); + }); + + input.click(); + }), + [handleSubmit] + ); +} diff --git a/studio/features/editor/hooks/useModelList.ts b/app/features/editor/hooks/useModelList.ts similarity index 100% rename from studio/features/editor/hooks/useModelList.ts rename to app/features/editor/hooks/useModelList.ts diff --git a/studio/features/editor/hooks/useModelToggleVisibility.ts b/app/features/editor/hooks/useModelToggleVisibility.ts similarity index 100% rename from studio/features/editor/hooks/useModelToggleVisibility.ts rename to app/features/editor/hooks/useModelToggleVisibility.ts diff --git a/studio/features/editor/hooks/useModels.ts b/app/features/editor/hooks/useModels.ts similarity index 100% rename from studio/features/editor/hooks/useModels.ts rename to app/features/editor/hooks/useModels.ts diff --git a/studio/features/editor/hooks/useProjectModels.ts b/app/features/editor/hooks/useProjectModels.ts similarity index 100% rename from studio/features/editor/hooks/useProjectModels.ts rename to app/features/editor/hooks/useProjectModels.ts diff --git a/studio/features/editor/hooks/useRemoveModels.ts b/app/features/editor/hooks/useRemoveModels.ts similarity index 100% rename from studio/features/editor/hooks/useRemoveModels.ts rename to app/features/editor/hooks/useRemoveModels.ts diff --git a/studio/features/editor/hooks/useRemoveSubmodels.ts b/app/features/editor/hooks/useRemoveSubmodels.ts similarity index 100% rename from studio/features/editor/hooks/useRemoveSubmodels.ts rename to app/features/editor/hooks/useRemoveSubmodels.ts diff --git a/studio/features/editor/hooks/useRender.ts b/app/features/editor/hooks/useRender.ts similarity index 100% rename from studio/features/editor/hooks/useRender.ts rename to app/features/editor/hooks/useRender.ts diff --git a/studio/features/editor/hooks/useScene.ts b/app/features/editor/hooks/useScene.ts similarity index 100% rename from studio/features/editor/hooks/useScene.ts rename to app/features/editor/hooks/useScene.ts diff --git a/studio/features/editor/hooks/useSelected.ts b/app/features/editor/hooks/useSelected.ts similarity index 100% rename from studio/features/editor/hooks/useSelected.ts rename to app/features/editor/hooks/useSelected.ts diff --git a/studio/features/editor/hooks/useSelectedSubmodelCount.ts b/app/features/editor/hooks/useSelectedSubmodelCount.ts similarity index 100% rename from studio/features/editor/hooks/useSelectedSubmodelCount.ts rename to app/features/editor/hooks/useSelectedSubmodelCount.ts diff --git a/studio/features/editor/hooks/useSelection.ts b/app/features/editor/hooks/useSelection.ts similarity index 100% rename from studio/features/editor/hooks/useSelection.ts rename to app/features/editor/hooks/useSelection.ts diff --git a/studio/features/editor/hooks/useSplitModel.ts b/app/features/editor/hooks/useSplitModel.ts similarity index 97% rename from studio/features/editor/hooks/useSplitModel.ts rename to app/features/editor/hooks/useSplitModel.ts index 3cad4e6c..6c9013e5 100644 --- a/studio/features/editor/hooks/useSplitModel.ts +++ b/app/features/editor/hooks/useSplitModel.ts @@ -15,10 +15,12 @@ export function useSplitModel() { async (oldModel: EditorModel, submodels: Set) => { const newModels = splitModel(oldModel, submodels); if (!newModels) return; - await importModels(newModels); + await importModels(newModels, { + disableShift: true, + }); removeModels([oldModel]); }, - [importModels, removeModels], + [importModels, removeModels] ); return split; diff --git a/studio/features/editor/hooks/useTooltip.ts b/app/features/editor/hooks/useTooltip.ts similarity index 100% rename from studio/features/editor/hooks/useTooltip.ts rename to app/features/editor/hooks/useTooltip.ts diff --git a/studio/features/editor/providers/EditorProvider.tsx b/app/features/editor/providers/EditorProvider.tsx similarity index 93% rename from studio/features/editor/providers/EditorProvider.tsx rename to app/features/editor/providers/EditorProvider.tsx index d4731f9b..b70b502a 100644 --- a/studio/features/editor/providers/EditorProvider.tsx +++ b/app/features/editor/providers/EditorProvider.tsx @@ -3,20 +3,9 @@ import * as GL from "@bananagl/bananagl"; import { EditorModel } from "@editor/data/EditorModel"; import { ModelStyle, Style } from "@editor/data/types"; import { vec3 } from "gl-matrix"; -import { - Dispatch, - ReactNode, - SetStateAction, - createContext, - useEffect, - useState, -} from "react"; - -export type SelectFunction = ( - selection: SelectionType, - toggle?: boolean, - extend?: boolean, -) => void; +import { Dispatch, ReactNode, SetStateAction, createContext, useEffect, useState } from "react"; + +export type SelectFunction = (selection: SelectionType, toggle?: boolean, extend?: boolean) => void; export type SelectionType = Map>; export type Tooltip = { data: any; x: number; y: number } | null; @@ -52,9 +41,7 @@ type EditorContextProps = { setViewMode: Dispatch>; }; -export const context = createContext( - {} as EditorContextProps, -); +export const context = createContext({} as EditorContextProps); export function EditorProvider(props: { children: ReactNode }) { const [renderer] = useState(new GL.Renderer()); @@ -70,9 +57,7 @@ export function EditorProvider(props: { children: ReactNode }) { const [modelStyles, setModelStyles] = useState({}); const [greyscale, setGreyscale] = useState(false); const [activeMetadataColumn, setActiveMetadataColumn] = useState(""); - const [projection, setProjection] = useState( - GL.ProjectionType.ORTHOGRAPHIC, - ); + const [projection, setProjection] = useState(GL.ProjectionType.ORTHOGRAPHIC); const [viewMode, setViewMode] = useState(GL.CameraView.Free); //TODO darkmode load from user device settings diff --git a/studio/features/editor/types.ts b/app/features/editor/types.ts similarity index 100% rename from studio/features/editor/types.ts rename to app/features/editor/types.ts diff --git a/old/studio/src/utils/color.ts b/app/features/editor/utils/color.ts similarity index 100% rename from old/studio/src/utils/color.ts rename to app/features/editor/utils/color.ts diff --git a/old/studio/src/utils/formats/errors.tsx b/app/features/editor/utils/formats/errors.tsx similarity index 100% rename from old/studio/src/utils/formats/errors.tsx rename to app/features/editor/utils/formats/errors.tsx diff --git a/studio/features/editor/utils/formats/gltf.worker.ts b/app/features/editor/utils/formats/gltf.worker.ts similarity index 100% rename from studio/features/editor/utils/formats/gltf.worker.ts rename to app/features/editor/utils/formats/gltf.worker.ts diff --git a/studio/features/editor/utils/formats/gltf/parse.ts b/app/features/editor/utils/formats/gltf/parse.ts similarity index 100% rename from studio/features/editor/utils/formats/gltf/parse.ts rename to app/features/editor/utils/formats/gltf/parse.ts diff --git a/old/studio/src/utils/formats/gltf/transform.ts b/app/features/editor/utils/formats/gltf/transform.ts similarity index 100% rename from old/studio/src/utils/formats/gltf/transform.ts rename to app/features/editor/utils/formats/gltf/transform.ts diff --git a/studio/features/editor/utils/formats/gltf/unindex.ts b/app/features/editor/utils/formats/gltf/unindex.ts similarity index 100% rename from studio/features/editor/utils/formats/gltf/unindex.ts rename to app/features/editor/utils/formats/gltf/unindex.ts diff --git a/studio/features/editor/utils/formats/loader.ts b/app/features/editor/utils/formats/loader.ts similarity index 82% rename from studio/features/editor/utils/formats/loader.ts rename to app/features/editor/utils/formats/loader.ts index 060e1372..398a830c 100644 --- a/studio/features/editor/utils/formats/loader.ts +++ b/app/features/editor/utils/formats/loader.ts @@ -1,13 +1,8 @@ -"use client"; - import { ModelData, UserInputModel } from "@editor/data/types"; import { EditorData } from "./metacity/types"; import { WorkerPool } from "./pool"; -export async function load( - files: Map, - updateStatus?: (status: string) => void, -) { +export async function load(files: Map, updateStatus?: (status: string) => void) { const models = await filterModelFiles(files); const modelData = await loadModels(models, updateStatus); return modelData; @@ -24,9 +19,7 @@ async function filterModelFiles(files: Map) { return [...gltf, ...ifc, ...shapefile, ...metacity]; } -async function filterMetacity( - files: Map, -): Promise { +async function filterMetacity(files: Map): Promise { const data = []; for (const [name, file] of files) { if (name.endsWith("metacity") && !name.endsWith("json.metacity")) { @@ -74,9 +67,7 @@ async function filterIFC(files: Map): Promise { return data; } -async function filterShapefile( - files: Map, -): Promise { +async function filterShapefile(files: Map): Promise { const data = []; for (const [name, file] of files) { if (name.endsWith("shp")) { @@ -109,37 +100,34 @@ async function getFile(files: Map, name: string) { return undefined; } -const pool = new WorkerPool< - UserInputModel, - EditorData | ModelData | ModelData[] ->(10); +const pool = new WorkerPool(10); function spawnGLTFWorker() { - return new Worker(new URL("./gltf.worker.ts", import.meta.url)); + return new Worker(new URL("./gltf.worker.ts", import.meta.url), { + type: "module", + }); } function spawnMetacityWorker() { - return new Worker(new URL("./metacity.worker.ts", import.meta.url)); + return new Worker(new URL("./metacity.worker.ts", import.meta.url), { + type: "module", + }); } function spawnShapefileWorker() { - return new Worker(new URL("./shapefile.worker.ts", import.meta.url)); + return new Worker(new URL("./shapefile.worker.ts", import.meta.url), { + type: "module", + }); } -export async function loadModels( - models: UserInputModel[], - updateStatus?: (status: string) => void, -) { +export async function loadModels(models: UserInputModel[], updateStatus?: (status: string) => void) { const jobs: Promise[] = []; for (const model of models) { if (model.name.endsWith("gltf") || model.name.endsWith("glb")) { jobs.push(loadWorker(model, spawnGLTFWorker, updateStatus)); } else if (model.name.endsWith("shp")) { jobs.push(loadWorker(model, spawnShapefileWorker, updateStatus)); - } else if ( - model.name.endsWith("metacity") || - model.name.endsWith("mcmodel") - ) { + } else if (model.name.endsWith("metacity") || model.name.endsWith("mcmodel")) { jobs.push(loadWorker(model, spawnMetacityWorker, updateStatus)); } } @@ -157,7 +145,7 @@ export async function loadModels( function loadWorker( model: UserInputModel, worker: () => Worker, - updateStatus?: (status: string) => void, + updateStatus?: (status: string) => void ): Promise { return new Promise((resolve, reject) => { pool.process( @@ -168,11 +156,10 @@ function loadWorker( updateStatus && updateStatus(`Loading models: Loaded ${model.name}`); } else { reject(`Could not parse model ${model.name}`); - updateStatus && - updateStatus(`Loading models: Could not parse model ${model.name}`); + updateStatus && updateStatus(`Loading models: Could not parse model ${model.name}`); } }, - worker, + worker ); }); } diff --git a/studio/features/editor/utils/formats/metacity.worker.ts b/app/features/editor/utils/formats/metacity.worker.ts similarity index 100% rename from studio/features/editor/utils/formats/metacity.worker.ts rename to app/features/editor/utils/formats/metacity.worker.ts diff --git a/studio/features/editor/utils/formats/metacity/parse.ts b/app/features/editor/utils/formats/metacity/parse.ts similarity index 100% rename from studio/features/editor/utils/formats/metacity/parse.ts rename to app/features/editor/utils/formats/metacity/parse.ts diff --git a/studio/features/editor/utils/formats/metacity/read.ts b/app/features/editor/utils/formats/metacity/read.ts similarity index 100% rename from studio/features/editor/utils/formats/metacity/read.ts rename to app/features/editor/utils/formats/metacity/read.ts diff --git a/app/features/editor/utils/formats/metacity/streams.ts b/app/features/editor/utils/formats/metacity/streams.ts new file mode 100644 index 00000000..c50b3edd --- /dev/null +++ b/app/features/editor/utils/formats/metacity/streams.ts @@ -0,0 +1,111 @@ +export class WriteOnlyMemoryStream { + buffers: Uint8Array[] = []; + unfinishedBuffer: number[] = []; + MAX_BUFFER_SIZE = 1024 * 1024 * 10; // 10MB + + writeUint8(byte: number) { + this.unfinishedBuffer.push(byte); + this.add(); + } + + writeInt32(int: number) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setInt32(0, int, true); + this.unfinishedBuffer.push(...new Uint8Array(buffer)); + this.add(); + } + + writeFloat32(float: number) { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + view.setFloat32(0, float, true); + this.unfinishedBuffer.push(...new Uint8Array(buffer)); + this.add(); + } + + writeUint8Array(array: Uint8Array | ArrayBuffer) { + const buffer = new Uint8Array(array); + for (let i = 0; i < buffer.length; i++) { + this.unfinishedBuffer.push(buffer[i]); + this.add(); + } + } + + add() { + if (this.unfinishedBuffer.length >= this.MAX_BUFFER_SIZE) { + this.flush(); + } + } + + private flush() { + this.buffers.push(new Uint8Array(this.unfinishedBuffer)); + this.unfinishedBuffer.length = 0; + } + + close() { + this.flush(); + } +} + +export class ReadOnlyMemoryStream { + position = 0; + buffer: Uint8Array; + constructor(buffer: ArrayBuffer) { + this.buffer = new Uint8Array(buffer); + } + + seek(position: number) { + this.position = position; + } + + readUint8() { + const byte = this.buffer[this.position]; + this.position += 1; + return byte; + } + + readInt32() { + const buffer = this.buffer.slice(this.position, this.position + 4); + const view = new DataView(buffer.buffer); + const float = view.getInt32(0, true); + this.position += 4; + return float; + } + + readFloat32() { + const buffer = this.buffer.slice(this.position, this.position + 4); + const view = new DataView(buffer.buffer); + const float = view.getFloat32(0, true); + this.position += 4; + return float; + } + + readUint8Array(length: number) { + const array = new Uint8Array(length); + const view = new Uint8Array(this.buffer.buffer, this.position, length); + array.set(view); + this.position += length; + return array; + } + + readInt32Array(length: number) { + const array = new Int32Array(length); + const view = new Int32Array(this.buffer.buffer, this.position, length); + array.set(view); + this.position += length * 4; + return array; + } + + readFloat32Array(length: number) { + const array = new Float32Array(length); + const view = new Float32Array(this.buffer.buffer, this.position, length); + array.set(view); + this.position += length * 4; + return array; + } + + empty() { + return this.position >= this.buffer.length; + } +} diff --git a/studio/features/editor/utils/formats/metacity/types.ts b/app/features/editor/utils/formats/metacity/types.ts similarity index 100% rename from studio/features/editor/utils/formats/metacity/types.ts rename to app/features/editor/utils/formats/metacity/types.ts diff --git a/studio/features/editor/utils/formats/metacity/write.ts b/app/features/editor/utils/formats/metacity/write.ts similarity index 98% rename from studio/features/editor/utils/formats/metacity/write.ts rename to app/features/editor/utils/formats/metacity/write.ts index 049e89b9..6ef509ea 100644 --- a/studio/features/editor/utils/formats/metacity/write.ts +++ b/app/features/editor/utils/formats/metacity/write.ts @@ -78,7 +78,7 @@ export interface ConstructableTypedArray { function writeTypedArray(array: TypedArray, stream: WriteOnlyMemoryStream) { const length = array.length; stream.writeInt32(length); - stream.writeUint8Array(array.buffer); + stream.writeUint8Array(array.buffer as ArrayBuffer); } function writeString(string: string, stream: WriteOnlyMemoryStream) { diff --git a/studio/features/editor/utils/formats/pool.ts b/app/features/editor/utils/formats/pool.ts similarity index 93% rename from studio/features/editor/utils/formats/pool.ts rename to app/features/editor/utils/formats/pool.ts index 8b30aed6..6e56b0dc 100644 --- a/studio/features/editor/utils/formats/pool.ts +++ b/app/features/editor/utils/formats/pool.ts @@ -46,11 +46,7 @@ export class WorkerPool { this.queue = new Queue(); } - process( - data: InputType, - callback: (output: ResultType | undefined) => void, - worker: () => Worker, - ) { + process(data: InputType, callback: (output: ResultType | undefined) => void, worker: () => Worker) { this.queue.enqueue({ data: data, worker: worker, diff --git a/studio/features/editor/utils/formats/shapefile.worker.ts b/app/features/editor/utils/formats/shapefile.worker.ts similarity index 100% rename from studio/features/editor/utils/formats/shapefile.worker.ts rename to app/features/editor/utils/formats/shapefile.worker.ts diff --git a/studio/features/editor/utils/formats/shp/parse.ts b/app/features/editor/utils/formats/shp/parse.ts similarity index 100% rename from studio/features/editor/utils/formats/shp/parse.ts rename to app/features/editor/utils/formats/shp/parse.ts diff --git a/old/studio/src/utils/placeholders.ts b/app/features/editor/utils/placeholders.ts similarity index 100% rename from old/studio/src/utils/placeholders.ts rename to app/features/editor/utils/placeholders.ts diff --git a/old/studio/src/utils/predicates.ts b/app/features/editor/utils/predicates.ts similarity index 100% rename from old/studio/src/utils/predicates.ts rename to app/features/editor/utils/predicates.ts diff --git a/studio/app/embeds/[embedId]/page.tsx b/app/features/embeds/components/EmbedRoot.tsx similarity index 50% rename from studio/app/embeds/[embedId]/page.tsx rename to app/features/embeds/components/EmbedRoot.tsx index 729e2668..34b905d8 100644 --- a/studio/app/embeds/[embedId]/page.tsx +++ b/app/features/embeds/components/EmbedRoot.tsx @@ -1,38 +1,23 @@ -"use client"; - import { Flex, View } from "@adobe/react-spectrum"; -import { withPageAuthRequired } from "@auth0/nextjs-auth0/client"; -import { withUserEnabled } from "@core/utils/withUserEnabled"; -import { MetadataProvider } from "@features/editor-metadata/providers/MetadataProvider"; +import { MetadataProvider } from "@features/metadata/providers/MetadataProvider"; import { EditorProvider } from "@features/editor/providers/EditorProvider"; import Viewer from "@features/viewer/components/Viewer"; import ViewerHeader from "@features/viewer/components/ViewerHeader"; -type ProjectPageProps = { - params: { - embedId: string; - }; -}; - -function EmbedPage({ params }: ProjectPageProps) { - const embedId = params.embedId; - const sanitizedId = parseInt(embedId, 10); - +export default function EmbedRoot() { return ( - + - + ); } - -export default withPageAuthRequired(withUserEnabled(EmbedPage)); diff --git a/app/features/exports/components/EditorExports.tsx b/app/features/exports/components/EditorExports.tsx new file mode 100644 index 00000000..73aa3c01 --- /dev/null +++ b/app/features/exports/components/EditorExports.tsx @@ -0,0 +1,29 @@ +import { Item, TabList, TabPanels, Tabs, Text, View } from "@adobe/react-spectrum"; +import { PositioningContainer } from "@core/components/PositioningContainer"; +import EditorExportsCreate from "./EditorExportsCreate"; + +export default function EditorExports() { + return ( + + + + + + Create Export + + + Exports + + + + + + + + + + + + + ); +} diff --git a/studio/features/editor-exports/components/EditorExportsCreate.tsx b/app/features/exports/components/EditorExportsCreate.tsx similarity index 72% rename from studio/features/editor-exports/components/EditorExportsCreate.tsx rename to app/features/exports/components/EditorExportsCreate.tsx index 8b104edc..596da685 100644 --- a/studio/features/editor-exports/components/EditorExportsCreate.tsx +++ b/app/features/exports/components/EditorExportsCreate.tsx @@ -1,5 +1,3 @@ -"use client"; - import { ActionBar, ActionBarContainer, @@ -18,19 +16,12 @@ import { import { PositioningContainer } from "@core/components/PositioningContainer"; import { MdiExport } from "@core/icons/MdiExport"; -import uploadEmbed from "@features/api-sdk/uploadEmbed"; -import useMetadataContext from "@features/editor-metadata/hooks/useMetadataContext"; import useExportEmbed from "@features/editor/hooks/useExportModels"; import { useRenderer } from "@features/editor/hooks/useRender"; +import useMetadataContext from "@features/metadata/hooks/useMetadataContext"; import { useCallback, useState } from "react"; -type EditorExportsCreateProps = { - sanitizedId: number; -}; - -export default function EditorExportsCreate({ - sanitizedId, -}: EditorExportsCreateProps) { +export default function EditorExportsCreate() { const { columns } = useMetadataContext(); const [name, setName] = useState("Untitled Embed"); @@ -41,12 +32,8 @@ export default function EditorExportsCreate({ const renderer = useRenderer(); const saveEmbed = useCallback(() => { - async function handleUploadEmbed( - dataFile: File, - thumbnailFileContents: string, - ) { - await uploadEmbed(sanitizedId, dataFile, thumbnailFileContents, name); - + async function handleUploadEmbed(dataFile: File, thumbnailFileContents: string) { + //TODO download embed file setIsSavingDialogOpen(false); } @@ -63,7 +50,7 @@ export default function EditorExportsCreate({ //upload project version void handleUploadEmbed(dataFile, image); }; - }, [sanitizedId, name, exportEmbeds, renderer, selectedKeys]); + }, [name, exportEmbeds, renderer, selectedKeys]); const handleGlobalAction = useCallback( (key: Key) => { @@ -71,31 +58,22 @@ export default function EditorExportsCreate({ saveEmbed(); } }, - [saveEmbed], + [saveEmbed] ); return ( - + - + void; - onSubmit: ( - name: string, - defaultValue: string | number, - defaultType: "string" | "number", - ) => void; + onSubmit: (name: string, defaultValue: string | number, defaultType: "string" | "number") => void; }; -export default function AddColumnDialog({ - close, - onSubmit, -}: AddColumnDialogProps) { +export default function AddColumnDialog({ close, onSubmit }: AddColumnDialogProps) { const [name, setName] = useState(""); const [defaultValue, setDefaultValue] = useState(""); const [defaultType, setDefaultType] = useState<"string" | "number">("string"); const handleSubmit = useCallback(async () => { if (!name) { - ToastQueue.negative("Column name is required"); + ToastQueue.negative("Column name is required", toasterOptions); return; } try { onSubmit(name, defaultValue, defaultType); - ToastQueue.positive("Column created successfully"); + ToastQueue.positive("Column created successfully", toasterOptions); close(); } catch (error) { console.error(error); - ToastQueue.negative("Failed to create column"); + ToastQueue.negative("Failed to create column", toasterOptions); } }, [name, defaultValue, defaultType, onSubmit, close]); diff --git a/studio/features/editor-metadata/components/EditorMetadataImport.tsx b/app/features/metadata/components/EditorMetadataImport.tsx similarity index 70% rename from studio/features/editor-metadata/components/EditorMetadataImport.tsx rename to app/features/metadata/components/EditorMetadataImport.tsx index 9a82ec7c..fb317b3a 100644 --- a/studio/features/editor-metadata/components/EditorMetadataImport.tsx +++ b/app/features/metadata/components/EditorMetadataImport.tsx @@ -1,14 +1,7 @@ -import { - Button, - Checkbox, - ComboBox, - Flex, - Item, - Text, - View, -} from "@adobe/react-spectrum"; +import { Button, Checkbox, ComboBox, Flex, Item, Text, View } from "@adobe/react-spectrum"; import { NoData } from "@core/components/Empty"; import { PositioningContainer } from "@core/components/PositioningContainer"; +import { toasterOptions } from "@core/defaults"; import { ToastQueue } from "@react-spectrum/toast"; import { useCallback, useMemo, useState } from "react"; import useMetadataContext from "../hooks/useMetadataContext"; @@ -31,29 +24,14 @@ export default function EditorMetadataImport() { const { columns } = useMetadataContext(); const handleMapping = useMetadataMap(); - useMetadataMappingStyle( - sourceMetadataColumn, - tableData, - targetMetadataColumn, - ); + useMetadataMappingStyle(sourceMetadataColumn, tableData, targetMetadataColumn); const onSubmit = useCallback(() => { if (!tableData) return; - handleMapping( - sourceMetadataColumn, - tableData, - targetMetadataColumn, - removeExisting, - ); - ToastQueue.positive("Data mapped successfully"); - }, [ - handleMapping, - sourceMetadataColumn, - tableData, - targetMetadataColumn, - removeExisting, - ]); + handleMapping(sourceMetadataColumn, tableData, targetMetadataColumn, removeExisting); + ToastQueue.positive("Data mapped successfully", toasterOptions); + }, [handleMapping, sourceMetadataColumn, tableData, targetMetadataColumn, removeExisting]); return ( @@ -71,20 +49,13 @@ export default function EditorMetadataImport() { )} {tableData && ( - + Map data from the table based on: - setSourceMetadataColumn(key?.toString() || "") - } + onSelectionChange={(key) => setSourceMetadataColumn(key?.toString() || "")} selectedKey={sourceMetadataColumn} > {(item) => {item.key}} @@ -94,26 +65,18 @@ export default function EditorMetadataImport() { label="Existing Metadata column" defaultItems={columns} width="100%" - onSelectionChange={(key) => - setTargetMetadataColumn(key?.toString() || "") - } + onSelectionChange={(key) => setTargetMetadataColumn(key?.toString() || "")} selectedKey={targetMetadataColumn} > {(item) => {item.key}} - + Remove all existing data - - - ); -} diff --git a/old/studio/src/components/Editor/ModelList.tsx b/old/studio/src/components/Editor/ModelList.tsx deleted file mode 100644 index 1030f8d7..00000000 --- a/old/studio/src/components/Editor/ModelList.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ColumnContainer, OverflowContainer } from '@elements/Containers'; -import { Empty } from '@elements/Empty'; - -import { EditorModel } from '@data/EditorModel'; - -import { useModels } from '@hooks/useModels'; -import { useSelected } from '@hooks/useSelected'; -import { useSelection } from '@hooks/useSelection'; - -import { ModelItem } from './ModelItem'; - -export function ModelList() { - const models = useModels(); - const select = useSelection(); - const selection = useSelected(); - - const handleSelect = (model: EditorModel) => { - if (selection.size > 1) select(new Map([[model, new Set()]])); - else if (selection.has(model)) select(new Map()); - else { - const submodels = Object.keys(model.metadata).map((submodel) => parseInt(submodel)); - select(new Map([[model, new Set(submodels)]])); - } - }; - - return ( - - - {models.length === 0 && No models} - {models.length >= 0 && - models.map((model, index) => ( - handleSelect(model)} - /> - ))} - - - ); -} diff --git a/old/studio/src/components/Editor/Modifiers.tsx b/old/studio/src/components/Editor/Modifiers.tsx deleted file mode 100644 index 26f45b49..00000000 --- a/old/studio/src/components/Editor/Modifiers.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Empty } from '@elements/Empty'; - -import { useSelected } from '@hooks/useSelected'; - -import { DeleteSubmodelsWidget } from './WidgetDeleteSubmodels'; -import { JoinSubmodelWidget } from './WidgetJoinSubmodels'; -import { MappingWidget } from './WidgetMapping'; -import { SplitModelWidget } from './WidgetSplitModel'; -import { ModelTransformationWidget } from './WidgetTransformation'; - -export function Modifiers() { - const selection = useSelected(); - - if (selection.size === 0) return Nothing selected; - if (selection.size > 1) return Select only a single model; - const model = Array.from(selection)[0][0]; - - return ( -
- - - - - -
- ); -} diff --git a/old/studio/src/components/Editor/SidePanel.tsx b/old/studio/src/components/Editor/SidePanel.tsx deleted file mode 100644 index 790483bf..00000000 --- a/old/studio/src/components/Editor/SidePanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import 'allotment/dist/style.css'; -import { VscJson, VscMove, VscSymbolColor, VscTable, VscTools } from 'react-icons/vsc'; - -import { ColumnContainer } from '@elements/Containers'; -import { TabButton, TabGroup, TabList, TabPanel, TabPanels } from '@elements/Tabs'; - -import { IOMenu } from '@shared/IOMenu'; -import { StyleSidePanel } from '@shared/StyleSidePanel'; -import { ViewPanel } from '@shared/ViewPanel'; - -import { MetadataSidePanel } from './MetadataSidePanel'; -import { TableSidePanel } from './TableSidePanel'; -import { TransformSidePanel } from './TransformSidePanel'; - -export function SidePanel() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/old/studio/src/components/Editor/TableList.tsx b/old/studio/src/components/Editor/TableList.tsx deleted file mode 100644 index 9f2f0e07..00000000 --- a/old/studio/src/components/Editor/TableList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import clsx from 'clsx'; -import { IoClose } from 'react-icons/io5'; - -import { RowContainer } from '@elements/Containers'; - -import { useTabelsEmpty } from '@hooks/useTabeIsEmpty'; -import { useTableRemoveSheet } from '@hooks/useTableRemoveSheet'; -import { useTableSheetCount } from '@hooks/useTableSheetCount'; -import { useTableSheetIndex } from '@hooks/useTableSheetIndex'; - -export function TablesSheetList() { - const removeSheet = useTableRemoveSheet(); - const [sheetIndex, setSheetIndex] = useTableSheetIndex(); - const empty = useTabelsEmpty(); - const sheetCount = useTableSheetCount(); - - const handleRemoveSheet = (index: number) => { - removeSheet(index); - if (index === sheetIndex) { - setSheetIndex(0); - } - }; - - if (empty) return null; - - return ( - - {Array(sheetCount).map((_, index) => ( - - - - - ))} - - ); -} diff --git a/old/studio/src/components/Editor/TableRow.tsx b/old/studio/src/components/Editor/TableRow.tsx deleted file mode 100644 index be0d3102..00000000 --- a/old/studio/src/components/Editor/TableRow.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import clsx from 'clsx'; -import { BiLink } from 'react-icons/bi'; - -import { rowType } from '@context/TablesContext'; - -import { useTableGetJSON } from '@hooks/useTableGetJSON'; -import { useTableSetRowType } from '@hooks/useTableSetRowType'; -import { useTableSheetIndex } from '@hooks/useTableSheetIndex'; - -export type AssignToGeometryCallback = (data: any) => void; - -interface TableRowProps { - index: number; - row: string[]; - rowType: string; - assignToGeometry: AssignToGeometryCallback; -} - -function ActionButton(props: { children: React.ReactNode; onClick?: () => void; title?: string }) { - return ( - - ); -} - -export function TableRow(props: TableRowProps) { - const { index, row, rowType } = props; - const getJSON = useTableGetJSON(); - const updateRowType = useTableSetRowType(); - const [activeSheet] = useTableSheetIndex(); - - const handleRowTypeUpdate = (event: React.ChangeEvent) => { - const { value } = event.target; - updateRowType(index, value as rowType); - }; - - const handleLinkToSelection = () => { - const json = getJSON(index); - props.assignToGeometry(json); - }; - - return ( - - -
- - - -
- - - - - {row.map((cell, cindex) => ( - {cell} - ))} - - ); -} - -export function Td(props: { children: React.ReactNode; className?: string }) { - return ( - - {props.children} - - ); -} diff --git a/old/studio/src/components/Editor/TableSheet.tsx b/old/studio/src/components/Editor/TableSheet.tsx deleted file mode 100644 index a1636ea5..00000000 --- a/old/studio/src/components/Editor/TableSheet.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; - -import { useTabelsEmpty } from '@hooks/useTabeIsEmpty'; -import { useTableSheet } from '@hooks/useTableActiveSheet'; -import { useTableRowTypes } from '@hooks/useTableRowTypes'; -import { useTableSheetIndex } from '@hooks/useTableSheetIndex'; - -import { AssignToGeometryCallback, TableRow } from './TableRow'; - -interface SheetProps { - assignToGeometry: AssignToGeometryCallback; -} - -export function Sheet(props: SheetProps) { - const empty = useTabelsEmpty(); - const sheet = useTableSheet(); - const rowTypes = useTableRowTypes(); - const [sheetIndex] = useTableSheetIndex(); - - if (empty || !sheet || !rowTypes) return null; - - return ( - - - - - - {sheet[0].map((_, index) => ( - - ))} - - - - {sheet.map((row, index) => ( - - ))} - -
#type{encodeTableColumnName(index)}
- ); -} - -export function Th(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} - -function encodeTableColumnName(col: number): string { - let name = ''; - while (col >= 0) { - name = String.fromCharCode((col % 26) + 65) + name; - col = Math.floor(col / 26) - 1; - } - return name; -} diff --git a/old/studio/src/components/Editor/TableSidePanel.tsx b/old/studio/src/components/Editor/TableSidePanel.tsx deleted file mode 100644 index 9c7de79a..00000000 --- a/old/studio/src/components/Editor/TableSidePanel.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import { ColumnContainer, OverflowAbsoluteContainer, StretchContainer } from '@elements/Containers'; -import { PanelTitle } from '@elements/PanelTitle'; - -import { useMetadata } from '@hooks/useMetadata'; -import { useMetadatHeatmap } from '@hooks/useMetadataHeatmap'; - -import { TablesSheetList } from './TableList'; -import { Tables } from './Tables'; - -export function TableSidePanel() { - const [applyHeatmap, resetHeatmap] = useMetadatHeatmap(); - const metadata = useMetadata(); - - React.useEffect(() => { - applyHeatmap(); - }, [metadata]); - - React.useEffect(() => { - applyHeatmap(); - return () => { - resetHeatmap(); - }; - }, []); - - return ( - - - - - - - - - - ); -} diff --git a/old/studio/src/components/Editor/Tables.tsx b/old/studio/src/components/Editor/Tables.tsx deleted file mode 100644 index f3d1697b..00000000 --- a/old/studio/src/components/Editor/Tables.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import { assignDataNoDelete } from '@utils/metadata'; - -import { Empty } from '@elements/Empty'; - -import { useLogger } from '@hooks/useLogger'; -import { useUpdateMetadata } from '@hooks/useMetadataUpdate'; -import { useSelected } from '@hooks/useSelected'; -import { useTabelsEmpty } from '@hooks/useTabeIsEmpty'; - -import { Sheet } from './TableSheet'; - -export function Tables() { - const selected = useSelected(); - const updateMetadata = useUpdateMetadata(); - const logger = useLogger(); - const empty = useTabelsEmpty(); - - const handleAssignToGeometry = React.useCallback( - (data: any) => { - if (Object.keys(data).length === 0) { - logger('No data to assign, check is there are any rows with type "key" assigned'); - return; - } - - assignDataNoDelete(selected, data); - updateMetadata(); - logger(`Assigned data to ${selected.size} models`); - }, - [selected] - ); - - if (empty) return No tables; - return ; -} diff --git a/old/studio/src/components/Editor/TransformSidePanel.tsx b/old/studio/src/components/Editor/TransformSidePanel.tsx deleted file mode 100644 index da25f103..00000000 --- a/old/studio/src/components/Editor/TransformSidePanel.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Allotment } from 'allotment'; -import 'allotment/dist/style.css'; - -import { ColumnContainer, OverflowContainer } from '@elements/Containers'; -import { PanelTitle } from '@elements/PanelTitle'; - -import { ModelList } from './ModelList'; -import { Modifiers } from './Modifiers'; - -export function TransformSidePanel() { - return ( - - - - - - - - - - - - - - - - - ); -} diff --git a/old/studio/src/components/Editor/Widget.tsx b/old/studio/src/components/Editor/Widget.tsx deleted file mode 100644 index 200f2ecf..00000000 --- a/old/studio/src/components/Editor/Widget.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { EditorModel } from '@data/EditorModel'; - -export interface WidgetProps { - model: EditorModel; -} diff --git a/old/studio/src/components/Editor/WidgetDeleteModel.tsx b/old/studio/src/components/Editor/WidgetDeleteModel.tsx deleted file mode 100644 index b9627d2f..00000000 --- a/old/studio/src/components/Editor/WidgetDeleteModel.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { FiDelete } from 'react-icons/fi'; - -import { Widget, WidgetDescription, WidgetLine, WidgetTitle } from '@elements/Widgets'; - -import { useRemoveModels } from '@hooks/useRemoveModels'; - -import { WidgetProps } from './Widget'; - -export function DeleteModelWidget(props: WidgetProps) { - const remove = useRemoveModels(); - - const apply = () => { - remove([props.model]); - }; - - return ( - - - - - Delete Model - - - - Delete the model from the scene. - - - ); -} diff --git a/old/studio/src/components/Editor/WidgetDeleteSubmodels.tsx b/old/studio/src/components/Editor/WidgetDeleteSubmodels.tsx deleted file mode 100644 index e13e652e..00000000 --- a/old/studio/src/components/Editor/WidgetDeleteSubmodels.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FiDelete } from 'react-icons/fi'; - -import { Widget, WidgetDescription, WidgetLine, WidgetTitle } from '@elements/Widgets'; - -import { useProcessing } from '@hooks/useProcessing'; -import { useRemoveSubmodels } from '@hooks/useRemoveSubmodels'; -import { useSelected } from '@hooks/useSelected'; - -import { WidgetProps } from './Widget'; - -export function DeleteSubmodelsWidget(props: WidgetProps) { - const removeSubmodels = useRemoveSubmodels(); - const selection = useSelected(); - const [, setProcessing] = useProcessing(); - - const apply = async () => { - const selectedSubmodels = selection.get(props.model); - if (!selectedSubmodels) return; //TODO handle with a popup - setProcessing(true, 'Deleting submodels...'); - await removeSubmodels(props.model, selectedSubmodels); - setProcessing(false, 'Finished deleting'); - }; - - return ( - - - - - Delete Selected - - - - Remove the selected submodels from the model. - - - ); -} diff --git a/old/studio/src/components/Editor/WidgetJoinSubmodels.tsx b/old/studio/src/components/Editor/WidgetJoinSubmodels.tsx deleted file mode 100644 index 6f1f5305..00000000 --- a/old/studio/src/components/Editor/WidgetJoinSubmodels.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { TbLayersUnion } from 'react-icons/tb'; - -import { Widget, WidgetDescription, WidgetLine, WidgetTitle } from '@elements/Widgets'; - -import { useJoinSubmodels } from '@hooks/useJoinSubmodels'; -import { useProcessing } from '@hooks/useProcessing'; -import { useSelected } from '@hooks/useSelected'; - -import { WidgetProps } from './Widget'; - -export function JoinSubmodelWidget(props: WidgetProps) { - const join = useJoinSubmodels(); - const [, setProcessing] = useProcessing(); - const selection = useSelected(); - - const apply = async () => { - const submodelIDs = selection.get(props.model); - if (!submodelIDs) return; //TODO handle with a popup - setProcessing(true, 'Joining submodels...'); - await join(props.model, submodelIDs); - setProcessing(false, 'Finished joining submodels'); - }; - - return ( - - - - - Join Submodels - - - - - Join selected submodels into a single submodel. - - - - ); -} diff --git a/old/studio/src/components/Editor/WidgetMapping.tsx b/old/studio/src/components/Editor/WidgetMapping.tsx deleted file mode 100644 index eb85e8f1..00000000 --- a/old/studio/src/components/Editor/WidgetMapping.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { BsLayersFill, BsLayersHalf } from 'react-icons/bs'; - -import { - Widget, - WidgetButton, - WidgetDescription, - WidgetLine, - WidgetPrompt, - WidgetTitle, -} from '@elements/Widgets'; - -import { EditorModel } from '@data/EditorModel'; - -import { useModels } from '@hooks/useModels'; -import { useProcessing } from '@hooks/useProcessing'; -import { useProjectModels } from '@hooks/useProjectModels'; - -import { WidgetProps } from './Widget'; - -export function MappingWidget(props: WidgetProps) { - const project = useProjectModels(); - const models = useModels(); - const [, setProcessing] = useProcessing(); - - const apply = async (target: EditorModel) => { - setProcessing(true, 'Projecting models...'); - await project(props.model, target); - setProcessing(false, 'Finished projecting'); - }; - - return ( - - - - - Projection Mapping - - - - - Project the geometry of the selected model onto another model. - - - - Select a model to project onto: - - {models.map((model) => { - if (model === props.model) return null; - return ( - - apply(model)}> - - {model.name} - - - ); - })} - - ); -} diff --git a/old/studio/src/components/Editor/WidgetSplitModel.tsx b/old/studio/src/components/Editor/WidgetSplitModel.tsx deleted file mode 100644 index 1a461b15..00000000 --- a/old/studio/src/components/Editor/WidgetSplitModel.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { TbLayersIntersect } from 'react-icons/tb'; - -import { Widget, WidgetDescription, WidgetLine, WidgetTitle } from '@elements/Widgets'; - -import { useProcessing } from '@hooks/useProcessing'; -import { useSelected } from '@hooks/useSelected'; -import { useSplitModel } from '@hooks/useSplitModel'; - -import { WidgetProps } from './Widget'; - -export function SplitModelWidget(props: WidgetProps) { - const split = useSplitModel(); - const selection = useSelected(); - const [, setProcessing] = useProcessing(); - - const apply = async () => { - const submodelIDs = selection.get(props.model); - if (!submodelIDs) return; //TODO handle with a popup - setProcessing(true, 'Splitting model...'); - await split(props.model, submodelIDs); - setProcessing(false, 'Model splitting finised'); - }; - - return ( - - - - - Split Model - - - - - Split the model based on the current selection, the selected parts will be - removed from the original model and placed into a new model. - - - - ); -} diff --git a/old/studio/src/components/Editor/WidgetTransformation.tsx b/old/studio/src/components/Editor/WidgetTransformation.tsx deleted file mode 100644 index 25d15b04..00000000 --- a/old/studio/src/components/Editor/WidgetTransformation.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import clsx from 'clsx'; -import { vec3 } from 'gl-matrix'; -import React from 'react'; -import { TiArrowMove } from 'react-icons/ti'; - -import * as GL from '@bananagl/bananagl'; - -import { Input } from '@elements/Input'; -import { Widget, WidgetDescription, WidgetLine, WidgetTitle } from '@elements/Widgets'; - -import { useRenderer } from '@hooks/useRender'; - -import { WidgetProps } from './Widget'; - -function CustomInput(props: React.InputHTMLAttributes) { - const { className, ...rest } = props; - return ( - - ); -} - -function VectorComponentInput(props: { - label: string; - value: number; - onChange: (value: number) => void; -}) { - const { label, value, onChange } = props; - - return ( - - onChange(parseFloat(e.target.value))} - title={label} - /> - - ); -} - -interface VectorControlsProps { - title: string; - value: vec3; - onChange: (value: vec3) => void; -} - -function VectorControls(props: VectorControlsProps) { - const { value, onChange } = props; - const [x, y, z] = value; - - return ( - - {props.title} - onChange([v, y, z])} /> - onChange([x, v, z])} /> - onChange([x, y, v])} /> - - ); -} - -export function ModelTransformationWidget(props: WidgetProps) { - const { model } = props; - const renderer = useRenderer(); - const [position, setPosition] = React.useState(model.position); - const [rotation, setRotation] = React.useState(model.rotation); - const [scale, setScale] = React.useState(model.scale); - - const [moveShortcut] = React.useState(new GL.ShortcutOnMouseMove('KeyG')); - const [scaleShortcut] = React.useState(new GL.ShortcutOnMouseMove('KeyS')); - - React.useEffect(() => { - renderer.onInit = () => { - const controls = renderer.window.controls; - controls.addShortcut(moveShortcut); - controls.addShortcut(scaleShortcut); - }; - - return () => { - const controls = renderer.window.controls; - controls.removeShortcut(moveShortcut); - controls.removeShortcut(scaleShortcut); - }; - }, [renderer]); - - React.useEffect(() => { - moveShortcut.onTrigger = (view, dx, dy) => { - const dir = view.camera.cameraPlaneVector(dx, dy); - setPosition((p) => vec3.add(p, p, dir).slice() as vec3); - }; - - moveShortcut.onStartCall = () => { - moveShortcut.storage = model.position.slice() as vec3; - }; - - moveShortcut.onCancelCall = (data) => { - if (data) setPosition(data.slice() as vec3); - }; - - scaleShortcut.onTrigger = (view, dx, dy) => { - let fact = dx > 0 ? 1.01 : 1; - fact = dx < 0 ? 0.99 : fact; - setScale((s) => vec3.scale(s, s, fact).slice() as vec3); - }; - - scaleShortcut.onStartCall = () => { - scaleShortcut.storage = model.scale.slice() as vec3; - }; - - scaleShortcut.onCancelCall = (data) => { - if (data) setScale(data.slice() as vec3); - }; - }, [model, moveShortcut, scaleShortcut]); - - React.useEffect(() => { - setPosition(model.position); - setRotation(model.rotation); - setScale(model.scale); - }, [model]); - - React.useEffect(() => { - if (position.some((v) => isNaN(v))) return; - model.position = position; - }, [position, model]); - - React.useEffect(() => { - if (rotation.some((v) => isNaN(v))) return; - model.rotation = rotation; - }, [rotation, model]); - - React.useEffect(() => { - if (scale.some((v) => isNaN(v))) return; - model.scale = scale; - }, [scale, model]); - - return ( - - - - - Transform Model - - - - Shift, Scale and Rotate the model - - - - - - - -
-
- ); -} diff --git a/old/studio/src/components/Editor/main.tsx b/old/studio/src/components/Editor/main.tsx deleted file mode 100644 index 7d8b8896..00000000 --- a/old/studio/src/components/Editor/main.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { RouterProvider, createBrowserRouter } from 'react-router-dom'; - -import { ErrorPage } from '@elements/Error'; - -import { ProcessingContext } from '@context/ProcessingContext'; -import { ViewContext } from '@context/ViewContext'; - -import '@assets/index.css'; - -import { TablesContext } from '../../context/TablesContext'; -import { ModelEditor } from './Editor'; - -const router = createBrowserRouter([ - { - path: '/editor', - element: , - errorElement: , - }, -]); - -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - - - - - -); diff --git a/old/studio/src/components/Elements/Button.tsx b/old/studio/src/components/Elements/Button.tsx deleted file mode 100644 index 6692c897..00000000 --- a/old/studio/src/components/Elements/Button.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -import { Input } from './Input'; - -export function Button(props: { - children: React.ReactNode; - disabled?: boolean; - onClick?: React.MouseEventHandler; -}) { - return ( - - ); -} - -export function ButtonFileInput(props: { - children: React.ReactNode; - onChange?: React.ChangeEventHandler; - multiple?: boolean; - id: string; - className?: string; - accept?: string; -}) { - return ( - <> - - - - ); -} diff --git a/old/studio/src/components/Elements/ButtonMenu.tsx b/old/studio/src/components/Elements/ButtonMenu.tsx deleted file mode 100644 index a2b99459..00000000 --- a/old/studio/src/components/Elements/ButtonMenu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import clsx from 'clsx'; - -interface MenuButtonProps { - children: React.ReactNode; - tipTitle?: string; - active?: boolean; - disabled?: boolean; - onClick?: () => void; -} - -export function MenuButton(props: MenuButtonProps) { - const { active, disabled } = props; - - return ( - - ); -} - -export function MenuGroup(props: { children: React.ReactNode }) { - return
{props.children}
; -} diff --git a/old/studio/src/components/Elements/ColorPicker.tsx b/old/studio/src/components/Elements/ColorPicker.tsx deleted file mode 100644 index 66b5f7ed..00000000 --- a/old/studio/src/components/Elements/ColorPicker.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { hexToHsva, hsvaToHex } from '@uiw/color-convert'; -import { Wheel } from '@uiw/react-color'; -import clsx from 'clsx'; -import React from 'react'; - -import { Input } from '@elements/Input'; - -export interface ColorPickerProps { - color: string; - onChange: (color: string) => void; -} - -export function ColorPicker(props: ColorPickerProps) { - const { color, onChange } = props; - - const [hsva, setHsva] = React.useState(hexToHsva(color)); - const [hex, setHex] = React.useState(color); - const [isValid, setIsValid] = React.useState(true); - - React.useEffect(() => { - onChange(hsvaToHex(hsva)); - if (hex !== hsvaToHex(hsva)) setHex(hsvaToHex(hsva)); - }, [hsva]); - - React.useEffect(() => { - if (validateHexColor(hex)) { - if (hex !== hsvaToHex(hsva)) setHsva(hexToHsva(hex)); - setIsValid(true); - } else { - setIsValid(false); - } - }, [hex]); - - const reset = () => { - setHsva(hexToHsva('#eeeeee')); - }; - - const validateHexColor = (color: string) => { - const regex = /^#([A-Fa-f0-9]{6})$/; - return regex.test(color); - }; - - return ( -
- setHsva(color.hsva)} onDoubleClick={reset} /> -
- setHsva((hsva) => ({ ...hsva, v: Number(e.target.value) }))} - onDoubleClick={reset} - /> -
-
- { - setHex(e.target.value); - }} - /> -
-
- ); -} diff --git a/old/studio/src/components/Elements/Containers.tsx b/old/studio/src/components/Elements/Containers.tsx deleted file mode 100644 index 92447473..00000000 --- a/old/studio/src/components/Elements/Containers.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -export function OverflowContainer(props: { children: React.ReactNode; className?: string }) { - return ( -
{props.children}
- ); -} - -export function OverflowAbsoluteContainer(props: { - children: React.ReactNode; - className?: string; -}) { - return ( - - {props.children} - - ); -} - -export function ColumnContainer(props: { children: React.ReactNode; className?: string }) { - return ( -
{props.children}
- ); -} - -export function RowContainer(props: { children: React.ReactNode; className?: string }) { - return ( -
{props.children}
- ); -} - -export function StretchContainer(props: { children: React.ReactNode; className?: string }) { - return ( -
- {props.children} -
- ); -} - -export function BottomRowContainer(props: { - children: React.ReactNode; - className?: string; - style?: React.CSSProperties; -}) { - return ( -
- {props.children} -
- ); -} diff --git a/old/studio/src/components/Elements/Dialog.tsx b/old/studio/src/components/Elements/Dialog.tsx deleted file mode 100644 index 1e72049c..00000000 --- a/old/studio/src/components/Elements/Dialog.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Dialog, Transition } from '@headlessui/react'; -import clsx from 'clsx'; -import React from 'react'; - -function dialogClassNames(props: { - title: string; - body: string; - onClick?: (() => void) | undefined; - href?: string | undefined; - className?: string | undefined; - secondary?: boolean | undefined; -}) { - return clsx( - 'w-full text-left rounded-md px-4 py-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', - 'bg-neutral-200 text-neutral-900 hover:bg-neutral-200 focus-visible:ring-neutral-500', - 'dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 dark:focus-visible:ring-neutral-500', - props.className - ); -} - -export function DialogOption(props: { - title: string; - body: string; - onClick?: () => void; - href?: string; - className?: string; - secondary?: boolean; -}) { - const sharedClasses = dialogClassNames(props); - - if (props.onClick) { - return ( - - ); - } - - if (props.href) { - return ( - - {props.title} - {props.body} - - ); - } - - return null; -} - -export function DialogButton(props: { - title: string; - body: string; - onClick?: () => void; - href?: string; - className?: string; - secondary?: boolean; -}) { - const sharedClasses = dialogClassNames(props); - - return ( - - ); -} - -export function OverlayDialog(props: { - isOpen: boolean; - onClose: () => void; - children: React.ReactNode; -}) { - const { isOpen, onClose } = props; - - return ( - - - -
- - -
-
- - - {props.children} - - -
-
-
-
- ); -} diff --git a/old/studio/src/components/Elements/Empty.tsx b/old/studio/src/components/Elements/Empty.tsx deleted file mode 100644 index a9b6df5a..00000000 --- a/old/studio/src/components/Elements/Empty.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -export function Empty(props: { children?: React.ReactNode }) { - return ( -
-
- {props.children} -
-
- ); -} diff --git a/old/studio/src/components/Elements/Error.tsx b/old/studio/src/components/Elements/Error.tsx deleted file mode 100644 index 594cb364..00000000 --- a/old/studio/src/components/Elements/Error.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { Link, useRouteError } from 'react-router-dom'; - -export function ErrorPage() { - const error = useRouteError() as any; - console.error(error); - - return ( -
-

Ooops!

-

Sorry, an unexpected error has occurred.

- -
Go back to the main page
- -
- Please contact the system administrator with the following message: -
-

{error.statusText || error.message}

-

{error.stack}

-
- ); -} diff --git a/old/studio/src/components/Elements/Icons.tsx b/old/studio/src/components/Elements/Icons.tsx deleted file mode 100644 index 59a6a237..00000000 --- a/old/studio/src/components/Elements/Icons.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import React from 'react'; - -interface IconProps { - className?: string; -} - -export function CubeEmpty(props: IconProps) { - return ( - - - - ); -} - -export function CubeLeft(props: IconProps) { - return ( - - - - ); -} - -export function CubeRight(props: IconProps) { - return ( - - - - ); -} - -export function CubeTop(props: IconProps) { - return ( - - - - ); -} - -export function TriangleFull(props: IconProps) { - return ( - - - - - - - - - - - ); -} - -export function TriangleFullFilled(props: IconProps) { - return ( - - - - - - - - - - - ); -} - -export function MouseLeft(props: IconProps) { - return ( - - - - - - ); -} - -export function MouseRight(props: IconProps) { - return ( - - - - - - ); -} - -export function MouseWheel(props: IconProps) { - return ( - - - - - - ); -} diff --git a/old/studio/src/components/Elements/Input.tsx b/old/studio/src/components/Elements/Input.tsx deleted file mode 100644 index 65034e6e..00000000 --- a/old/studio/src/components/Elements/Input.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -//use with onChange to update state -export function Input(props: React.InputHTMLAttributes) { - const className = props.className || ''; - return ( - e.stopPropagation()} - onKeyUp={(e) => e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - /> - ); -} diff --git a/old/studio/src/components/Elements/List.tsx b/old/studio/src/components/Elements/List.tsx deleted file mode 100644 index 5e52c3d7..00000000 --- a/old/studio/src/components/Elements/List.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; -import { FiChevronRight } from 'react-icons/fi'; -import { VscJson, VscSymbolColor } from 'react-icons/vsc'; - -export type ButtonHandler = (e: React.MouseEvent) => void; - -interface ListButtonBase { - onClick?: ButtonHandler; - title?: string; -} - -interface ListBaseButtonProps extends ListButtonBase { - children: React.ReactNode; - disabled?: boolean; -} - -function ListBaseButton(props: ListBaseButtonProps & { children: React.ReactNode }) { - return ( - - ); -} - -interface ChevronButtonProps extends ListButtonBase { - open: boolean; -} - -export function ChevronButton(props: ChevronButtonProps) { - return ( - - - - ); -} - -export function BracketsButton(props: ListButtonBase) { - return ( - - - - ); -} - -export function StyleButton(props: ListButtonBase) { - return ( - - - - ); -} - -interface ListButtonProps extends ListBaseButtonProps { - padded?: boolean; - className?: string; -} - -export function ListButton(props: ListButtonProps) { - return ( - - ); -} - -interface ListItemProps { - hoverable?: boolean; - children: React.ReactNode; - active?: boolean; - depth?: number; -} - -export function ListItem(props: ListItemProps) { - return ( -
- {props.children} -
- ); -} - -interface ListGroupGroupProps { - children: React.ReactNode; -} - -export function ListGroup(props: ListGroupGroupProps) { - return
{props.children}
; -} - -export function ListGroupChildren(props: ListGroupGroupProps) { - return
{props.children}
; -} diff --git a/old/studio/src/components/Elements/PanelTitle.tsx b/old/studio/src/components/Elements/PanelTitle.tsx deleted file mode 100644 index 1efd68c3..00000000 --- a/old/studio/src/components/Elements/PanelTitle.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function PanelTitle(props: { title: string }) { - return
{props.title}
; -} diff --git a/old/studio/src/components/Elements/Range.tsx b/old/studio/src/components/Elements/Range.tsx deleted file mode 100644 index abb3d08b..00000000 --- a/old/studio/src/components/Elements/Range.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -export function Range(props: { - value: number; - min: number; - max: number; - onChange: (value: number) => void; -}) { - const { value, min, max, onChange } = props; - - const updateValue = (e: React.ChangeEvent) => { - const i = parseFloat(e.target.value); - if (isNaN(i)) return; - else onChange(i); - }; - - return ( - - ); -} diff --git a/old/studio/src/components/Elements/SizeGuard.tsx b/old/studio/src/components/Elements/SizeGuard.tsx deleted file mode 100644 index 71b99483..00000000 --- a/old/studio/src/components/Elements/SizeGuard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -import cat from '@assets/cat.gif'; - -function UnsuitableSize() { - return ( -
- cat -

- Sorry, Metacity Studio requires larger screen to - display the content. -

-
- ); -} - -interface SizeGuardProps { - children: React.ReactNode; - minWidth: number; - minHeight: number; -} - -export function SizeGuard(props: SizeGuardProps) { - const [width, setWidth] = React.useState(0); - const [height, setHeight] = React.useState(0); - - const ref = React.useRef(null); - - React.useEffect(() => { - const resize = () => { - if (ref.current) { - setWidth(ref.current.clientWidth); - setHeight(ref.current.clientHeight); - } - }; - - resize(); - - window.addEventListener('resize', resize); - return () => { - window.removeEventListener('resize', resize); - }; - }, []); - - return ( -
- {(width < props.minWidth || height < props.minHeight) && } - {props.children} -
- ); -} diff --git a/old/studio/src/components/Elements/Tabs.tsx b/old/studio/src/components/Elements/Tabs.tsx deleted file mode 100644 index 92440404..00000000 --- a/old/studio/src/components/Elements/Tabs.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Tab } from '@headlessui/react'; -import clsx from 'clsx'; -import React from 'react'; - -export function TabButton(props: { children: React.ReactNode; title: string }) { - return ( - - selected ? 'active-no-background tab-button' : 'base-no-background tab-button' - } - title={props.title} - > - {props.children} - - ); -} - -export function TabPanel(props: { children: React.ReactNode }) { - return {props.children}; -} - -//containers -export function TabPanels(props: { children: React.ReactNode }) { - return {props.children}; -} - -export function TabList(props: { children: React.ReactNode; className?: string }) { - return ( - - {props.children} - - ); -} - -export function TabGroup(props: { children: React.ReactNode }) { - return ( -
- {props.children} -
- ); -} diff --git a/old/studio/src/components/Elements/TitleChain.tsx b/old/studio/src/components/Elements/TitleChain.tsx deleted file mode 100644 index 80107f59..00000000 --- a/old/studio/src/components/Elements/TitleChain.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { FiChevronRight } from 'react-icons/fi'; - -export function TitleChain(props: { categories: string[] }) { - let { categories } = props; - return ( - <> - {categories.map((category, index) => { - return ( - - - {category} - - {index < categories.length - 1 && ( - - )} - - ); - })} - - ); -} diff --git a/old/studio/src/components/Elements/Widgets.tsx b/old/studio/src/components/Elements/Widgets.tsx deleted file mode 100644 index 61d283dd..00000000 --- a/old/studio/src/components/Elements/Widgets.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -export function Widget(props: { children?: React.ReactNode; onClick?: () => void }) { - return ( -
- {props.children} -
- ); -} - -export function WidgetLine(props: { children?: React.ReactNode; className?: string }) { - return ( -
- {props.children} -
- ); -} - -export function WidgetTitle(props: { children?: React.ReactNode }) { - return
{props.children}
; -} - -export function WidgetDescription(props: { children?: React.ReactNode }) { - return
{props.children}
; -} - -export function WidgetPrompt(props: { children?: React.ReactNode }) { - return
{props.children}
; -} - -export function WidgetButton(props: { children?: React.ReactNode; onClick?: () => void }) { - return ( -
- {props.children} -
- ); -} diff --git a/old/studio/src/components/Privacy/main.tsx b/old/studio/src/components/Privacy/main.tsx deleted file mode 100644 index e97cd999..00000000 --- a/old/studio/src/components/Privacy/main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; - -import '@assets/index.css'; -import '@assets/static.css'; diff --git a/old/studio/src/components/Shared/AutoLoader.tsx b/old/studio/src/components/Shared/AutoLoader.tsx deleted file mode 100644 index cdbf394b..00000000 --- a/old/studio/src/components/Shared/AutoLoader.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; - -import { loadProjectFiles } from '@utils/formats/loader'; - -import { CoordinateMode, useImportModels } from '@hooks/useImportModels'; -import { useProcessing } from '@hooks/useProcessing'; -import { useUpdateStyles } from '@hooks/useStyleUpdate'; - -export function AutoLoader() { - const importModels = useImportModels(); - const updateStyles = useUpdateStyles(); - const [, setProcessing] = useProcessing(); - const [lstyles, setLStyles] = React.useState(); - - React.useEffect(() => { - //get model param from url - const urlParams = new URLSearchParams(window.location.search); - const modelParam = urlParams.get('model'); - const styleParam = urlParams.get('style'); - - //fetch model from url - const load = async () => { - let buffer, stylesData; - const name = 'Demomodel.mcmodel'; - - if (modelParam) { - setProcessing(true, 'Loading model...'); - const modelURL = new URL(`https://${modelParam}`); - const modelResponse = await fetch(modelURL); - buffer = await modelResponse.arrayBuffer(); - if (styleParam) { - setProcessing(true, 'Loading styles...'); - const styleURL = new URL(`https://${styleParam}`); - const data = await fetch(styleURL); - stylesData = await data.json(); - } - - setProcessing(true, 'Parsing files...'); - const { models, styles } = await loadProjectFiles(name, buffer, stylesData); - - setProcessing(true, 'Building BVH...'); - await importModels(models, { - coordMode: CoordinateMode.Keep, - }); - - if (styles) setLStyles(styles); - setProcessing(false, 'Finished auto-loading models'); - } - }; - - load(); - }, []); - - React.useEffect(() => { - if (lstyles) { - updateStyles(lstyles); - setLStyles(undefined); - } - }, [lstyles]); - - return null; -} diff --git a/old/studio/src/components/Shared/Canvas.tsx b/old/studio/src/components/Shared/Canvas.tsx deleted file mode 100644 index 4f04c03a..00000000 --- a/old/studio/src/components/Shared/Canvas.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { EditorModel } from 'data/EditorModel'; -import React from 'react'; - -import * as GL from '@bananagl/bananagl'; - -import { SelectionType } from '@context/ViewContext'; - -import { useRenderer } from '@hooks/useRender'; -import { useSelection } from '@hooks/useSelection'; - -type SelectionArrayType = { - object: GL.Pickable; - primitiveIndices: number[]; -}[]; - -type SelectionSingleType = { - object: GL.Pickable; - primitiveIndices: number; -}; - -type SelectionOutput = SelectionArrayType | SelectionSingleType; - -interface CanvasProps { - canvasRef: React.RefObject; - onTooltip?: (meta: any, x: number, y: number) => void; - onHideTooltip?: () => void; -} - -export function Canvas(props: CanvasProps) { - const renderer = useRenderer(); - const select = useSelection(); - const timerRef = React.useRef(); - - function handlePick(selection: SelectionOutput, shiftKey: boolean) { - const multiselect = Array.isArray(selection); - const selectionObj = primitiveIndicesToSubmodelIndices(selection); - let { toggle, extend } = selectionFlags(multiselect, shiftKey); - select(selectionObj, toggle, extend); - } - - const handleWheel = (event: WheelEvent) => { - event.preventDefault(); - }; - - const handlePointerMove = (event: PointerEvent) => { - if (!props.onTooltip || !props.onHideTooltip) return; - if (timerRef.current) clearTimeout(timerRef.current); - props.onHideTooltip!(); - timerRef.current = setTimeout(() => { - const selection = renderer.controls?.pointerHover(event); - if (!selection) return; - const selectionObj = primitiveIndicesToSubmodelIndices(selection); - const model = selectionObj.keys().next().value; - const submodel = selectionObj.get(model)?.values().next().value; - const metadata = model.metadata[submodel]; - - if (!metadata) return; - - const { xpos, ypos } = getOffset(event); - props.onTooltip!(metadata, xpos, ypos); - }, 100); - }; - - const handlePointerLeave = (event: PointerEvent) => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - - React.useEffect(() => { - const canvas = props.canvasRef.current; - if (!canvas) return; - canvas.addEventListener('wheel', handleWheel); - return () => { - canvas.removeEventListener('wheel', handleWheel); - }; - }, [props.canvasRef]); - - return ( - { - renderer.controls?.pointerDown(e.nativeEvent); - }} - onPointerMove={(e) => { - renderer.controls?.pointerMove(e.nativeEvent); - handlePointerMove(e.nativeEvent); - }} - onPointerUp={(e) => { - let selection = renderer.controls?.pointerUp(e.nativeEvent); - const shift = renderer.controls?.keyboard.keyMap.shift ?? false; - if (selection) handlePick(selection, shift); - //else deselecteAll(); //TODO handle only if clicked, no drag and move - }} - onPointerLeave={(e) => { - renderer.controls?.pointerOut(e.nativeEvent); - handlePointerLeave(e.nativeEvent); - }} - onWheel={(e) => { - renderer.controls?.wheel(e.nativeEvent); - }} - onPointerOut={(e) => { - renderer.controls?.pointerOut(e.nativeEvent); - }} - onContextMenu={(e) => { - renderer.controls?.contextMenu(e.nativeEvent); - }} - /> - ); -} - -function primitiveIndicesToSubmodelIndices(selection: SelectionOutput) { - const selectedMap: SelectionType = new Map(); - - const arrayedSelection = Array.isArray(selection) - ? selection - : [ - { - object: selection.object, - primitiveIndices: [selection.primitiveIndices], - }, - ]; - - for (const { object, primitiveIndices } of arrayedSelection) { - const submodel = object.attributes.getAttribute('submodel') as GL.Attribute; - const submodelIds = submodel.buffer.getView(Uint32Array); - const submodelIDs = new Set(); - for (const idx of primitiveIndices) submodelIDs.add(submodelIds[idx * 3]); - selectedMap.set(object as EditorModel, submodelIDs); - } - - return selectedMap; -} - -function selectionFlags(multiselect: boolean, shiftKey: boolean) { - let toggle = false; - let extend = false; - - if (multiselect) { - if (shiftKey) extend = true; - else toggle = true; - } else if (shiftKey) toggle = true; - return { toggle, extend }; -} - -function getOffset(event: any) { - const xpos: number = (!event.offsetX ? event.layerX! : event.offsetX) ?? 0; - const ypos: number = (!event.offsetY ? event.layerY! : event.offsetY) ?? 0; - return { xpos, ypos }; -} diff --git a/old/studio/src/components/Shared/CanvasWrapper.tsx b/old/studio/src/components/Shared/CanvasWrapper.tsx deleted file mode 100644 index 07aa2c3f..00000000 --- a/old/studio/src/components/Shared/CanvasWrapper.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; - -import * as GL from '@bananagl/bananagl'; - -import { GridModel } from '@data/GridModel'; - -import { useDarkmode } from '@hooks/useDarkmode'; -import { useRenderer } from '@hooks/useRender'; -import { useScene } from '@hooks/useScene'; -import { useTooltip } from '@hooks/useTooltip'; - -import { Canvas } from './Canvas'; - -export function CanvasWrapper() { - const canvasRef = React.useRef(null); - const scene = useScene(); - const renderer = useRenderer(); - const darkmode = useDarkmode(); - const [_, setTooltip] = useTooltip(); - - React.useEffect(() => { - if (canvasRef.current && renderer) { - GL.mountRenderer(canvasRef.current, renderer, {}, [ - { - view: new GL.View(scene), - size: { - mode: 'relative', - width: 100, - height: 100, - }, - position: { - mode: 'relative', - top: 0, - left: 0, - }, - }, - ]); - - const grid = new GridModel(); - scene.add(grid); - - if (darkmode) renderer.clearColor = [0.1, 0.1, 0.1, 1]; - else renderer.clearColor = [1, 1, 1, 1]; - - const down = (e: KeyboardEvent) => { - renderer.window.controls.keydown(e); - }; - - const up = (e: KeyboardEvent) => { - renderer.window.controls.keyup(e); - }; - - document.addEventListener('keydown', down); - document.addEventListener('keyup', up); - - return () => { - document.removeEventListener('keydown', down); - document.removeEventListener('keyup', up); - GL.unmountRenderer(renderer); - }; - } - }, [renderer, scene]); - - const handleTooltip = (data: any, x: number, y: number) => { - setTooltip({ data, x, y }); - }; - - const handleHideTooltip = () => { - setTooltip(null); - }; - - return ( - - ); -} diff --git a/old/studio/src/components/Shared/Controls.tsx b/old/studio/src/components/Shared/Controls.tsx deleted file mode 100644 index effc81b3..00000000 --- a/old/studio/src/components/Shared/Controls.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import clsx from 'clsx'; - -import { useGreyscale } from '@hooks/useGreyscale'; - -import { DarkmodeControls } from './ControlsDarkmode'; -import { DirectionControls } from './ControlsDirection'; -import { ProjectionControls } from './ControlsProjection'; -import { ScreenshotControls } from './ControlsScreenshot'; -import { SelectionControls } from './ControlsSelect'; -import { ShaderControls } from './ControlsShader'; - -export function Controls() { - const [greyscale] = useGreyscale(); - return ( - <> -
- - - - - -
-
- -
- - ); -} diff --git a/old/studio/src/components/Shared/ControlsDarkmode.tsx b/old/studio/src/components/Shared/ControlsDarkmode.tsx deleted file mode 100644 index b1ef1cbb..00000000 --- a/old/studio/src/components/Shared/ControlsDarkmode.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { MdOutlineDarkMode, MdOutlineLightMode } from 'react-icons/md'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; - -import { useDarkmode } from '@hooks/useDarkmode'; - -export function DarkmodeControls() { - const [darkmode, setDarkmode] = useDarkmode(); - - return ( - - setDarkmode(!true)} - tipTitle="Perspective Camera" - active={!darkmode} - > - - - setDarkmode(true)} - tipTitle="Orthographic Camera" - active={darkmode} - > - - - - ); -} diff --git a/old/studio/src/components/Shared/ControlsDirection.tsx b/old/studio/src/components/Shared/ControlsDirection.tsx deleted file mode 100644 index 6fdaec2d..00000000 --- a/old/studio/src/components/Shared/ControlsDirection.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; - -import * as GL from '@bananagl/bananagl'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; -import { CubeEmpty, CubeLeft, CubeRight, CubeTop } from '@elements/Icons'; - -import { useActiveView } from '@hooks/useActiveView'; -import { useRenderer } from '@hooks/useRender'; - -export function DirectionControls() { - const [mode, setMode] = React.useState(GL.CameraView.Free); - const renderer = useRenderer(); - const activeView = useActiveView(); - - const setView = (viewMode: GL.CameraView) => { - const view = renderer.views[activeView].view; - view.cameraLock.mode = viewMode; - setMode(viewMode); - }; - - React.useEffect(() => { - renderer.onInit = () => { - const controls = renderer.window.controls; - controls.addShortcut( - new GL.ShortcutOnPress('Digit7', (view: GL.View) => { - setView(GL.CameraView.Top); - }) - ); - - controls.addShortcut( - new GL.ShortcutOnPress('Digit1', (view: GL.View) => { - setView(GL.CameraView.Front); - }) - ); - - controls.addShortcut( - new GL.ShortcutOnPress('Digit3', (view: GL.View) => { - setView(GL.CameraView.Right); - }) - ); - - controls.addShortcut( - new GL.ShortcutOnPress('Digit5', (view: GL.View) => { - setView(GL.CameraView.Free); - }) - ); - }; - }, []); - - return ( - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/old/studio/src/components/Shared/ControlsProjection.tsx b/old/studio/src/components/Shared/ControlsProjection.tsx deleted file mode 100644 index 283aac0b..00000000 --- a/old/studio/src/components/Shared/ControlsProjection.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { BiRectangle } from 'react-icons/bi'; -import { TbPerspective } from 'react-icons/tb'; - -import * as GL from '@bananagl/bananagl'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; - -import { useActiveView } from '@hooks/useActiveView'; -import { useRenderer } from '@hooks/useRender'; - -export function ProjectionControls() { - const renderer = useRenderer(); - const activeView = useActiveView(); - - const [projection, setProjection] = React.useState( - GL.ProjectionType.ORTHOGRAPHIC - ); - - const setPerspective = () => { - const view = renderer.views[activeView].view; - view.camera.projectionType = GL.ProjectionType.PERSPECTIVE; - setProjection(GL.ProjectionType.PERSPECTIVE); - }; - - const setOrtho = () => { - const view = renderer.views[activeView].view; - view.camera.projectionType = GL.ProjectionType.ORTHOGRAPHIC; - setProjection(GL.ProjectionType.ORTHOGRAPHIC); - }; - - return ( - - - - - - - - - ); -} diff --git a/old/studio/src/components/Shared/ControlsScreenshot.tsx b/old/studio/src/components/Shared/ControlsScreenshot.tsx deleted file mode 100644 index 7ded57d7..00000000 --- a/old/studio/src/components/Shared/ControlsScreenshot.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { AiFillCamera } from 'react-icons/ai'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; - -import { useRenderer } from '@hooks/useRender'; - -export function ScreenshotControls() { - const renderer = useRenderer(); - - const saveCanvas = () => { - //save contents of the canvas to a png file - renderer.afterRenderOnce = () => { - const canvas: HTMLCanvasElement = renderer.window.rawCanvas; - - const image = canvas.toDataURL('image/png'); - const link = document.createElement('a'); - link.download = 'render.png'; - link.href = image; - link.click(); - }; - }; - - return ( - - - - - - ); -} diff --git a/old/studio/src/components/Shared/ControlsSelect.tsx b/old/studio/src/components/Shared/ControlsSelect.tsx deleted file mode 100644 index 70b8932e..00000000 --- a/old/studio/src/components/Shared/ControlsSelect.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { MdDeselect, MdSelectAll } from 'react-icons/md'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; - -import { useModels } from '@hooks/useModels'; -import { useSelection } from '@hooks/useSelection'; - -export function SelectionControls() { - const models = useModels(); - const select = useSelection(); - - const selectAll = () => { - const selected = new Map(); - models.forEach((model) => { - const submodels = Object.keys(model.metadata).map((key) => parseInt(key)); - selected.set(model, new Set(submodels)); - }); - select(selected); - }; - - const deselect = () => { - select(new Map()); - }; - - return ( - - - - - - - - - ); -} diff --git a/old/studio/src/components/Shared/ControlsShader.tsx b/old/studio/src/components/Shared/ControlsShader.tsx deleted file mode 100644 index 15552ad2..00000000 --- a/old/studio/src/components/Shared/ControlsShader.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import { TbVectorTriangle } from 'react-icons/tb'; - -import { MenuButton, MenuGroup } from '@elements/ButtonMenu'; -import { TriangleFull, TriangleFullFilled } from '@elements/Icons'; - -import { EditorModel } from '@data/EditorModel'; -import { GeometryMode } from '@data/types'; - -import { useScene } from '@hooks/useScene'; - -export function ShaderControls() { - const [geometryMode, setGeometryMode] = React.useState(GeometryMode.SOLID); - const scene = useScene(); - - const setWireframe = () => { - setGeometryMode(GeometryMode.WIREFRAME); - - scene.objects.forEach((obj) => { - if (obj instanceof EditorModel) { - obj.geometryMode = GeometryMode.WIREFRAME; - obj.attributes.needsRebind = true; - } - }); - - scene.shadersChanged = true; - }; - - const setSolid = () => { - setGeometryMode(GeometryMode.SOLID); - - scene.objects.forEach((obj) => { - if (obj instanceof EditorModel) { - obj.geometryMode = GeometryMode.SOLID; - obj.attributes.needsRebind = true; - } - }); - - scene.shadersChanged = true; - }; - - const setNoEdges = () => { - setGeometryMode(GeometryMode.NOEDGES); - - scene.objects.forEach((obj) => { - if (obj instanceof EditorModel) { - obj.geometryMode = GeometryMode.NOEDGES; - obj.attributes.needsRebind = true; - } - }); - - scene.shadersChanged = true; - }; - - return ( - - - - - - - - - - - - ); -} diff --git a/old/studio/src/components/Shared/DialogExport.tsx b/old/studio/src/components/Shared/DialogExport.tsx deleted file mode 100644 index 130a13f8..00000000 --- a/old/studio/src/components/Shared/DialogExport.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Dialog } from '@headlessui/react'; -import React from 'react'; - -import { DialogButton, OverlayDialog } from '@elements/Dialog'; -import { Input } from '@elements/Input'; - -export function ExportDialog(props: { isOpen: boolean; onClose: (title: string | null) => void }) { - const { isOpen, onClose } = props; - - const [title, setTitle] = React.useState('project'); - - return ( - onClose(null)}> - - Export - -
Enter project title
-
- setTitle(e.target.value)} - defaultValue={title} - /> -
-
-
Exports the project as 2 files:
-
-
    -
  • .mcmodel containing geometry
  • -
  • .mcstyle containing styles
  • -
-
-
The files can be loaded into Studio and Viewer.
-
-
- onClose(title)} - className="mt-2" - /> - onClose(null)} - className="mt-2" - secondary - /> -
-
- ); -} diff --git a/old/studio/src/components/Shared/DialogImport.tsx b/old/studio/src/components/Shared/DialogImport.tsx deleted file mode 100644 index 19ebdf2e..00000000 --- a/old/studio/src/components/Shared/DialogImport.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Dialog } from '@headlessui/react'; - -import { DialogOption, OverlayDialog } from '@elements/Dialog'; - -import { CoordinateMode } from '@hooks/useImportModels'; - -export function ImportDialog(props: { isOpen: boolean; onClose: (mode: CoordinateMode) => void }) { - const { isOpen, onClose } = props; - - return ( - {}}> - - Transforming Coordinates - -
- Select how to handle coordinates of the imported models: -
-
- onClose(CoordinateMode.Keep)} - /> - onClose(CoordinateMode.Center)} - className="mt-2" - /> -
-
- ); -} diff --git a/old/studio/src/components/Shared/IOMenu.tsx b/old/studio/src/components/Shared/IOMenu.tsx deleted file mode 100644 index 07d4e215..00000000 --- a/old/studio/src/components/Shared/IOMenu.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from 'react'; - -import { load } from '@utils/formats/loader'; - -import { Button, ButtonFileInput } from '@elements/Button'; - -import { ModelData, Style } from '@data/types'; - -import { useExportModels } from '@hooks/useExportModels'; -import { CoordinateMode, useImportModels } from '@hooks/useImportModels'; -import { useProcessing } from '@hooks/useProcessing'; -import { useRenderer } from '@hooks/useRender'; -import { useScene } from '@hooks/useScene'; -import { useUpdateStyles } from '@hooks/useStyleUpdate'; -import { useTableAddSheet } from '@hooks/useTableAddSheet'; - -import { ExportDialog } from './DialogExport'; -import { ImportDialog } from './DialogImport'; -import { Vitals } from './Vitals'; - -export function IOMenu(props: { export?: boolean }) { - const renderer = useRenderer(); - const scene = useScene(); - const updateStyle = useUpdateStyles(); - const importModels = useImportModels(); - const exportProject = useExportModels(); - const [, setProcessing] = useProcessing(); - const addSheet = useTableAddSheet(); - - const [importOpen, setImportOpen] = React.useState(false); - const [importedModels, setImportedModels] = React.useState([]); - const [importedStyles, setImportedStyles] = React.useState([]); - const [exportOpen, setExportOpen] = React.useState(false); - const [loadStyles, setLoadStyles] = React.useState(false); - - const onModelsSelected = async (event: React.ChangeEvent) => { - setProcessing(true, 'Reading files...'); - const { models, tables, styles } = await load(event, (status: string) => - setProcessing(true, status) - ); - setImportedModels(models); - tables.forEach((table) => addSheet(table)); - if (models.length > 0) { - setImportOpen(true); - if (styles.length > 0) setImportedStyles(styles); - } else if (styles.length > 0) updateStyle(styles[0]); - - setProcessing(false, 'Finished reading files'); - event.target.value = ''; - event.preventDefault(); - }; - - const handleModelsAdded = async (mode: CoordinateMode) => { - setProcessing(true, 'Building BVH...'); - setImportOpen(false); - - await importModels(importedModels, { - coordMode: mode, - }); - - setLoadStyles(true); - setProcessing(false, 'Finished loading models'); - setImportedModels([]); - }; - - React.useEffect(() => { - if (loadStyles && importedStyles.length > 0) { - updateStyle(importedStyles[0]); - setImportedStyles([]); - setLoadStyles(false); - } - }, [loadStyles, importedStyles]); - - const handleExport = (title: string | null) => { - setExportOpen(false); - if (title === null) return; - setProcessing(true, 'Exporting project...'); - exportProject(title); - setProcessing(false, 'Finished exporting project'); - }; - - const handleOpenExport = (e: React.MouseEvent) => { - setExportOpen(true); - e.preventDefault(); - }; - - return ( -
- - Import - - {props.export && } - - - -
- ); -} diff --git a/old/studio/src/components/Shared/MetadataItem.tsx b/old/studio/src/components/Shared/MetadataItem.tsx deleted file mode 100644 index 4d2c84b3..00000000 --- a/old/studio/src/components/Shared/MetadataItem.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -import { ChevronButton, ListButton, ListGroup, ListGroupChildren, ListItem } from '@elements/List'; -import { TitleChain } from '@elements/TitleChain'; - -import { Metadata } from '@data/types'; - -import { MetadataValue } from './MetadataValue'; - -interface MetadataItemProps { - onValuePick: (node: Metadata, value: any) => void; - category: string; - node: Metadata; - depth?: number; - initialOpen?: boolean; -} - -export function MetadataItem(props: MetadataItemProps) { - let { category, onValuePick, depth } = props; - const [open, setOpen] = React.useState(props.initialOpen || false); - - const { categories, node } = aggregateLabel(category, props.node); - const hasValues = node.values !== undefined; - const hasChildren = node.children && node.children.size > 0; - const children = [...(node.children ?? [])].sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); - - const handleOpen = (e: React.MouseEvent) => { - setOpen(!open); - e.stopPropagation(); - }; - - return ( - - {(hasChildren || hasValues) && ( - - - - - - - )} - {hasChildren && open && ( - - {children.map(([key, value]) => { - return ( - - ); - })} - - )} - {hasValues && open && ( - - )} - - ); -} - -function aggregateLabel(category: string, node: Metadata) { - const categories: string[] = [category]; - while (node.children && node.children.size === 1 && !node.values) { - const key = node.children.keys().next().value; - categories.push(key); - node = node.children.get(key)!; - } - return { categories, node }; -} diff --git a/old/studio/src/components/Shared/MetadataList.tsx b/old/studio/src/components/Shared/MetadataList.tsx deleted file mode 100644 index c8e132f3..00000000 --- a/old/studio/src/components/Shared/MetadataList.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -import { filterMetadata } from '@utils/metadata'; - -import { ColumnContainer, OverflowAbsoluteContainer, StretchContainer } from '@elements/Containers'; -import { Empty } from '@elements/Empty'; -import { Input } from '@elements/Input'; - -import { Metadata } from '@data/types'; - -import { useMetadata } from '@hooks/useMetadata'; - -import { MetadataItem } from './MetadataItem'; - -export const rootNodeLabel = 'Metadata'; - -interface MetadataListProps { - onValuePick: (root: Metadata, node: Metadata, value: any) => void; -} - -export function MetadataList(props: MetadataListProps) { - const metadata = useMetadata(); - const [fitlered, setFiltered] = React.useState(metadata); - const [search, setSearch] = React.useState(''); - const timerRef = React.useRef(); - - React.useEffect(() => { - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - }, []); - - React.useEffect(() => { - setFiltered(filterMetadata(metadata, '', search)); - }, [metadata, search]); - - const handleSearchChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => { - setSearch(value); - }, 500); - }; - - return ( -
- - - - - {!fitlered.children && !fitlered.values && No metadata} - {(fitlered.children || fitlered.values) && ( - - props.onValuePick(fitlered, node, value) - } - depth={0} - initialOpen={true} - /> - )} - - - -
- ); -} diff --git a/old/studio/src/components/Shared/MetadataValue.tsx b/old/studio/src/components/Shared/MetadataValue.tsx deleted file mode 100644 index d48e264a..00000000 --- a/old/studio/src/components/Shared/MetadataValue.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -import { getValueOrDefault } from '@utils/placeholders'; -import { isEmpty } from '@utils/predicates'; - -import { BracketsButton, ListButton, ListItem } from '@elements/List'; - -import { Metadata } from '@data/types'; - -interface MetadataValueProps { - node: Metadata; - onValuePick: (node: Metadata, value: any) => void; - depth?: number; -} - -export function MetadataValue(props: MetadataValueProps) { - const { node, onValuePick, depth } = props; - const [displayCount, setDisplayCount] = React.useState(10); - - const handleUseMetadata = (e: React.MouseEvent, value: any) => { - onValuePick(node, value); - e.stopPropagation(); - }; - - const unique = Array.from(new Set(node.values)).sort(); - - return ( - <> - {unique.slice(0, displayCount).map((value) => ( - - handleUseMetadata(e, value)} /> - handleUseMetadata(e, value)} - className={clsx(isEmpty(value) && 'text-neutral-500')} - > - {getValueOrDefault(value)} - - - ))} - {unique.length > displayCount && ( - - - - )} - - ); -} diff --git a/old/studio/src/components/Shared/StatusBar.tsx b/old/studio/src/components/Shared/StatusBar.tsx deleted file mode 100644 index 690c8663..00000000 --- a/old/studio/src/components/Shared/StatusBar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -import { useLog } from '@hooks/useLog'; -import { useProcessing } from '@hooks/useProcessing'; - -import chicken1 from '@assets/Chicken_Run.gif'; -import chicken3 from '@assets/Chicken_Strut.gif'; - -const messages = [ - 'Processing your data', - 'This may take a while depending on the size of your model', - 'Thank you for your patience', - 'Please do not close this window', -]; - -const chickens = [chicken1, chicken3]; - -export function StatusBar() { - const [processing] = useProcessing(); - const [message, setMessage] = React.useState(messages[0]); - const [chicken, setChicken] = React.useState(chickens[0]); - const [log] = useLog(); - const [status, setStatus] = React.useState(log[log.length - 1]); - let timerRef = React.useRef(); - - React.useEffect(() => { - setStatus(log[log.length - 1]); - if (timerRef.current) clearTimeout(timerRef.current); - if (!processing) - timerRef.current = setTimeout(() => { - setStatus('Ready'); - }, 10000); - }, [log, processing]); - - React.useEffect(() => { - if (processing) { - let i = 0; - const interval = setInterval(() => { - i++; - setMessage(messages[i % messages.length]); - setChicken(chickens[i % chickens.length]); - }, 5000); - - return () => clearInterval(interval); - } - }, [processing]); - - return ( -
-
- {processing &&
Working
} - {processing && } -
{status}
-
- {processing && ( -
-
{message}
-
- )} -
- ); -} diff --git a/old/studio/src/components/Shared/StyleCategoryEditor.tsx b/old/studio/src/components/Shared/StyleCategoryEditor.tsx deleted file mode 100644 index 0ba9d209..00000000 --- a/old/studio/src/components/Shared/StyleCategoryEditor.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ColorPicker } from '@elements/ColorPicker'; - -import { Categories } from '@data/types'; - -import { useApplyStyle } from '@hooks/useApplyStyle'; -import { useStyleKeychain } from '@hooks/useStyleKeychain'; -import { useUpdateStyles } from '@hooks/useStyleUpdate'; -import { useStyles } from '@hooks/useStyles'; - -interface CategoryStyleEditorProps { - category: string; - color: string; - categories: Categories; -} - -export function CategoryStyleEditor(props: CategoryStyleEditorProps) { - const { categories, category, color } = props; - const keychain = useStyleKeychain(); - const styles = useStyles(); - const updateStyles = useUpdateStyles(); - const [, applyStyle] = useApplyStyle(); - - const handleChange = (color: string) => { - if (!styles || !keychain) return; - categories[category] = color; - updateStyles({ ...styles }); - applyStyle(keychain); - }; - - return ( -
- -
- ); -} diff --git a/old/studio/src/components/Shared/StyleCategoryList.tsx b/old/studio/src/components/Shared/StyleCategoryList.tsx deleted file mode 100644 index 0f0dc533..00000000 --- a/old/studio/src/components/Shared/StyleCategoryList.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -import { getValueOrDefault } from '@utils/placeholders'; -import { isEmpty } from '@utils/predicates'; - -import { Categories } from '@data/types'; - -import { CategoryStyleEditor } from './StyleCategoryEditor'; - -export function StyleCategoryList(props: { categories: Categories }) { - const e = Object.entries(props.categories); - e.sort((a, b) => a[0].localeCompare(b[0])); - return ( -
- {e.map(([category, color], i) => { - return ( - - ); - })} -
- ); -} - -interface CategoryValueProps { - category: string; - color: string; - categories: Categories; -} - -function CategoryValue(props: CategoryValueProps) { - const [editing, setEditing] = React.useState(false); - - return ( - <> - - {editing && } - - ); -} - -interface CategoryButtonProps extends CategoryValueProps { - editing: boolean; - setEditing: (e: boolean) => void; -} - -function CategoryButton(props: CategoryButtonProps) { - const { category, color, editing, setEditing } = props; - - return ( - - ); -} diff --git a/old/studio/src/components/Shared/StyleDetail.tsx b/old/studio/src/components/Shared/StyleDetail.tsx deleted file mode 100644 index 0ecf8980..00000000 --- a/old/studio/src/components/Shared/StyleDetail.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Empty } from '@elements/Empty'; - -import { useStyle } from '@hooks/useStyle'; -import { useStyleKeychain } from '@hooks/useStyleKeychain'; - -import { StyleCategoryList } from './StyleCategoryList'; -import { StyleScalars } from './StyleScalars'; - -export function StyleDetail() { - const [histogram, style] = useStyle(); - const keychain = useStyleKeychain(); - - if (!style || !keychain) return No Style Info; - - return ( -
- {style.style?.scalars && histogram && ( - - )} - {style.style?.categories && } -
- ); -} diff --git a/old/studio/src/components/Shared/StyleHistogram.tsx b/old/studio/src/components/Shared/StyleHistogram.tsx deleted file mode 100644 index 7b0f0c46..00000000 --- a/old/studio/src/components/Shared/StyleHistogram.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { vec3 } from 'gl-matrix'; - -import { colorMapToValues } from '@utils/color'; - -import { Histogram, Scalars } from '@data/types'; - -interface StyleHistogramProps { - scalars: Scalars; - histogram: Histogram; -} - -export function StyleHistogram(props: StyleHistogramProps) { - const { scalars, histogram } = props; - const map = colorMapToValues(scalars.colormap); - const gradient = getGradientStyle(map, scalars.min, scalars.max, histogram.min, histogram.max); - - const max = Math.max(...histogram.histogram); - const bins = histogram.histogram.length; - - const TICKS_SIZE = 5; - const xTicks = new Array(TICKS_SIZE).fill(0).map((_, i) => { - const value = histogram.min + (i / (TICKS_SIZE - 1)) * (histogram.max - histogram.min); - return value.toFixed(2); - }); - - return ( -
-
- {histogram.histogram.map((value, i) => ( -
- ))} -
- -
-
- {xTicks.map((tick, i) => ( -
- {tick} -
- ))} -
-
- ); -} - -function getGradientStyle( - map: vec3[], - valueMin: number, - valueMax: number, - gradientMin: number, - gradientMax: number -) { - const range = valueMax - valueMin; - - if (range === 0) { - return `rgb(${map[0][0] * 255}, ${map[0][1] * 255}, ${map[0][2] * 255})`; - } - - const percGradientMin = (gradientMin - valueMin) / range; - const percGradientMax = (gradientMax - valueMin) / range; - const gradientRange = percGradientMax - percGradientMin; - - return map - .map((color, i) => { - const percent = percGradientMin + (i / (map.length - 1)) * gradientRange; - return `rgb(${color[0] * 255}, ${color[1] * 255}, ${color[2] * 255}) ${percent * 100}%`; - }) - .join(', '); -} diff --git a/old/studio/src/components/Shared/StyleItem.tsx b/old/studio/src/components/Shared/StyleItem.tsx deleted file mode 100644 index fc22fb17..00000000 --- a/old/studio/src/components/Shared/StyleItem.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; - -import { - ChevronButton, - ListButton, - ListGroup, - ListGroupChildren, - ListItem, - StyleButton, -} from '@elements/List'; -import { TitleChain } from '@elements/TitleChain'; - -import { Style } from '@data/types'; - -export type StyleMenuPickFunciton = (node: Style) => void; - -interface StyleItemProps { - onValuePick: StyleMenuPickFunciton; - category: string; - node: Style; - depth?: number; - initialOpen?: boolean; -} - -export function StyleItem(props: StyleItemProps) { - let { category, onValuePick, depth } = props; - const { categories, node } = aggregateLabel(category, props.node); - const [open, setOpen] = React.useState(props.initialOpen || false); - - const handleOpen = (e: React.MouseEvent) => { - setOpen(!open); - e.stopPropagation(); - }; - - const handleUseStyle = (e: React.MouseEvent) => { - onValuePick(node); - e.stopPropagation(); - }; - - const hasChildren = node.children && Object.keys(node.children).length > 0; - const hasValues = node.style !== undefined; - const children = Object.entries(node.children ?? {}).sort(([keyA], [keyB]) => - keyA.localeCompare(keyB) - ); - - return ( - - {hasChildren && ( - <> - - - - - - - - {hasValues && open && ( - - - Apply Style - - )} - {hasChildren && open && ( - - {children.map(([key, value]) => { - return ( - - ); - })} - - )} - - )} - {!hasChildren && hasValues && ( - - - - - - - )} - - ); -} - -function aggregateLabel(category: string, node: Style) { - const categories: string[] = [category]; - let keys: string[]; - while (node.children && (keys = Object.keys(node.children)).length === 1 && !node.style) { - const key = keys[0]; - categories.push(key); - node = node.children[key]; - } - return { categories, node }; -} diff --git a/old/studio/src/components/Shared/StyleList.tsx b/old/studio/src/components/Shared/StyleList.tsx deleted file mode 100644 index 72b645ff..00000000 --- a/old/studio/src/components/Shared/StyleList.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; - -import { filterStyles } from '@utils/style'; - -import { ColumnContainer, OverflowAbsoluteContainer, StretchContainer } from '@elements/Containers'; -import { Empty } from '@elements/Empty'; -import { Input } from '@elements/Input'; - -import { Style } from '@data/types'; - -import { useStyles } from '@hooks/useStyles'; - -import { StyleItem } from './StyleItem'; - -export const rootNodeLabel = 'Styles'; - -interface StyleListProps { - onValuePick: (root: Style, node: Style) => void; -} - -export function StyleList(props: StyleListProps) { - const style = useStyles(); - const [fitlered, setFiltered] = React.useState