import { fileStorage, Placeable, Placement, useLayout } from "@apricot/react";
import type {
Collide, Layout, LayoutItem, LayoutOptions, LayoutStorage, MarkerOptions, Place, PlaceCheck, PlaceOn,
PlacementArea, PlacementBounds, PlacementHandle, PlacementProps, PlacementSnap, PlacementZone, PlaceableProps, SnapPoint,
} from "@apricot/react";Placement#
Things to pick up, move and turn by hand. Tap a <Placeable> to pick it up, drag it, twist two fingers to turn it, tap empty space to put it down. Drags elsewhere still reach <CameraRig orbit>. A drag that starts on the marker moves the picked-up thing. Put it inside <Scene>.
| Prop | Type | Default | Description |
|---|---|---|---|
bounds |
[minX, minZ, maxX, maxZ] |
anywhere | The floor area things stay inside, in metres: a room's walls |
wallHeight |
metres | 2.4 |
Height of the walls round bounds, for things hung on them |
areas |
PlacementArea[] |
anywhere (or bounds) |
Ground and water of any shape, each at its level |
zones |
PlacementZone[] |
none | Places nothing may stand |
snaps |
SnapPoint[] |
none | Spots that pull things into place and turn them |
snap |
PlacementSnap |
free | { grid, turn, wall } steps while moving |
collide |
Collide |
"block" |
What a moved thing does when it meets another |
canPlace |
(id, place: Place, check: PlaceCheck) => boolean |
Your own rule, called while a thing moves or turns | |
selected |
string | null |
uncontrolled | The picked-up thing's id, to control it yourself |
onSelect |
(id: string | null) => void |
Picked up (its id) or put down (null) |
|
onChange |
(id: string, place: Place) => void |
A thing was moved or turned and let go: its new place. Things standing on it report theirs too | |
onFitChange |
(id: string, fits: boolean) => void |
Whether the picked-up thing fits where it is | |
lift |
metres | 0.03 |
How high a picked-up thing floats while moved |
marker |
colour, MarkerOptions or false |
"#4f8cff" |
The patch under (or behind) the picked-up thing |
ref |
Ref<PlacementHandle> |
||
children |
ReactNode |
<Placeable>s, possibly inside your own components |
Places come from your state (position, turn on each <Placeable>) and go back through onChange. Store them (useLayout does).
Collide#
| Value | A moved thing that meets another |
|---|---|
"block" |
Stops against it and slides along it. canPlace false: it stays where it last fitted |
"warn" |
Passes through it with the marker in its blocked colour; let go there, it goes back to where it last fitted. Also passes over the wrong kind of ground, water and zones, marked |
"off" |
Ignores it |
PlacementSnap#
| Field | Default | Description |
|---|---|---|
grid |
0 (free) |
Grid step for positions, metres |
turn |
0 (free) |
Step for turns, degrees |
wall |
0 (off) |
Moves things flush against a wall within this distance, metres |
Place and PlaceCheck#
Place is { position: Vec3, turn: number } (turn in degrees).
PlaceCheck, given to canPlace:
| Field | Type | Description |
|---|---|---|
on |
PlaceOn |
Where it goes |
standingOn |
string | null |
The id of what it would stand on (a desk), or null (the floor, a wall) |
overlapping |
string[] |
Solid things it would overlap (always empty with "block" or "off") |
area |
string | null |
The id of the area it would stand or float in |
zones |
string[] |
The ids of the no-go zones it would touch |
MarkerOptions#
| Field | Default | Description |
|---|---|---|
color |
"#4f8cff" |
Where it fits |
blocked |
"#ff5a4f" |
Where it doesn't (overlap, zone, wrong ground, or canPlace false) |
opacity |
0.8 |
0 to 1 |
margin |
0.12 |
How far it reaches past the thing on each side, metres |
radius |
0.06 |
Corner rounding, metres. Large values make a pill or a disc |
outline |
0 (filled) |
Draws only a ring this thick, metres |
Areas, zones and snap points#
PlacementArea:
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
What canPlace is told |
|
outline |
Outline |
required | [x, z] corners in order, metres |
level |
metres | 0 |
Height of its top, where things stand |
water |
Waves | true |
Water: only on="water" things go in it, bobbing on these waves |
Things stay wholly inside one area, sliding along its edge. Later areas lie on earlier ones (the sand on the sea, a deck on the sand).
PlacementZone: { id?, outline }. A thing touching one doesn't fit.
SnapPoint:
| Field | Type | Default | Description |
|---|---|---|---|
at |
[x, z] |
required | Where, metres |
turn |
degrees | as it was | The turn it gives what snaps to it |
radius |
metres | 0.3 |
How close it pulls from |
accepts |
(id: string) => boolean |
all | Which things it takes |
PlacementHandle#
| Method | Description |
|---|---|
turnBy(id, degrees) |
Turns a thing (snapped to snap.turn), carrying what stands on it. Returns false if it isn't there or hangs on a wall; with "block", also if it has no room or canPlace says no. Otherwise it turns even where it doesn't fit (marked) |
standingOn(id) |
The ids of the things standing on it |
Placeable#
Something to place by hand. Must be inside a <Placement> (it throws otherwise).
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Unique within its <Placement>: what onSelect and onChange report |
position |
Vec3 |
[0, 0, 0] |
Metres. On the floor, y is the level it moves along; on a surface, the surface's top; on a wall, [along, height, wall] |
turn |
degrees | 0 |
Round the vertical axis. On a wall, set by the wall: 0 back (min z), 90 left (min x), 180 front (max z), 270 right (max x) |
on |
PlaceOn |
"floor" |
"floor", "surface" (on things marked surface, or the floor), "wall" (hung on the walls round bounds, facing in), "water" (floats in water areas) |
surface |
boolean | number |
false |
Things with on="surface" can stand on it and move with it: true for its top, or its top's height in its own space |
solid |
boolean |
true |
Others can't overlap it, nor it them. false for a rug |
locked |
boolean |
false |
Can't be picked up, but still blocks others |
children |
ReactNode |
What it is: shapes, <Merged>, <Model>, a <Lamp> |
Build children standing on their origin and facing +z; hung things with their back at z = 0. The footprint is the children's bounding box (a <Lamp>'s glow is left out). A <Model> still loading has no size: it fits anywhere until it arrives, then it is checked.
While a thing moves (dragged, easing back, or bobbing on water) it casts no shadow, so the shadow map is not redrawn every frame.
useLayout#
const layout = useLayout<T extends LayoutItem>(initial: T[], options?: LayoutOptions): Layout<T>;LayoutItem is { id: string; position: Vec3; turn: number }; add your own fields (kind, a colour).
LayoutOptions#
| Option | Type | Default | Description |
|---|---|---|---|
save |
string |
not kept | Keep it between runs under this name, and start from what was kept |
version |
number |
1 |
The version of your items' shape. A layout kept by another version is ignored |
history |
number |
100 |
How many steps undo goes back |
storage |
LayoutStorage |
fileStorage() |
Where it is kept |
Layout<T>#
| Member | Description |
|---|---|
items |
The layout now |
place(id, place) |
Moves an item. Pass it to <Placement onChange>. Calls in the same moment (one gesture) are one step |
add(item) |
Adds an item |
update(id, change) |
Changes some of an item's fields |
remove(ids) |
Removes one id or several, as one step |
set(items) |
Replaces every item (a reset), as one step |
undo(), redo() |
|
canUndo, canRedo |
|
newId(prefix) |
An unused id: prefix-1, prefix-2… |
loaded |
With save: true once the kept layout was read. True at once without save |
A kept layout is written about 0.3 s after the last change, and at once when the component unmounts.
LayoutStorage and fileStorage#
LayoutStorage is { read(name): Promise<string | null>; write(name, text): Promise<void> | void }.
fileStorage(), the default:
- on iOS and Android, a
<name>.jsonfile in anapricotfolder in the app's documents (expo-file-system). Withoutexpo-file-systeminstalled, nothing is kept and a warning says so once; - on the web,
localStorageunderapricot:<name>.
Your own, for example with AsyncStorage or a server:
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { LayoutStorage } from "@apricot/react";
export const asyncStorage: LayoutStorage = {
read: (name) => AsyncStorage.getItem(`layout:${name}`),
write: (name, text) => AsyncStorage.setItem(`layout:${name}`, text),
};Core has the pieces underneath: LayoutHistory, writeLayout(items, version), readLayout(text, version) and freeId(prefix, items).