# Interactive 3D scene for a web page: build brief

Assembled by AgentGid's starter kit builder from recipe `3d-scene` v0.3 (https://agentgid.com/build/kits/3d-scene/). Data snapshot: 2026-10-12.
Constraints: commercial use; free parts only; crediting authors is fine.
Recipe status: test-built on 2026-10-12 (2 builds) with exactly these parts. The fixes from the last build are in this version, which has not been built again as a whole.
Licenses and prices change. Before you ship, open the linked AgentGid pages and confirm.

## What to build

A page with a 3D model under realistic lighting that the visitor can orbit, zoom and change, sized for phones as well as desktops, built into static files you can host anywhere.

## Definition of done

- [ ] `npm run dev` serves the page with no console errors, and `npm run build` writes static files that also work when they are served from a sub-folder.
- [ ] The model is visible, lit by the environment map, with a soft contact shadow under it.
- [ ] Orbit and zoom work with mouse and touch, panning is off, and the camera cannot go below the floor or inside the model.
- [ ] At least one interaction visibly changes the scene: a colour switch or a hotspot.
- [ ] At 390 x 844 and at 1440 x 900 the canvas fills its container without stretching and the whole model is inside the frame.
- [ ] A loading state shows until the model is ready, and a plain message shows when WebGL is unavailable.
- [ ] Model, texture and HDR files together are under 5 MB, and the page requests nothing from another host.
- [ ] Screenshots at both sizes show the model, not a blank canvas.
- [ ] `CREDITS.md` has a row for every third-party file, and the build contains a license text for every bundled package and for the decoder, with no empty section.

Left for the person, because no check can do it:

- Look at the model for brand names, logos or a recognisable product shape before a commercial launch.
- Deploy the build: it needs your account on the host.

## Before you start

These files are already in this folder. Keep them, and never let a scaffolder overwrite them:

- `AGENTS.md`: this brief.
- `CLAUDE.md`: one line that imports AGENTS.md for Claude Code.
- `.mcp.json`: the MCP servers listed under "Tools you have".
- `CREDITS.md`: the license record. Add to it as you add files.
- `kit.json`: this kit as data (parts, versions, checks). `install.txt`: the install commands below. `first-prompt.txt`: the message the person starts you with.

You may do these without asking:

- Install the packages in the Stack section.
- Download the files linked under Assets, from the links given.
- Read the documentation this brief links to.
- Use these without asking, because they do not ship: Node's built-in `http` module as a static server, `git`, and the test runner in the Stack. Its browser download (`npx playwright install chromium`) is allowed too.
- Download the two license texts named in the plan.

Stop and ask the person for:

- an account, a key or a payment;
- a runtime dependency, asset source or API that this brief does not list;
- a check that still fails after two real fixes (say what you tried).

## Stack

Use these parts and versions. Ask before swapping one.

| Part | Choice | Version | Kind |
|---|---|---|---|
| 3D library | Three.js | 0.186.1 | dependency |
| React layer | React Three Fiber | 9.8.1 | dependency |
| Helpers | Drei | 10.7.9 | dependency |
| UI framework | React | 19.3.0 | dependency |
| Dev server and bundler | Vite | 8.3.4 | dev dependency |
| Language | TypeScript | 7.0.2 | dev dependency |
| Model pipeline | gltfjsx | 6.5.3 | run with npx, not installed |
| Scripted browser tests | Playwright (JavaScript) | 1.64.0 | dev dependency |
| Also | React DOM (same version as React) | 19.3.0 | dependency |
| Also | Vite React plugin (React support for Vite) | 6.1.2 | dev dependency |
| Also | Types for three.js (three.js ships no types) | 0.186.0 | dev dependency |

Install exactly these versions (the same lines are in `install.txt`):

```sh
npm install --save-exact three@0.186.1 @react-three/fiber@9.8.1 @react-three/drei@10.7.9 react@19.3.0 react-dom@19.3.0
npm install -D --save-exact vite@8.3.4 typescript@7.0.2 @playwright/test@1.64.0 @vitejs/plugin-react@6.1.2 @types/three@0.186.0
# when the plan calls for it: npx gltfjsx@6.5.3 <model>.gltf --transform --types
```

## Rules for this project

- Write every run-time URL (model, .hdr, decoder) with `import.meta.env.BASE_URL`, never with a leading slash. With `base: './'` the same build then works at a domain root and in a sub-folder. (test build)
- The page must not fetch anything from another host: no CDN fonts, no Environment presets, no remote decoder. (test build)
- Everything in `public/` is copied into `dist/`. Keep only files that have a row in CREDITS.md there.

### React Three Fiber, from its AgentGid page

- Keep @react-three/fiber on the major that matches React: v8 for React 18, v9 for React 19.
- Animate inside useFrame by mutating refs and multiplying by delta; never call setState every frame or on pointer move.
- Reuse geometries and materials (useMemo or module scope); use instancing for repeated objects; keep draw calls in the hundreds.
- Hide heavy parts with visible={false} instead of mounting and unmounting them.
- For mostly static scenes use <Canvas frameloop="demand"> and call invalidate() after changing three.js objects directly.
- Load models with useLoader / Drei's useGLTF (cached by URL); turn GLTF files into components with gltfjsx.
- More: https://agentgid.com/tools/react-three/

### Vite, from its AgentGid page

- Use the standard scripts: `vite` for dev, `vite build` for production, and `vite preview` to check the build locally.
- More: https://agentgid.com/tools/vite/

### Docs for lookups

Not required reading. Each plan step names what to read first.

- llms.txt (index of the docs) for React Three Fiber: https://r3f.docs.pmnd.rs/llms.txt
- llms-full.txt (full docs in one file) for React Three Fiber: https://r3f.docs.pmnd.rs/llms-full.txt
- Drei llms.txt for React Three Fiber: https://drei.docs.pmnd.rs/llms.txt
- Drei, full docs in one file (about 200 KB): https://drei.docs.pmnd.rs/llms-full.txt
- three.js instructions for LLMs (5 KB): https://threejs.org/docs/llms.txt

## Assets and licenses

Add a row to CREDITS.md for every file you bring into the project, at the moment you add it.

### 3D model: Poly Haven, Arm Chair 01

- Use this: 1k glTF, 0.8 MB: https://dl.polyhaven.org/file/ph-assets/Models/gltf/1k/ArmChair_01/ArmChair_01_1k.gltf (the .gltf references 4 more files (bin + textures) listed under gltf.1k.gltf.include)
- Larger, not needed for this plan: 2k glTF, 2.8 MB.
- All files and sizes: https://api.polyhaven.com/files/ArmChair_01
- Size: 0.85 x 1.07 x 0.77 m (width, height, depth) in real-world metres, base at y = 0. 5,626 polygons.
- For the camera: bounding sphere radius R = 0.78 m, footprint radius r = 0.57 m, middle of the height at y = 0.53 m.
- Nodes: ArmChair_01. Materials: Armchair_01.
- Its surface is light on average (brightness 117 of 255 on the source thumbnail), so a colour set with `material.color` shows. One material covers the whole model, so that colour lands on every part of it.
- Author: Kirill Sannikov. License of this item: CC0.
- Source terms: Commercial use OK; Credit when you use its API; Can ship inside your build.
- Note: Do not scrape its website. Use its API, one request at a time, and download only the files you use.
- Poly Haven's API is free for any purpose and needs no key: download these files yourself, with a User-Agent that names your project.
- AgentGid's rules for Poly Haven: Send a unique Referer or User-Agent with API requests. Do not imply endorsement by Poly Haven. Do not scrape the website; ask about the paid Bulk Snapshot for bulk needs.
- License page: https://polyhaven.com/license (AgentGid read it on Oct 10, 2026). Details: https://agentgid.com/assets/poly-haven/

### Lighting environment: Poly Haven, Studio Small 09

- Use this: 1k .hdr, 1.6 MB: https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/studio_small_09_1k.hdr
- Larger, not needed for this plan: 2k .hdr, 6.3 MB.
- All files and sizes: https://api.polyhaven.com/files/studio_small_09
- Author: Sergej Majboroda. License of this item: CC0.
- Source terms: Commercial use OK; Credit when you use its API; Can ship inside your build.
- Note: Do not scrape its website. Use its API, one request at a time, and download only the files you use.
- The rules for Poly Haven above apply here too.

## Tools you have

These MCP servers are in the config file listed under "Before you start".

- pmndrs docs: Version-matched React Three Fiber and Drei docs for the agent
- Playwright MCP: Lets the agent open the app in a browser, click, and take screenshots
- AgentGid: Live data for choosing tools, assets and APIs while building

If a server's tools are missing from your session, either the person has not approved them yet (Claude Code holds the servers of a project `.mcp.json` as "Pending approval" until the person accepts them), or the config was added after the session started. Ask the person to approve the servers or restart the agent in this folder; until then, use the links in this brief.

## Ship

- Where to publish: Netlify. Build and deploy web projects with serverless functions, database, and edge computing. Free tier: 300 credits/month.
- Free plan in numbers (Netlify pricing page, Oct 12, 2026): 300 credits a month; a production deploy costs 15 credits and bandwidth 20 credits per GB. When the credits run out, the site is paused until the next billing cycle. A page of about 3 MB uses roughly 1 GB per 330 first visits.
- Deploying needs the person's Netlify account. Hand over `dist/`; the person runs `npx netlify-cli deploy --dir=dist --prod --no-build` and signs in when asked. Without `--no-build` the CLI builds the project again before it uploads.
- Details: https://agentgid.com/apis/netlify/

## Plan

Work one step at a time. Do not start a step until the previous one meets its "Done when" lines.

### 1. Scaffold

- This folder already holds the kit files, and `create-vite` refuses a folder that is not empty: it prints `Operation cancelled` and still exits with 0. Scaffold next to them and copy the result in: `npm create vite@latest .scaffold -- --template react-ts --no-interactive && cp -a .scaffold/. . && rm -r .scaffold`. Never pass `--overwrite`: it deletes AGENTS.md. Check that `package.json` exists afterwards.
- Set `name` in package.json and `<title>` in index.html: both say `scaffold`. Replace the template's README.md, which describes the template.
- Run the install lines of the Stack section. The template pins older versions; the install replaces them. Run `git init` and commit `package-lock.json`. The template's other dev dependencies (`@types/react`, `@types/react-dom`, `@types/node`, `oxlint`) stay as it pins them.
- Delete the template's demo: the markup in `src/App.tsx`, `src/App.css`, `src/assets/`, `public/favicon.svg` and `public/icons.svg` (a sheet of brand logos), and empty `src/index.css`. In index.html replace the favicon link with `<link rel="icon" href="data:,">`.
- Give the canvas's parent a height and remove the page margin: `html, body, #root { height: 100%; margin: 0 }`. A canvas fills its parent, and without a height it is 150 px tall.
- In the Vite config set `base: './'` and `build: { license: { fileName: 'licenses.md' }, chunkSizeWarningLimit: 1200 }`. The first makes the build work in a sub-folder, the second writes the license texts of the bundled packages into `dist/`, the third silences a warning that three.js alone triggers.
- Read first: https://r3f.docs.pmnd.rs/api/canvas.md (14 KB); https://threejs.org/docs/llms.txt (5 KB)
- Done when:
  - The page shows a lit test cube that fills the window and that the pointer can orbit, with no console errors.
  - `npm run build` passes and `dist/index.html` loads its script from `./assets/`.
  - The kit files listed under "Before you start" are still there.

### 2. Fetch assets

- Download the files marked "Use this" under Assets into `raw-assets/` with a script you keep in the repository. Keep the folder out of `public/` and add it to `.gitignore`.
- For the model, call `https://api.polyhaven.com/files/ArmChair_01`, take `gltf.1k.gltf`, and download the .gltf together with every file in its `include` map, saving each under the relative path the map gives. The textures do not sit next to the .gltf on the server, so resolving the paths yourself returns 404.
- The .hdr is in the same kind of answer: `https://api.polyhaven.com/files/<id>` lists it under `hdri.1k.hdr` with its URL, size and MD5.
- Send a User-Agent that names your project with every request to the Poly Haven API, and check each file's size against the API's answer.
- Fill in CREDITS.md: one row per downloaded file, with the author named under Assets.
- Open the downloaded textures and look for brand names or logos. If you find one, stop and tell the person: CC0 does not cover trademarks.
- Done when:
  - `raw-assets/` holds the model with its textures and the .hdr file, and each has a row in CREDITS.md.
  - Running the download script again changes nothing.

### 3. Prepare the model

- Run `npx gltfjsx@6.5.3 raw-assets/<model>.gltf --transform --types`. It writes `<model>-transformed.glb` and `<model>.tsx` into the current folder: move the .glb into `public/` and the component into `src/`.
- The generated component is written for React 18 and does not compile here. Make six edits: remove `import React`; remove the `three-stdlib` import; delete `GLTF & ` from `type GLTFResult`; remove the `animations: GLTFAction[]` line; type the props as `ThreeElements['group']` (a type import from `@react-three/fiber`) and cast the result, `useGLTF(url, draco) as unknown as GLTFResult`; replace both `'/<model>-transformed.glb'` strings with a URL built from `import.meta.env.BASE_URL`.
- The compressed .glb needs the Draco decoder, and Drei fetches it from www.gstatic.com unless you pass a path. Copy `draco_decoder.js`, `draco_decoder.wasm` and `draco_wasm_wrapper.js` from `node_modules/three/examples/jsm/libs/draco/gltf/` into `public/draco/`, and give that folder to both calls: `useGLTF(url, import.meta.env.BASE_URL + 'draco/')` and `useGLTF.preload(url, ...)`. The folder has no license file: download https://raw.githubusercontent.com/google/draco/main/LICENSE (Apache-2.0) into `public/draco/LICENSE.txt`.
- Copy the 1k .hdr into `public/`.
- Read first: https://drei.docs.pmnd.rs/loaders/gltf-use-gltf.md (4 KB)
- Done when:
  - `npx tsc -b` passes with the model component in place.
  - The compressed model and the .hdr in `public/` total under 5 MB.

### 4. Build the scene

- `<Canvas frameloop="demand">` with a camera, the model, `<Environment files={import.meta.env.BASE_URL + '<name>.hdr'} />` from your own copy, `<ContactShadows>`, and `<OrbitControls enablePan={false}>` with `minDistance`, `maxDistance` and a `maxPolarAngle` a little under `Math.PI / 2`.
- Frame the model from the two radii under Assets, so that it stays inside the frame from every angle: distance = max(R / sin(vfov / 2), r / sin(hfov / 2)), where hfov = 2 * atan(tan(vfov / 2) * aspect). Recompute when the window changes shape. Aim the camera at the middle of the model's height, set `minDistance` a little above R, and set the near plane to about a tenth of the closest distance.
- Keep `<ContactShadows>` at x = 0 and z = 0 and size it with `scale`. With `frames={1}` put it in a memoised component, or it is baked again on every render.
- Before mounting `<Canvas>`, test `document.createElement('canvas').getContext('webgl2')` and render a plain message when it is null. Wrap the 3D part in an error boundary as well. The Canvas `fallback` prop alone leaves a blank page.
- Load the 3D part with `React.lazy` behind the loading state, so the page appears before the large chunk has downloaded. To know when the model is ready, put a small component inside the same `<Suspense>` as the model and call `onReady()` from its effect: it mounts only when the model and the .hdr have loaded. If you wait for the first drawn frame instead, subscribe in a layout effect and call `invalidate()` there: drawing on demand, an ordinary effect can run after the only frame.
- Show a message when the model, the decoder or the .hdr does not arrive. A failed decoder download never reaches an error boundary, so give the loading state a time limit as well.
- Expose a small test hook, in dev builds and when the URL contains `?test`: `window.__scene = { ready, frames, drawCalls, animating, camera }`. Checks wait on it instead of sleeping: with software WebGL a single frame can take a second.
- Read first: https://drei.docs.pmnd.rs/staging/environment.md (5 KB); https://drei.docs.pmnd.rs/staging/contact-shadows.md (1 KB)
- Done when:
  - A screenshot shows the model with reflections from the environment, and dragging rotates the view.
  - From the start view and from the highest and lowest angles the controls allow, the model's projected bounds stay inside the frame at both window sizes.
  - With the view zoomed in and dragged down as far as it goes, the camera's y stays above 0.
  - With WebGL disabled (a test script that makes `HTMLCanvasElement.prototype.getContext` return null for 'webgl2'), the message is visible.
  - The loading text is gone on every one of six reloads.

### 5. Interaction

- Add one interaction a visitor can see, with plain DOM buttons outside the canvas.
- Switch the colour from the buttons: this model's surface is light, so `material.color` shows. Offer the original and three or four colours.
- This model has one material for everything, so `material.color` tints all of it, the dark parts too. To recolour only the light surface, patch the material in `onBeforeCompile` so that the colour is applied where the colour texture is light (for example `smoothstep(0.11, 0.2, luminance)`), and check the result on a screenshot.
- Keep the choice in React state and animate inside `useFrame` by changing refs, never with setState every frame. With `frameloop="demand"`, call `invalidate()` when the state changes and again from `useFrame` until the animation has settled. Take the animation's progress from the clock (`performance.now()`), not from an accumulated `delta`: after a pause the first `delta` is the length of the pause, and on a slow machine a clamped `delta` turns the fade into slow motion. Set the hook's `animating` flag while it runs.
- Done when: Clicking each control changes the pixels of the model in a screenshot by an amount a person would notice. Take the screenshot when the hook says the animation has finished.

### 6. Performance and sizes

- Log the draw calls of a whole frame: set `gl.info.autoReset = false`, reset the counter in `addEffect`, read `gl.info.render.calls` in `addAfterEffect`, and log a frame after the first, which includes one-off shadow and environment setup. Drawing on demand there is no second frame unless you ask for one, and `invalidate()` called inside those two callbacks is ignored: ask with `requestAnimationFrame(() => invalidate())`. Keep the number in the low hundreds.
- Check the layout at 390 x 844 and at 1440 x 900.
- Read first: https://r3f.docs.pmnd.rs/advanced/scaling-performance.md (16 KB)
- Done when:
  - Screenshots at both sizes show the whole model inside the frame.
  - The draw-call count is logged, and two seconds after the last input nothing is drawn any more.

### 7. Verify and ship

- Run `npm run build`, serve `dist/` from a sub-folder (not from the root) and repeat the screenshots and the no-WebGL check there.
- Read the network log of that run: the page requests nothing from another host.
- Open `dist/licenses.md` and look for a heading with nothing under it: a package that ships no license file leaves one. React Three Fiber 9.8.1 is such a package: download its MIT text from https://raw.githubusercontent.com/pmndrs/react-three-fiber/master/LICENSE and ship it next to the list (for example as `public/licenses/react-three-fiber.txt`).
- List `dist/`: every file is the page, a script or style, a license text, the decoder, or a file with a row in CREDITS.md.
- Hand over `dist/` as the Ship section says.
- Done when: Every line under Definition of done is true.

## How to check your work

- Start the dev server with `npm run dev -- --strictPort` and read the URL from its output. If the port is taken, pass another one with `--port`; do not stop a server you did not start.
- Check through the test hook that the scene has drawn frames, then look at a screenshot. A page screenshot alone can fool you: a CSS background behind an empty canvas is not one flat colour either.
- Read the console after every change: a missing texture or a failed HDR load shows up there first.
- Repeat at 390 x 844 and at 1440 x 900.
- On a machine without a GPU, start the test browser with software WebGL (`--use-angle=swiftshader --enable-unsafe-swiftshader --ignore-gpu-blocklist`). Rendering is slow there, which is one more reason to draw on demand and to wait on the hook, not on a timer.

## Known traps

- React Three Fiber follows React's major version: v9 needs React 19. Its npm entry accepts react and react-dom >=19 <19.4, and the two must be the same version. (npm registry, Oct 12, 2026)
- Do not write the old pattern with a script tag pointing at three.js r128 on a CDN. Import from 'three' and 'three/addons/'. (three.js instructions for LLMs)
- Do not ship `<Environment preset="studio" />`. Drei's docs say presets are not meant for production because they load from a CDN. Use your own copy of the .hdr. (Drei docs)
- A Draco-compressed model has the same problem in a quieter form: Drei downloads the decoder from www.gstatic.com on every visit unless `useGLTF` gets a local path. (Drei source and test build)
- gltfjsx 6.5.3 writes React 18 TypeScript and absolute model URLs. Used unchanged, the page is blank ("does not provide an export named 'GLTF'"), `tsc -b` reports five errors, and the model is missing as soon as the site sits in a sub-folder. (gltfjsx source and test build)
- The Canvas `fallback` prop does not show when WebGL is missing: the renderer error unmounts the whole React tree and the page stays blank. (test build)
- OrbitControls pans by default. Panning moves the orbit centre, so a visitor can drag it under the floor whatever the distance and angle limits are. (test build)
- A perspective camera's field of view is vertical. A view tuned for a wide window cuts off the sides of the model in a tall one. (test build)
- Animate by changing refs inside useFrame and scale by the frame delta. Calling setState every frame or on pointer move sends each update through React and slows the scene. (AgentGid: React Three Fiber)
- A Poly Haven .gltf is not one file. It points to a .bin and several textures that must sit at the relative paths its files API lists. (Poly Haven API)
- CC0 covers copyright, not trademarks. Poly Haven flags some models of real products, and the flag is not complete: look at the model before a commercial launch. (Poly Haven API, checked Oct 12, 2026)
- The simple framing formula, height / (2 * tan(fov / 2)), is for a flat picture seen head-on. A model seen from a three-quarter angle needs 30 to 45% more distance, and a margin of 10 to 20% hides the problem until a leg is cut off. (second test build)
- Vite's license list covers bundled packages only. Files you copy through `public/` (the decoder) are not in it, and a package without a license file gets an empty section while the build stays green. (Vite docs and second test build)
- The three.js instructions for LLMs show an import map that loads three.js from a CDN. That is for single-file demos: this project installs three.js from npm and must not fetch from another host. (three.js instructions for LLMs)

## Warnings you can ignore

- "THREE.Clock: This module has been deprecated. Please use THREE.Timer instead." on every page load. React Three Fiber 9.8.1 creates a THREE.Clock, which three.js deprecated in r183. It is harmless: do not swap versions to remove it, and count it as a warning, not an error.
- Vite's "Some chunks are larger than 500 kB" warning, if the limit is lower than the chunk: three.js with React Three Fiber and Drei is about 1.15 MB minified (about 315 kB gzipped).
- The template's `npm run lint` (oxlint) reports about a thousand warnings for the decoder files in `public/draco/`. Exclude that folder from linting.

## Limits

- Poly Haven's rule about showing 'Powered by Poly Haven' applies to products that call its API while running. This kit downloads files once, at build time, and ships your own copies.
- Product photos, logos and brand models are not covered by any library license here. Use your own.
- CC0 covers copyright only. A model that copies a real product can still raise trademark or design questions.
- Never read or print the contents of `.env` files, and never commit keys.

## When you need something that is not listed

Do not pick a library, asset source or API from memory. Ask the AgentGid MCP server first:

- `find_tools(goal)` for libraries, APIs and agents that fit a task.
- `find_assets(category, query, free_commercial_only=true)` for media sources and their license terms. It knows sources such as Kenney or Pixabay, not single packs, models or fonts.
- `get_api(api)` for prices, free tiers and limits of entries in AgentGid's API catalog. Media sources are not in it: use `find_assets`.
- `check_status(names)` when a service seems down.

Then tell the person what you want to add and why, cite the AgentGid URL, and wait for a yes before you add a runtime dependency.

## When this brief is wrong

Keep a list of what in this brief was wrong, missing or out of date, with the line and what worked instead (in PROGRESS.md). When you finish, send it to AgentGid's editors with `report_kit_issue(recipe="3d-scene", version="0.3", text=...)` on the AgentGid MCP server: only they read it. Leave out keys, personal data and the person's code.

## Sources

- https://agentgid.com/tools/three/
- https://agentgid.com/tools/react-three/
- https://agentgid.com/tools/react/
- https://agentgid.com/tools/vite/
- https://agentgid.com/tools/typescript/
- https://github.com/pmndrs/gltfjsx
- https://agentgid.com/tools/playwright-test/
- https://agentgid.com/assets/poly-haven/
- https://polyhaven.com/a/ArmChair_01
- https://polyhaven.com/a/studio_small_09
- https://r3f.docs.pmnd.rs/llms.txt
- https://agentgid.com/mcp/servers/io-github-microsoft-playwright-mcp/
- https://agentgid.com/connect/
- https://agentgid.com/apis/netlify/
- https://registry.npmjs.org/@react-three/fiber/latest
- https://threejs.org/docs/llms.txt
- https://drei.docs.pmnd.rs/staging/environment.md
- https://cdn.jsdelivr.net/npm/@react-three/drei@10.7.9/core/Gltf.js
- https://cdn.jsdelivr.net/npm/gltfjsx@6.5.3/src/utils/parser.js
- https://api.polyhaven.com/files/ArmChair_01
- https://api.polyhaven.com/assets?t=models
- https://vite.dev/config/build-options.md
- https://raw.githubusercontent.com/Poly-Haven/Public-API/master/ToS.md
