Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Examples
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
schedule:
- cron: "23 10 * * 1"
permissions:
contents: read
jobs:
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run check
- run: npm run assets:resolve -- --mode remote
- run: npm run assets:pull
- run: npm test
- run: npm run build
- uses: actions/upload-artifact@v6
with:
name: threejs-examples
path: dist/
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
dist/
asset_cache/
asset-resolution.json
.env
.env.*
!.env.example
/asset_cache/
/asset-resolution.json
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
26
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Land of Assets

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
139 changes: 137 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,137 @@
# loa-examples
Official Land of Assets examples: pinned production assets in Three.js, with JSON resolution and Vite.
# Land of Assets · Three.js examples

Two small applications load the same pinned, public model from **[landofassets.com](https://landofassets.com)** using the published LoA packages:

| Example | Resolution | Asset delivery |
| -------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
| [`examples/json`](examples/json) | `loa resolve` → `asset-resolution.json` → `loadLoaResolver` | Production cloud, or pulled files during development |
| [`examples/vite`](examples/vite) | `loa()` → `virtual:loa` → `createLoaResolver` | Production cloud, or verified local files emitted into the build |

Both register `assets.urlModifier` on a `THREE.LoadingManager` and load `loa:robot` with `GLTFLoader`. [`shared/scene.js`](shared/scene.js) contains the common scene, animation, orbit controls, loading status, resolved URL, and actual WebGL renderer identity.

## Quick start

Use Node 26 (see `.nvmrc`). No LoA account, API token, global CLI installation, or Three.js checkout is needed to consume the public asset.

```sh
npm ci
npm run assets:resolve -- --mode remote
npm run dev:json
# Open http://127.0.0.1:4173
```

In another terminal, start the Vite example:

```sh
npm run dev:vite
# Open http://127.0.0.1:5173
```

Drag to orbit, scroll to zoom, or pause the robot's idle animation. Both pages should report a loaded model and show the same production URL. Errors appear in the page and browser console. A hardware rendering check requires an identified real GPU; the page rejects SwiftShader, llvmpipe, and lavapipe rather than treating them as a successful hardware check.

## What is committed

[`assets.json`](assets.json) selects `BenHouston3D/three/robotexpressive@1.0.0` as `robot`. [`assets-lock.json`](assets-lock.json) records its production host, exact version, immutable content URL, and SHA-256 integrity. Commit those two files when changing assets. Dependencies are pinned in `package-lock.json`.

`asset-resolution.json` (singular), `asset_cache/`, `node_modules/`, and `dist/` are generated and ignored. The generated map is intentionally separate from the manifest and lockfile.

All `npm run loa` and asset scripts force `LOA_HOST=https://api.landofassets.com`, overriding any preview or personal host in your global CLI configuration. The runtime uses the committed production URLs; it never receives a credential.

## Plain JavaScript and a JSON map

[`examples/json/main.js`](examples/json/main.js) loads the generated map **before** constructing loaders. The HTML import map points at installed browser ES modules; the development server exposes those modules under `/vendor/`. This example uses no Vite or other bundler.

```js
import { loadLoaResolver } from "@landofassets/runtime";

const assets = await loadLoaResolver("./asset-resolution.json");
const manager = new THREE.LoadingManager();
manager.setURLModifier(assets.urlModifier);
new GLTFLoader(manager).load("loa:robot", (gltf) => scene.add(gltf.scene));
```

For local JSON development:

```sh
npm run assets:pull
npm run assets:resolve -- --mode local
npm run dev:json
```

The server exposes the pulled `asset_cache/` files. The default resolve mode is `auto`, which prefers pulled files; pass `--mode remote` explicitly to test cloud loading after pulling.

Build an unbundled static distribution with a remote map:

```sh
npm run build:json
node scripts/json.mjs serve dist/json
```

Deploy the contents of `dist/json/` to an HTTP static host. The build copies Three.js and the runtime unchanged, together with the shared scene and generated map. Opening `index.html` with `file://` does not work because module and fetch requests need HTTP.

## Vite: cloud or self-contained

[`examples/vite/vite.config.js`](examples/vite/vite.config.js) points the LoA plugin at the repository's lockfile. Cloud mode does not require a pull:

```sh
npm run dev:vite
npm run build:vite
# dist/vite-remote/
```

For local development or a build that includes the model:

```sh
npm run assets:pull
npm run dev:vite:local
npm run build:vite:local
# dist/vite-local/ contains the GLB; no production request is needed at runtime.
```

These scripts use Vite's `offline` mode to select `loa({ mode: 'local' })`. Vite reserves the mode name `local` for `.env.local`, so do not pass `vite --mode local`.

The Vite entry point imports `virtual:loa` and constructs a synchronous resolver. Asset binding names and scene code stay the same in cloud and local builds. Both Vite outputs use relative build paths and can be deployed beneath a static-host subdirectory.

## Change the selected model

```sh
npm run loa -- add your-org/your-project/your-model@1.2.3 --as robot
npm run assets:resolve -- --mode remote
```

Use a self-contained GLB with embedded buffers and images. Additional codecs require their corresponding Three.js decoder setup. This scene normalizes the model to three units high and uses an `Idle` clip, falling back to the first animation. Update tests and attribution when replacing the demonstration asset.

To publish your own asset, authenticate first and publish to a project you own:

```sh
npm run loa -- auth login
npm run loa -- publish ./model.glb --to your-org/your-project/your-model --version 1.0.0
```

Publication validates self-contained resources on the server. The examples themselves only consume an existing public immutable asset.

## Verify integrations

```sh
npm run check
npm run assets:resolve -- --mode remote
npm run assets:pull
npm test
npm run build
```

The tests exercise the installed SDK against production metadata, anonymous content downloads and CORS, SHA-256 verification, Three.js GLB parsing and animation discovery, the unbundled HTTP routes, Vite's dev virtual module, local asset serving, and both remote and local build output. The local Vite build must contain a GLB with the locked hash and must not reference the remote content URL.

These checks do **not** prove GPU rendering quality. For that, open both pages in a browser with a real GPU, confirm the reported renderer, inspect the robot and controls, and check the console for errors. This repository is a functional example, not a rendering throughput benchmark.

[CI](.github/workflows/ci.yml) runs on PRs, `main`, manual dispatch, and weekly against production. It uploads all three static builds as an artifact. Network or production outages intentionally fail these integration checks.

## Attribution and license

Example code is [MIT licensed](LICENSE). The model retains its original CC0 1.0 license:

- **RobotExpressive**, by [Tomás Laulhé / Quaternius](https://www.patreon.com/quaternius), with modifications by [Don McCurdy](https://donmccurdy.com/) (facial morph targets, FBX2GLTF conversion, material cleanup).
- [Three.js model and attribution](https://github.com/mrdoob/three.js/tree/f2b6a71bdd088d9da387fcc62802323341099b92/examples/models/gltf/RobotExpressive).
- [Pinned production asset](https://landofassets.com/BenHouston3D/three/assets/robotexpressive@1.0.0).

No model binaries are committed here. The selected production asset matches the RobotExpressive GLB in the Three.js examples.
20 changes: 20 additions & 0 deletions assets-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lockfileVersion": 1,
"importers": {
".": {
"name": "loa-examples",
"assets": {
"robot": {
"kind": "loa",
"specifier": "BenHouston3D/three/robotexpressive@1.0.0",
"host": "https://api.landofassets.com",
"ref": "BenHouston3D/three/robotexpressive",
"version": "1.0.0",
"hash": "sha256-BH9eX7O7bTeL0d8WymE38qWWyZs6G1aQtAIMBar28xk=",
"remoteUrl": "https://api.landofassets.com/files/047f5e5fb3bb6d378bd1df16ca6137f2a596c99b3a1b5690b4020c05aaf6f319",
"extension": ".glb"
}
}
}
}
}
6 changes: 6 additions & 0 deletions assets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "loa-examples",
"assets": {
"robot": "BenHouston3D/three/robotexpressive@1.0.0"
}
}
44 changes: 44 additions & 0 deletions examples/json/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LoA × Three.js · JSON resolution</title>
<link rel="stylesheet" href="./shared/style.css" />
<script type="importmap">
{
"imports": {
"three": "./vendor/three/three.module.js",
"three/addons/": "./vendor/three/addons/",
"@landofassets/runtime": "./vendor/loa/index.js",
"example-scene": "./shared/scene.js"
}
}
</script>
</head>
<body>
<main>
<header>
<a href="https://landofassets.com">LAND OF ASSETS</a><span>Three.js examples / 01</span>
</header>
<section class="intro">
<p class="eyebrow">JSON RESOLUTION</p>
<h1>One robot.<br />Pinned in the cloud.</h1>
<p>A plain JavaScript scene, a small URL map, and a public asset from Land of Assets.</p>
</section>
<div id="viewport" aria-label="Interactive 3D robot"></div>
<aside>
<p id="status" role="status">Loading resolution map…</p>
<p id="gpu"></p>
<p id="asset-source"></p>
<button id="animate" disabled>Pause animation</button>
<p class="hint">Drag to orbit · Scroll to zoom</p>
</aside>
<footer>
RobotExpressive by Tomás Laulhé / Quaternius · CC0 ·
<a href="https://github.com/drivecore/loa-examples">Source & setup</a>
</footer>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions examples/json/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { loadLoaResolver } from "@landofassets/runtime";
import { showError, startScene } from "example-scene";

try {
// Resolve the complete map before constructing any loaders. No bundler needed.
const assets = await loadLoaResolver(new URL("./asset-resolution.json", import.meta.url).href);
await startScene(assets);
} catch (error) {
showError(error);
}
36 changes: 36 additions & 0 deletions examples/vite/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LoA × Three.js · Vite</title>
</head>
<body>
<main>
<header>
<a href="https://landofassets.com">LAND OF ASSETS</a><span>Three.js examples / 02</span>
</header>
<section class="intro">
<p class="eyebrow">VITE INTEGRATION</p>
<h1>Same robot.<br />Ready to bundle.</h1>
<p>
The LoA Vite plugin turns a committed lockfile into URLs. Switch between cloud assets and
a local build.
</p>
</section>
<div id="viewport" aria-label="Interactive 3D robot"></div>
<aside>
<p id="status" role="status">Loading virtual:loa…</p>
<p id="gpu"></p>
<p id="asset-source"></p>
<button id="animate" disabled>Pause animation</button>
<p class="hint">Drag to orbit · Scroll to zoom</p>
</aside>
<footer>
RobotExpressive by Tomás Laulhé / Quaternius · CC0 ·
<a href="https://github.com/drivecore/loa-examples">Source & setup</a>
</footer>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/vite/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createLoaResolver } from "@landofassets/runtime";
import resolution from "virtual:loa";
import { showError, startScene } from "../../shared/scene.js";
import "../../shared/style.css";

try {
const assets = createLoaResolver({ resolution });
await startScene(assets);
} catch (error) {
showError(error);
}
18 changes: 18 additions & 0 deletions examples/vite/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import { loa } from "@landofassets/runtime/vite";

const repository = fileURLToPath(new URL("../../", import.meta.url));

export default defineConfig(({ mode }) => ({
root: fileURLToPath(new URL("./", import.meta.url)),
base: "./",
plugins: [loa({ root: repository, mode: mode === "offline" ? "local" : "remote" })],
server: { port: 5173, strictPort: true, fs: { allow: [repository] } },
build: {
outDir: fileURLToPath(
new URL(`../../dist/vite-${mode === "offline" ? "local" : "remote"}/`, import.meta.url),
),
emptyOutDir: true,
},
}));
Loading
Loading