# Building with AI

> How to give an AI assistant the Apricot docs, and the rules it must follow to write correct Apricot code on the first try. Paste this page into any assistant before asking it to build a scene.

These docs are written so an AI assistant can read them and build what you ask for. This page is the short version: the rules, the import map and the mistakes to avoid. It is the first page in [llms.txt](https://matthiasbrandolin.dev/apricot/llms.txt).

## Give your assistant the docs

| You use | Give it |
| --- | --- |
| An assistant that can read URLs (Claude, ChatGPT, Cursor, Claude Code…) | `https://matthiasbrandolin.dev/apricot/llms.txt`: an index of every page, each linked as Markdown. |
| An assistant with a large context, or offline | `https://matthiasbrandolin.dev/apricot/llms-full.txt`: every page in one file. |
| One topic | Any page's Markdown: add `.md` to its URL (`/apricot/guides/camera.md`), or use **Copy page** at the top of the page. |

A prompt that works well:

```text title="prompt.txt"
Read https://matthiasbrandolin.dev/apricot/llms.txt and the pages it links
that matter for this task. Follow the rules in "Building with AI" exactly.

Build me: a product screen where a gold coin spins slowly, speeds up when I
swipe, and shows its name in a label above it. Expo SDK 57, TypeScript.
```

## Rules for writing Apricot code

These are the facts that most often decide whether generated code works. Follow all of them.

1. **Import from the right entry point.**
   - Everything is imported from `@apricot/react`, **except** `Model`, `useModel`, `useTexture`, `useTextureState` and `defineModelKit`. Those come from `@apricot/react/model`.
   - Ready-made objects (`MoneyStack`, `Coin`, `Apple`, `Peach`, `Car`, `Card`, `Trees`, `Ground`, `ContactShadow`, `Backdrop`, `StudioFloor`, `CARD_SIZE`…) come from `@apricot/assets`.
   - Utilities without React (`seededRandom`, `disposeObject`, `Path`, `Trip`) come from `@apricot/core`.
   - App code does not import `three`. Use shapes, models, materials and refs instead. The exception is building custom geometry (see [Custom geometry and behaviors](https://matthiasbrandolin.dev/apricot/guides/custom)).
2. **`<Scene>` needs a size.** Give it `style={{ flex: 1 }}` or a height, like any view. Everything 3D goes inside a `<Scene>`, and hooks such as `useFrame` and `useScene` only work in components rendered inside one.
3. **Most `<Scene>` props are read once, at mount.** These are `look`, `camera`, `fog`, `environment`, `transparent`, `background`, `shadows`, `maxPixelRatio` and `linger`; changing them later does nothing. Instead:
   - change reflections with `<Environment>`,
   - change the time of day with `<Daylight>`,
   - move the camera with `<CameraRig>` or `<FitCamera>`,
   - remount the scene with a new `key` for anything else.

   `frameloop` is the one Scene prop that can change after mount.
4. **Animate without React renders.** Animatable props accept a plain value or a Reanimated shared value (`useSharedValue`, `useDerivedValue`). Drive per-frame logic with `useFrame`. Never call `setState` every frame.
5. **The scene sleeps when nothing changes** (`frameloop="demand"`).
   - In `useFrame`, return `true` while something is still animating, and `false` or nothing when it is done.
   - After changing an object directly (through a ref) outside `useFrame`, call `useScene().invalidate()`.
6. **Units and angles:**
   - Distances are scene units, meant as metres. Y is up. The camera looks down −z by default, so a model's front faces +z.
   - Object `rotation` is Euler angles in **radians** `[x, y, z]`. `<Spin speed>` is radians per second.
   - Camera views (`azimuth`, `elevation`), orbit limits and joint `range` are in **degrees**. `fov` is degrees.
   - Colours are CSS strings (`"#ff9f59"`) or anything three.js accepts.
7. **Many copies of the same thing are one `<Instances>`**, not a `.map()` of shapes. Each separate mesh costs about 80 µs per frame on a mid-range Android phone.
8. **Models are `.glb` files bundled with `require`:** `<Model src={require("./car.glb")} />`.
   - Optimise them first with `npx apricot-models ./models -o ./assets/models`. Draco, meshopt and KTX2 cannot be decoded on the device.
   - `ground` stands a model on y = 0.
   - Use a model kit (`defineModelKit`) to name parts, then refer to those names in `materials` and `joints`.
9. **Gestures wrap the scene.** Put `<Scene>` inside a `GestureDetector` (react-native-gesture-handler) and write shared values in the gesture callbacks. The app root needs `GestureHandlerRootView`.
10. **Overlays:** `<Scene transparent pointerEvents="none" style={StyleSheet.absoluteFill}>` draws 3D over a screen and lets touches through to it.
11. **Order matters for cameras that follow.** Put `<FollowCamera>` (and `<CameraRig follow>`) after the object they follow, so they read this frame's position.
12. **Do not invent props.** Every prop is listed in the [API reference](https://matthiasbrandolin.dev/apricot/reference/scene). If something is not there, build it with `useFrame`, refs or `defineBehavior`.
13. **It needs a development build.** Expo Go cannot run Apricot.

## Import map

```ts title="imports.ts"
// Scenes, shapes, lights, cameras, motion, hooks, types
import {
  Scene, Group, Primitive, Mesh, Box, Sphere, Cylinder, Cone, Plane, Torus, Capsule,
  AmbientLight, HemisphereLight, DirectionalLight, PointLight, SpotLight,
  Environment, Daylight, Blur, Label, Instances,
  FitCamera, CameraRig, FollowCamera,
  Spin, Vehicle, Pile, FollowPath,
  useFrame, useScene, useAnimatable, usePath, useTrip, useTripStatus, useSceneTouches,
  useMaterial, defineBehavior, definePreset, defineEnvironment, createMaterial,
  type SceneObject, type Vec3, type CameraRigHandle, type InstancesHandle,
} from "@apricot/react";

// Models and textures (separate entry point: it carries the glTF loader)
import { Model, defineModelKit, useModel, useTexture, type PartPressEvent } from "@apricot/react/model";

// Ready-made procedural objects
import { MoneyStack, Coin, Apple, Peach, Car, Card, CARD_SIZE, Trees, Ground, ContactShadow, Backdrop, StudioFloor } from "@apricot/assets";

// Engine utilities (no React)
import { seededRandom, disposeObject } from "@apricot/core";
```

## Components at a glance

| Need | Use | Page |
| --- | --- | --- |
| A 3D view | `<Scene>` | [Scene](https://matthiasbrandolin.dev/apricot/reference/scene) |
| Basic shapes | `<Box>`, `<Sphere>`, `<Cylinder>`, `<Cone>`, `<Plane>`, `<Torus>`, `<Capsule>`, `<Mesh geometry>` | [Objects](https://matthiasbrandolin.dev/apricot/reference/objects) |
| Group and move things together | `<Group position rotation scale>` | [Objects](https://matthiasbrandolin.dev/apricot/reference/objects) |
| What things are made of | `material="chrome"` or `{ preset, color, roughness, metalness }` | [Materials](https://matthiasbrandolin.dev/apricot/reference/materials) |
| Lighting | `look`, `environment`, `<Environment>`, `<Daylight>`, light components, `shadows` | [Lights](https://matthiasbrandolin.dev/apricot/reference/lights) |
| Frame an object at any screen size | `<FitCamera size>` | [Cameras](https://matthiasbrandolin.dev/apricot/reference/cameras) |
| Named views, flying between them, drag to orbit | `<CameraRig view orbit>` | [Cameras](https://matthiasbrandolin.dev/apricot/reference/cameras) |
| Chase camera | `<FollowCamera target>` | [Cameras](https://matthiasbrandolin.dev/apricot/reference/cameras) |
| A `.glb` model: paint parts, open doors, play clips | `<Model kit materials joints animations>` | [Model](https://matthiasbrandolin.dev/apricot/reference/model) |
| Spin something | `<Spin speed axis>` | [Motion](https://matthiasbrandolin.dev/apricot/reference/motion) |
| Anything per frame | `useFrame(({ delta }) => …)` | [Hooks](https://matthiasbrandolin.dev/apricot/reference/hooks) |
| A reusable behavior | `defineBehavior({ name, defaults, frame })` | [Motion](https://matthiasbrandolin.dev/apricot/reference/motion) |
| Hundreds of copies, tappable | `<Instances items onPress>` | [Instances](https://matthiasbrandolin.dev/apricot/reference/instances) |
| Objects that fall and pile up | `<Pile count>` | [Motion](https://matthiasbrandolin.dev/apricot/reference/motion) |
| A drivable toy car | `<Vehicle throttle steer>` | [Motion](https://matthiasbrandolin.dev/apricot/reference/motion) |
| A vehicle on a route with stops | `usePath`, `useTrip`, `<FollowPath>` | [Motion](https://matthiasbrandolin.dev/apricot/reference/motion) |
| A React Native view pinned to a 3D point | `<Label attach position>` | [Labels and blur](https://matthiasbrandolin.dev/apricot/reference/labels-and-blur) |
| Out-of-focus background | `<Blur radius>` | [Labels and blur](https://matthiasbrandolin.dev/apricot/reference/labels-and-blur) |

## Common mistakes

| Wrong | Right | Why |
| --- | --- | --- |
| `import { Model } from "@apricot/react"` | `import { Model } from "@apricot/react/model"` | The model loader is a separate entry point. |
| `rotation={[0, 90, 0]}` | `rotation={[0, Math.PI / 2, 0]}` | Object rotations are radians. |
| `<CameraRig view={{ azimuth: Math.PI / 4 }} />` | `<CameraRig view={{ azimuth: 45 }} />` | Camera angles are degrees. |
| `setState` in `useFrame` or a gesture to move things | A shared value passed to the prop, or a ref changed in `useFrame` | React renders per frame are slow, and the scene does not need them. |
| `useFrame(() => { ref.current.rotation.y += 0.01 })` | `useFrame(({ delta }) => { ref.current.rotation.y += delta; return true; })` | Use `delta` (seconds) for frame-rate independence, and return `true` to keep rendering. |
| `{seats.map(s => <Box key={s.id} … />)}` for hundreds of seats | `<Instances items={seats}><Box … /></Instances>` | One draw call instead of hundreds. |
| `<Scene look={dark ? "studio" : "soft"}>` to switch looks | `<Scene key={dark ? "dark" : "light"} look={…}>` or `<Environment name={…} />` | Scene options are read once at mount. |
| `<Scene>` with no style | `<Scene style={{ flex: 1 }}>` | A view with no size draws nothing. |
| `<FollowCamera target={car} />` before `<Vehicle ref={car}>` | Put it after the vehicle | It must read the car's position after the car moved this frame. |
| `src="./car.glb"` | `src={require("./car.glb")}` | Bundled models are loaded from the asset module. A string is fetched as a URL. |
| Shiny metal under `look="soft"` with no environment | `look="none" environment="studio"` | Metals need something to reflect. |

## A complete example to learn from

A swipeable gold coin with a label, using rules 1, 2, 4, 6 and 9:

```tsx title="CoinScreen.tsx"
import { Coin } from "@apricot/assets";
import { Group, Label, Scene, Spin } from "@apricot/react";
import { Text } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { cancelAnimation, useDerivedValue, useSharedValue, withDecay } from "react-native-reanimated";

export function CoinScreen() {
  const angle = useSharedValue(0);
  const rotation = useDerivedValue<readonly [number, number, number]>(() => [0, angle.value, 0]);
  const swipe = Gesture.Pan()
    .onBegin(() => cancelAnimation(angle))
    .onChange((e) => {
      angle.value += e.changeX * 0.012;
    })
    .onEnd((e) => {
      angle.value = withDecay({ velocity: e.velocityX * 0.012, deceleration: 0.997 });
    });

  return (
    <GestureDetector gesture={swipe}>
      <Scene style={{ flex: 1 }} look="none" environment="studio" background="#0e0f12" camera={{ position: [0, 0, 4] }}>
        <Spin speed={0.5}>
          <Group rotation={rotation}>
            {/* A coin lies flat; stand it up to face the camera. */}
            <Coin radius={1} rotation={[Math.PI / 2, 0, 0]} />
          </Group>
        </Spin>
        <Label position={[0, 1.3, 0]}>
          <Text style={{ color: "#ffffff", fontWeight: "600" }}>Gold coin</Text>
        </Label>
      </Scene>
    </GestureDetector>
  );
}
```
