@zkmake/three-studio 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -9
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/dist/{mount-DLIbCER1.d.ts → mount-CeU8JUWr.d.ts} +2 -2
- package/dist/{mount-D3fYtnHO.js → mount-DrLKxP3Q.js} +3 -3
- package/dist/{mount-D3fYtnHO.js.map → mount-DrLKxP3Q.js.map} +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{state-BsQ-taE3.d.ts → state-KovV7yXI.d.ts} +8 -3
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/{views-suF0RWQc.js → views-BH_N7aiA.js} +21 -10
- package/dist/views-BH_N7aiA.js.map +1 -0
- package/package.json +3 -3
- package/dist/views-suF0RWQc.js.map +0 -1
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ const config: StudioConfig = {
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`orbitViews(centre, near, { play })` gives `play`, `close` (along the play bearing), `front`, `side`,
|
|
37
|
-
`back` and `top`.
|
|
37
|
+
`back` and `top`. `front` looks from +x; for an object facing another way, pass where its front
|
|
38
|
+
camera stands, `{ front: [0, 0.25, -1] }` for a vehicle driving down -z, and `side` and `back` turn
|
|
39
|
+
with it. Write your own as `{ position, target, fov?, near?, far?, zoom? }`.
|
|
38
40
|
|
|
39
41
|
## With React Three Fiber
|
|
40
42
|
|
|
@@ -100,14 +102,15 @@ post-processing, pass `render: () => composer.render()` so shots include it.
|
|
|
100
102
|
|
|
101
103
|
## The URL
|
|
102
104
|
|
|
103
|
-
| Parameter
|
|
104
|
-
|
|
|
105
|
-
| `area=<name>`
|
|
106
|
-
| `v=<name>`
|
|
107
|
-
| `view=<name>`
|
|
108
|
-
| `t=<seconds>`
|
|
109
|
-
| `wire=1`
|
|
110
|
-
| `clean=1`
|
|
105
|
+
| Parameter | Does |
|
|
106
|
+
| -------------- | ------------------------------------------------------------------- |
|
|
107
|
+
| `area=<name>` | The area. A bare `?<name>` works too, as single-object studios link |
|
|
108
|
+
| `v=<name>` | The variant; the area's first by default |
|
|
109
|
+
| `view=<name>` | The camera view; `play` by default |
|
|
110
|
+
| `t=<seconds>` | Freezes animation at that time, for steady shots (`useStudioTime`) |
|
|
111
|
+
| `wire=1` | Draws every material as wireframe, to read the topology |
|
|
112
|
+
| `clean=1` | Hides the overlay, for screenshots of the whole window |
|
|
113
|
+
| `shots=<port>` | Where `saveShot` posts, when `three-studio serve` isn't on 8787 |
|
|
111
114
|
|
|
112
115
|
Unknown names fall back to the defaults and are listed in `state.unknown`. `studioHref(state, change)`
|
|
113
116
|
builds links: a new area starts at its first variant, the view and `wire` / `t` carry over.
|
|
@@ -129,6 +132,9 @@ await saveShot("round-1/trees-lush-play.png");
|
|
|
129
132
|
// "/…/shots/round-1/trees-lush-play.png 38 calls · 112,400 tris"
|
|
130
133
|
```
|
|
131
134
|
|
|
135
|
+
Port 8787 taken (another receiver, another agent's pass)? `serve` says so: run it with
|
|
136
|
+
`--port 8797` and open the page with `&shots=8797`, which links on the page carry along.
|
|
137
|
+
|
|
132
138
|
`saveShot` draws a frame and reads it in the same task, so it needs no `preserveDrawingBuffer` and
|
|
133
139
|
works in a hidden browser pane where `requestAnimationFrame` never fires. The receiver only writes
|
|
134
140
|
`.png` files inside its folder, and only listens locally. `{ size: "css" }` saves at the canvas's CSS
|
package/dist/cli.js
CHANGED
|
@@ -95,6 +95,8 @@ const main = async (argv, options = {}) => {
|
|
|
95
95
|
const shots = await startShotServer(files[0], {
|
|
96
96
|
port,
|
|
97
97
|
log: console.log
|
|
98
|
+
}).catch((error) => {
|
|
99
|
+
throw error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`port ${port} is taken, by another receiver or app: pass --port <free port>, and open the page with &shots=<that port>`) : error;
|
|
98
100
|
});
|
|
99
101
|
console.log(`three-studio: writing .png files inside ${shots.root} from ${shots.url}`);
|
|
100
102
|
await (options.stop ?? new Promise((stop) => {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-studio serve | diff | montage`: receive the page's shots, and compare them. `diff` exits 1\n * past `--max`, so a stored baseline can gate CI.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { parseArgs } from \"node:util\";\n\nimport { SHOT_PORT } from \"../core/capture.ts\";\nimport { diffImages, montage } from \"../node/compare.ts\";\nimport { decodePng, encodePng } from \"../node/png.ts\";\nimport { startShotServer } from \"../node/serve.ts\";\n\nconst HELP = `Usage: three-studio <command> [options]\n\nCommands:\n serve <folder> Receive saveShot() PNGs from the page and write them inside\n <folder>. Local only. Runs until stopped.\n --port <n> Default ${SHOT_PORT}, the port saveShot() posts to\n\n diff <before.png> <after.png> Count the pixels that differ. Prints \"<n> px differ (<share>%)\".\n --fuzz <percent> How far a channel may move and still match (default 3)\n --max <percent> Exit 1 when more than this share differs (default: never)\n --out <mask.png> Write the differing pixels in white over a dimmed before\n --json Print the result as JSON\n\n montage <out.png> <before> <after> [<before> <after>…]\n One row per pair, before left and after right, rows stacked.\n --scale <n> Scale of the result (default 0.5)\n --gap <px> Space between images, before scaling (default 8)\n\n -h, --help Show this help\n -v, --version Show the version\n\nExits 2 on bad arguments or a file it can't read.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-studio\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\nconst readPng = (file: string) => decodePng(readFileSync(file));\n\nconst percent = (text: string | undefined, name: string, fallback?: number) => {\n if (text === undefined) {\n return fallback;\n }\n\n const value = Number(text);\n\n if (!(value >= 0 && value <= 100)) {\n throw new Error(`--${name} must be a percentage from 0 to 100`);\n }\n\n return value / 100;\n};\n\nexport type MainOptions = {\n /** Resolves when `serve` should stop. Default: SIGINT or SIGTERM. */\n stop?: Promise<unknown> | undefined;\n};\n\nexport const main = async (argv: string[], options: MainOptions = {}): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n port: { type: \"string\" },\n fuzz: { type: \"string\" },\n max: { type: \"string\" },\n out: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n scale: { type: \"string\" },\n gap: { type: \"string\" },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-studio: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n const [command, ...files] = positionals;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n try {\n if (command === \"serve\" && files.length === 1) {\n const port = values.port === undefined ? SHOT_PORT : Number(values.port);\n\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(\"--port must be a port number\");\n }\n\n const shots = await startShotServer(files[0] as string, { port, log: console.log });\n\n console.log(`three-studio: writing .png files inside ${shots.root} from ${shots.url}`);\n await (options.stop ??\n new Promise((stop) => {\n process.once(\"SIGINT\", stop);\n process.once(\"SIGTERM\", stop);\n }));\n await shots.close();\n\n return 0;\n }\n\n if (command === \"diff\" && files.length === 2) {\n const fuzz = percent(values.fuzz, \"fuzz\", 0.03);\n const max = percent(values.max, \"max\");\n const result = diffImages(readPng(files[0] as string), readPng(files[1] as string), { fuzz });\n const over = max !== undefined && result.share > max;\n\n if (values.out) {\n writeFileSync(values.out, encodePng(result.mask));\n }\n\n if (values.json) {\n console.log(\n JSON.stringify({ pixels: result.pixels, total: result.total, share: result.share, over }),\n );\n } else {\n console.log(`${result.pixels} px differ (${(result.share * 100).toFixed(4)}%)`);\n }\n\n return over ? 1 : 0;\n }\n\n if (command === \"montage\" && files.length >= 3 && files.length % 2 === 1) {\n const [out, ...shots] = files as [string, ...string[]];\n const scale = values.scale === undefined ? 0.5 : Number(values.scale);\n const gap = values.gap === undefined ? 8 : Number(values.gap);\n\n if (!(scale > 0) || !(gap >= 0)) {\n throw new Error(\"--scale must be above 0 and --gap 0 or more\");\n }\n\n const pairs: [ReturnType<typeof readPng>, ReturnType<typeof readPng>][] = [];\n\n for (let i = 0; i < shots.length; i += 2) {\n pairs.push([readPng(shots[i] as string), readPng(shots[i + 1] as string)]);\n }\n\n writeFileSync(out, encodePng(montage(pairs, { scale, gap })));\n console.log(out);\n\n return 0;\n }\n } catch (error) {\n console.error(`three-studio: ${(error as Error).message}`);\n\n return 2;\n }\n\n console.error(HELP);\n\n return 2;\n};\n","#!/usr/bin/env node\n/** The `three-studio` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":";;;;;;;;;;AAaA,MAAM,OAAO;;;;;4CAK+B,UAAU;;;;;;;;;;;;;;;;;;AAmBtD,MAAM,gBAAgB;CACpB,KAAK,IAAI,KAAK,OAAO,GAAG,UAAU,IAAI,MAAM,OAC1C,IAAI;EACF,MAAM,MAAM,KAAK,MACf,aAAa,IAAI,IAAI,GAAG,GAAG,eAAe,YAAY,GAAG,GAAG,MAAM,CACpE;EAKA,IAAI,IAAI,SAAS,wBACf,OAAO,IAAI;CAEf,QAAQ,CAER;CAGF,OAAO;AACT;AAEA,MAAM,WAAW,SAAiB,UAAU,aAAa,IAAI,CAAC;AAE9D,MAAM,WAAW,MAA0B,MAAc,aAAsB;CAC7E,IAAI,SAAS,KAAA,GACX,OAAO;CAGT,MAAM,QAAQ,OAAO,IAAI;CAEzB,IAAI,EAAE,SAAS,KAAK,SAAS,MAC3B,MAAM,IAAI,MAAM,KAAK,KAAK,oCAAoC;CAGhE,OAAO,QAAQ;AACjB;AAOA,MAAa,OAAO,OAAO,MAAgB,UAAuB,CAAC,MAAuB;CACxF,IAAI;CAEJ,IAAI;EACF,SAAS,UAAU;GACjB,MAAM;GACN,kBAAkB;GAClB,SAAS;IACP,MAAM,EAAE,MAAM,SAAS;IACvB,MAAM,EAAE,MAAM,SAAS;IACvB,KAAK,EAAE,MAAM,SAAS;IACtB,KAAK,EAAE,MAAM,SAAS;IACtB,MAAM;KAAE,MAAM;KAAW,SAAS;IAAM;IACxC,OAAO,EAAE,MAAM,SAAS;IACxB,KAAK,EAAE,MAAM,SAAS;IACtB,MAAM;KAAE,MAAM;KAAW,OAAO;KAAK,SAAS;IAAM;IACpD,SAAS;KAAE,MAAM;KAAW,OAAO;KAAK,SAAS;IAAM;GACzD;EACF,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,MAAM,iBAAkB,MAAgB,QAAQ,MAAM,MAAM;EAEpE,OAAO;CACT;CAEA,MAAM,EAAE,QAAQ,gBAAgB;CAChC,MAAM,CAAC,SAAS,GAAG,SAAS;CAE5B,IAAI,OAAO,MAAM;EACf,QAAQ,IAAI,IAAI;EAEhB,OAAO;CACT;CAEA,IAAI,OAAO,SAAS;EAClB,QAAQ,IAAI,QAAQ,CAAC;EAErB,OAAO;CACT;CAEA,IAAI;EACF,IAAI,YAAY,WAAW,MAAM,WAAW,GAAG;GAC7C,MAAM,OAAO,OAAO,SAAS,KAAA,IAAY,YAAY,OAAO,OAAO,IAAI;GAEvE,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,MAAM,8BAA8B;GAGhD,MAAM,QAAQ,MAAM,gBAAgB,MAAM,IAAc;IAAE;IAAM,KAAK,QAAQ;GAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-studio serve | diff | montage`: receive the page's shots, and compare them. `diff` exits 1\n * past `--max`, so a stored baseline can gate CI.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { parseArgs } from \"node:util\";\n\nimport { SHOT_PORT } from \"../core/capture.ts\";\nimport { diffImages, montage } from \"../node/compare.ts\";\nimport { decodePng, encodePng } from \"../node/png.ts\";\nimport { startShotServer } from \"../node/serve.ts\";\n\nconst HELP = `Usage: three-studio <command> [options]\n\nCommands:\n serve <folder> Receive saveShot() PNGs from the page and write them inside\n <folder>. Local only. Runs until stopped.\n --port <n> Default ${SHOT_PORT}, the port saveShot() posts to\n\n diff <before.png> <after.png> Count the pixels that differ. Prints \"<n> px differ (<share>%)\".\n --fuzz <percent> How far a channel may move and still match (default 3)\n --max <percent> Exit 1 when more than this share differs (default: never)\n --out <mask.png> Write the differing pixels in white over a dimmed before\n --json Print the result as JSON\n\n montage <out.png> <before> <after> [<before> <after>…]\n One row per pair, before left and after right, rows stacked.\n --scale <n> Scale of the result (default 0.5)\n --gap <px> Space between images, before scaling (default 8)\n\n -h, --help Show this help\n -v, --version Show the version\n\nExits 2 on bad arguments or a file it can't read.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-studio\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\nconst readPng = (file: string) => decodePng(readFileSync(file));\n\nconst percent = (text: string | undefined, name: string, fallback?: number) => {\n if (text === undefined) {\n return fallback;\n }\n\n const value = Number(text);\n\n if (!(value >= 0 && value <= 100)) {\n throw new Error(`--${name} must be a percentage from 0 to 100`);\n }\n\n return value / 100;\n};\n\nexport type MainOptions = {\n /** Resolves when `serve` should stop. Default: SIGINT or SIGTERM. */\n stop?: Promise<unknown> | undefined;\n};\n\nexport const main = async (argv: string[], options: MainOptions = {}): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n port: { type: \"string\" },\n fuzz: { type: \"string\" },\n max: { type: \"string\" },\n out: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n scale: { type: \"string\" },\n gap: { type: \"string\" },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-studio: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n const [command, ...files] = positionals;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n try {\n if (command === \"serve\" && files.length === 1) {\n const port = values.port === undefined ? SHOT_PORT : Number(values.port);\n\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(\"--port must be a port number\");\n }\n\n const shots = await startShotServer(files[0] as string, { port, log: console.log }).catch(\n (error: NodeJS.ErrnoException) => {\n throw error.code === \"EADDRINUSE\"\n ? new Error(\n `port ${port} is taken, by another receiver or app: pass --port <free port>, and open the page with &shots=<that port>`,\n )\n : error;\n },\n );\n\n console.log(`three-studio: writing .png files inside ${shots.root} from ${shots.url}`);\n await (options.stop ??\n new Promise((stop) => {\n process.once(\"SIGINT\", stop);\n process.once(\"SIGTERM\", stop);\n }));\n await shots.close();\n\n return 0;\n }\n\n if (command === \"diff\" && files.length === 2) {\n const fuzz = percent(values.fuzz, \"fuzz\", 0.03);\n const max = percent(values.max, \"max\");\n const result = diffImages(readPng(files[0] as string), readPng(files[1] as string), { fuzz });\n const over = max !== undefined && result.share > max;\n\n if (values.out) {\n writeFileSync(values.out, encodePng(result.mask));\n }\n\n if (values.json) {\n console.log(\n JSON.stringify({ pixels: result.pixels, total: result.total, share: result.share, over }),\n );\n } else {\n console.log(`${result.pixels} px differ (${(result.share * 100).toFixed(4)}%)`);\n }\n\n return over ? 1 : 0;\n }\n\n if (command === \"montage\" && files.length >= 3 && files.length % 2 === 1) {\n const [out, ...shots] = files as [string, ...string[]];\n const scale = values.scale === undefined ? 0.5 : Number(values.scale);\n const gap = values.gap === undefined ? 8 : Number(values.gap);\n\n if (!(scale > 0) || !(gap >= 0)) {\n throw new Error(\"--scale must be above 0 and --gap 0 or more\");\n }\n\n const pairs: [ReturnType<typeof readPng>, ReturnType<typeof readPng>][] = [];\n\n for (let i = 0; i < shots.length; i += 2) {\n pairs.push([readPng(shots[i] as string), readPng(shots[i + 1] as string)]);\n }\n\n writeFileSync(out, encodePng(montage(pairs, { scale, gap })));\n console.log(out);\n\n return 0;\n }\n } catch (error) {\n console.error(`three-studio: ${(error as Error).message}`);\n\n return 2;\n }\n\n console.error(HELP);\n\n return 2;\n};\n","#!/usr/bin/env node\n/** The `three-studio` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as saveShot, S as installFrameShim, _ as setWireframe, a as StudioVariant, b as ShotSize, c as Vec3, d as studioTime, f as titleOf, g as frameStats, h as formatStats, i as StudioState, l as resolveStudio, m as StatsRenderer, n as StudioConfig, o as StudioView, p as FrameStats, r as StudioLink, s as StudioViews, t as StudioArea, u as studioHref, v as SHOT_PORT, x as canvasPng, y as SaveShotOptions } from "./state-
|
|
1
|
+
import { C as saveShot, S as installFrameShim, _ as setWireframe, a as StudioVariant, b as ShotSize, c as Vec3, d as studioTime, f as titleOf, g as frameStats, h as formatStats, i as StudioState, l as resolveStudio, m as StatsRenderer, n as StudioConfig, o as StudioView, p as FrameStats, r as StudioLink, s as StudioViews, t as StudioArea, u as studioHref, v as SHOT_PORT, x as canvasPng, y as SaveShotOptions } from "./state-KovV7yXI.js";
|
|
2
2
|
import { Camera, Vector3 } from "three";
|
|
3
3
|
/** What `applyView` moves alongside the camera: OrbitControls and the like. */
|
|
4
4
|
type ViewControls = {
|
|
@@ -21,11 +21,17 @@ type OrbitViewsOptions = {
|
|
|
21
21
|
} | undefined;
|
|
22
22
|
/** The lens of every view but `play`. Default 40. */
|
|
23
23
|
fov?: number | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Where `front` looks from: the direction from the object to a camera facing its front. Default
|
|
26
|
+
* `[1, 0.25, 0]`, for kits whose front is +x; `[0, 0.25, -1]` for one facing -z, like a vehicle
|
|
27
|
+
* driving down -z. `side` is a quarter turn round from it and `back` opposite, at its height.
|
|
28
|
+
*/
|
|
29
|
+
front?: Vec3 | undefined;
|
|
24
30
|
};
|
|
25
31
|
/**
|
|
26
32
|
* The views every studio wants around one object at `centre`: `play` (the game camera's exact
|
|
27
33
|
* bearing, distance and lens, when given), `close` (that bearing, `near` away), and `front`,
|
|
28
|
-
* `side`, `back` and `top` at `near`. Front looks down -x
|
|
34
|
+
* `side`, `back` and `top` at `near`. Front looks down -x by default (`options.front` turns it).
|
|
29
35
|
*/
|
|
30
36
|
export declare function orbitViews(centre: Vec3, near: number, options?: OrbitViewsOptions): StudioViews;
|
|
31
37
|
export { type FrameStats, type OrbitViewsOptions, SHOT_PORT, type SaveShotOptions, type ShotSize, type StatsRenderer, type StudioArea, type StudioConfig, type StudioLink, type StudioState, type StudioVariant, type StudioView, type StudioViews, type Vec3, type ViewControls, canvasPng, formatStats, frameStats, installFrameShim, resolveStudio, saveShot, setWireframe, studioHref, studioTime, titleOf };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { i as saveShot, n as canvasPng, r as installFrameShim, t as SHOT_PORT } from "./capture-D6phUGvg.js";
|
|
2
|
-
import { a as studioTime, c as frameStats, i as studioHref, l as setWireframe, n as orbitViews, o as titleOf, r as resolveStudio, s as formatStats, t as applyView } from "./views-
|
|
2
|
+
import { a as studioTime, c as frameStats, i as studioHref, l as setWireframe, n as orbitViews, o as titleOf, r as resolveStudio, s as formatStats, t as applyView } from "./views-BH_N7aiA.js";
|
|
3
3
|
export { SHOT_PORT, applyView, canvasPng, formatStats, frameStats, installFrameShim, orbitViews, resolveStudio, saveShot, setWireframe, studioHref, studioTime, titleOf };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as StudioState, m as StatsRenderer, y as SaveShotOptions } from "./state-
|
|
1
|
+
import { i as StudioState, m as StatsRenderer, y as SaveShotOptions } from "./state-KovV7yXI.js";
|
|
2
2
|
import { AuditHelpersOptions } from "@zkmake/three-audit";
|
|
3
3
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
4
4
|
import { Camera, Object3D } from "three";
|
|
@@ -98,4 +98,4 @@ declare global {
|
|
|
98
98
|
*/
|
|
99
99
|
declare function mountStudio(options: MountStudioOptions): StudioHandle;
|
|
100
100
|
export { mountStudio as a, mountStudioOverlay as c, StudioRenderer as i, StudioGlobal as n, StudioOverlay as o, StudioHandle as r, StudioOverlayOptions as s, MountStudioOptions as t };
|
|
101
|
-
//# sourceMappingURL=mount-
|
|
101
|
+
//# sourceMappingURL=mount-CeU8JUWr.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as saveShot, r as installFrameShim } from "./capture-D6phUGvg.js";
|
|
2
|
-
import { c as frameStats, i as studioHref, l as setWireframe, o as titleOf, s as formatStats, t as applyView } from "./views-
|
|
2
|
+
import { c as frameStats, i as studioHref, l as setWireframe, o as titleOf, s as formatStats, t as applyView } from "./views-BH_N7aiA.js";
|
|
3
3
|
import { installAuditHelpers } from "@zkmake/three-audit";
|
|
4
4
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
5
5
|
// src/ui/styles.ts
|
|
@@ -185,7 +185,7 @@ function mountStudio(options) {
|
|
|
185
185
|
canvas: renderer.domElement,
|
|
186
186
|
render,
|
|
187
187
|
stats,
|
|
188
|
-
endpoint: options.shotEndpoint,
|
|
188
|
+
endpoint: options.shotEndpoint ?? (state.shots === null ? void 0 : `http://127.0.0.1:${state.shots}`),
|
|
189
189
|
...shotOptions
|
|
190
190
|
});
|
|
191
191
|
const overlayOptions = typeof options.overlay === "object" ? options.overlay : {};
|
|
@@ -239,4 +239,4 @@ function mountStudio(options) {
|
|
|
239
239
|
}
|
|
240
240
|
export { injectStyles as i, mountStudioOverlay as n, STUDIO_STYLES as r, mountStudio as t };
|
|
241
241
|
|
|
242
|
-
//# sourceMappingURL=mount-
|
|
242
|
+
//# sourceMappingURL=mount-DrLKxP3Q.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount-D3fYtnHO.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/overlay.ts","../src/ui/mount.ts"],"sourcesContent":["/** The overlay's stylesheet, injected once. Chips on a frosted pill, light or dark by the system. */\nexport const STUDIO_STYLES = /* css */ `\n.tks {\n --tks-bg: rgba(22, 24, 29, 0.78);\n --tks-fg: #e6e8eb;\n --tks-dim: #9aa0aa;\n --tks-on-bg: #f2f3f5;\n --tks-on-fg: #16181d;\n --tks-pick: #ffe27a;\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n pointer-events: none;\n font: 12px/1.3 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n color: var(--tks-fg);\n}\n@media (prefers-color-scheme: light) {\n .tks {\n --tks-bg: rgba(250, 250, 252, 0.86);\n --tks-fg: #1a1c21;\n --tks-dim: #6b7079;\n --tks-on-bg: #1a1c21;\n --tks-on-fg: #f6f7f9;\n --tks-pick: #8a6d00;\n }\n}\n.tks *, .tks *::before, .tks *::after { box-sizing: border-box; }\n.tks__col {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: calc(50vw - 18px);\n}\n.tks__col--start {\n top: calc(10px + env(safe-area-inset-top, 0px));\n left: calc(10px + env(safe-area-inset-left, 0px));\n align-items: flex-start;\n}\n.tks__col--end {\n top: calc(10px + env(safe-area-inset-top, 0px));\n right: calc(10px + env(safe-area-inset-right, 0px));\n align-items: flex-end;\n}\n.tks__row {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 3px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n pointer-events: auto;\n}\n.tks__col--end .tks__row { justify-content: flex-end; }\n.tks__chip {\n padding: 4px 8px;\n border-radius: 6px;\n color: inherit;\n text-decoration: none;\n white-space: nowrap;\n}\n.tks__chip:hover { background: rgba(127, 127, 127, 0.18); }\n.tks__chip:focus-visible { outline: 2px solid var(--tks-pick); outline-offset: 1px; }\n.tks__chip[aria-current=\"true\"] { background: var(--tks-on-bg); color: var(--tks-on-fg); }\n.tks__row--variants .tks__chip[aria-current=\"true\"] { box-shadow: inset 0 -2px 0 var(--tks-pick); }\n.tks__stats {\n padding: 5px 9px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n font-variant-numeric: tabular-nums;\n pointer-events: auto;\n}\n.tks__stats:empty { display: none; }\n.tks__note { color: var(--tks-dim); }\n`;\n\nconst STYLE_ID = \"three-studio-styles\";\n\nexport const injectStyles = (target: Document = document) => {\n if (target.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = target.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = STUDIO_STYLES;\n target.head.append(style);\n};\n","import { type StudioState, studioHref, titleOf } from \"../core/state.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type StudioOverlayOptions = {\n state: StudioState;\n /** Default `document.body`. */\n container?: HTMLElement | undefined;\n /**\n * Switches the view in place when a view chip is clicked (no reload). Without it the chips are\n * plain links. Area and variant chips always load their link, since they mount other things.\n */\n onView?: ((name: string) => void) | undefined;\n /** Default true. */\n injectStyles?: boolean | undefined;\n};\n\nexport type StudioOverlay = {\n element: HTMLElement;\n /** The readout line, e.g. `formatStats(frameStats(renderer))`. */\n setStats: (text: string) => void;\n /** Marks a view chip current, after a switch made from code. */\n setView: (name: string) => void;\n dispose: () => void;\n};\n\nconst chip = (text: string, href: string, current: boolean) => {\n const link = document.createElement(\"a\");\n\n link.className = \"tks__chip\";\n link.href = href;\n link.textContent = text;\n link.setAttribute(\"aria-current\", String(current));\n\n return link;\n};\n\nconst row = (label: string, kind: string, links: HTMLAnchorElement[]) => {\n const nav = document.createElement(\"nav\");\n\n nav.className = `tks__row tks__row--${kind}`;\n nav.setAttribute(\"aria-label\", label);\n nav.append(...links);\n\n return nav;\n};\n\n/**\n * The switchers over the canvas: areas and variants top left with the readout under them, views\n * top right. Rows with one entry are left out. Nothing shows under `?clean=1`. The overlay is DOM,\n * so `saveShot` never captures it.\n */\nexport function mountStudioOverlay(options: StudioOverlayOptions): StudioOverlay {\n const { state, onView } = options;\n const container = options.container ?? document.body;\n\n if (options.injectStyles !== false) {\n injectStyles(container.ownerDocument);\n }\n\n const element = document.createElement(\"div\");\n\n element.className = \"tks\";\n element.dataset.studio = state.tag;\n element.hidden = state.clean;\n\n const start = document.createElement(\"div\");\n const end = document.createElement(\"div\");\n const stats = document.createElement(\"div\");\n\n start.className = \"tks__col tks__col--start\";\n end.className = \"tks__col tks__col--end\";\n stats.className = \"tks__stats\";\n stats.setAttribute(\"aria-live\", \"off\");\n\n const areas = Object.entries(state.config.areas);\n const variants = Object.entries(state.area.variants);\n\n if (areas.length > 1) {\n start.append(\n row(\n \"Areas\",\n \"areas\",\n areas.map(([name, area]) =>\n chip(titleOf(name, area), studioHref(state, { area: name }), name === state.areaName),\n ),\n ),\n );\n }\n\n if (variants.length > 1) {\n start.append(\n row(\n \"Variants\",\n \"variants\",\n variants.map(([name, variant]) =>\n chip(\n titleOf(name, variant),\n studioHref(state, { variant: name }),\n name === state.variantName,\n ),\n ),\n ),\n );\n }\n\n start.append(stats);\n\n const viewLinks = Object.keys(state.views).map((name) => {\n const link = chip(name, studioHref(state, { view: name }), name === state.viewName);\n\n link.dataset.view = name;\n\n if (onView) {\n link.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {\n return;\n }\n\n event.preventDefault();\n onView(name);\n });\n }\n\n return link;\n });\n\n if (viewLinks.length > 1) {\n end.append(row(\"Views\", \"views\", viewLinks));\n }\n\n element.append(start, end);\n container.append(element);\n\n return {\n element,\n setStats: (text) => {\n if (stats.textContent !== text) {\n stats.textContent = text;\n }\n },\n setView: (name) => {\n for (const link of viewLinks) {\n link.setAttribute(\"aria-current\", String(link.dataset.view === name));\n }\n },\n dispose: () => element.remove(),\n };\n}\n","import { type AuditHelpersOptions, installAuditHelpers } from \"@zkmake/three-audit\";\nimport type { Camera, Object3D } from \"three\";\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\n\nimport { installFrameShim, type SaveShotOptions, saveShot } from \"../core/capture.ts\";\nimport { formatStats, frameStats, setWireframe, type StatsRenderer } from \"../core/frame.ts\";\nimport type { StudioState } from \"../core/state.ts\";\nimport { applyView } from \"../core/views.ts\";\nimport { mountStudioOverlay, type StudioOverlay, type StudioOverlayOptions } from \"./overlay.ts\";\n\n/** What the studio needs of a renderer: `WebGLRenderer` and `WebGPURenderer` both fit. */\nexport type StudioRenderer = StatsRenderer & {\n domElement: HTMLCanvasElement;\n render: (scene: Object3D, camera: Camera) => unknown;\n};\n\nexport type MountStudioOptions = {\n state: StudioState;\n renderer: StudioRenderer;\n scene: Object3D;\n camera: Camera;\n /**\n * Draws one frame, for `saveShot`. Default `renderer.render(scene, camera)`; pass the\n * composer's render when the page has post-processing.\n */\n render?: (() => void) | undefined;\n /** OrbitControls on the canvas, starting at the view. Default true. */\n controls?: boolean | undefined;\n /**\n * three-audit's console helpers (`summary()`, `zfight()`, `census()`…) on the scene, with the\n * variant's group as the `studioObject`. Default true; options pass through.\n */\n audit?: boolean | Omit<AuditHelpersOptions, \"renderer\"> | undefined;\n /** The switchers and readout. Default true; options pass through. */\n overlay?: boolean | Omit<StudioOverlayOptions, \"state\" | \"onView\"> | undefined;\n /** `window.studio` and `window.saveShot`, for agents and the console. Default true. */\n globals?: boolean | undefined;\n /** Where `saveShot` posts. Default `http://127.0.0.1:8787` (`three-studio serve`). */\n shotEndpoint?: string | undefined;\n};\n\nexport type StudioHandle = {\n state: StudioState;\n camera: Camera;\n controls: OrbitControls | null;\n overlay: StudioOverlay | null;\n /** Moves the camera to a named view in place and puts it in the URL, so a reload keeps it. */\n setView: (name: string) => void;\n /** Draws a frame and saves it as a PNG at `path` (see `three-studio serve`). */\n saveShot: (path: string, options?: SaveShotOptions) => Promise<string>;\n /** The last frame's readout: `\"12 calls · 34,567 tris\"`. */\n stats: () => string;\n /** Call after each render: refreshes the readout and keeps `?wire=1` on new meshes. */\n update: () => void;\n dispose: () => void;\n};\n\n/** The object `window.studio` holds while a studio is mounted. */\nexport type StudioGlobal = Pick<\n StudioHandle,\n \"state\" | \"camera\" | \"setView\" | \"saveShot\" | \"stats\"\n> & {\n scene: Object3D;\n renderer: StudioRenderer;\n /** The view names `setView` takes. */\n views: string[];\n /** For a hidden browser pane: see `installFrameShim`. */\n installFrameShim: () => boolean;\n};\n\ndeclare global {\n interface Window {\n studio?: StudioGlobal;\n saveShot?: StudioHandle[\"saveShot\"];\n }\n}\n\n/**\n * Wires a page around one renderer: camera at the URL's view, orbit controls, three-audit's console\n * helpers, the overlay, `window.studio`, and `?wire=1`. The page builds the scene itself and tags\n * what's under study with `userData.studioObject = state.tag`.\n *\n * const state = resolveStudio(config, location.search);\n * const studio = mountStudio({ state, renderer, scene, camera });\n * renderer.setAnimationLoop(() => { renderer.render(scene, camera); studio.update(); });\n */\nexport function mountStudio(options: MountStudioOptions): StudioHandle {\n const { state, renderer, scene, camera } = options;\n const render = options.render ?? (() => renderer.render(scene, camera));\n const controls =\n options.controls === false ? null : new OrbitControls(camera, renderer.domElement);\n // The page's window; none when rendered under Node (tests, SSR), where only the camera moves.\n const page = typeof window === \"undefined\" ? null : window;\n\n applyView(camera, state.view, controls);\n\n const stats = () => formatStats(frameStats(renderer));\n\n const setView = (name: string) => {\n const view = state.views[name];\n\n if (!view) {\n throw new Error(\n `three-studio: no view \"${name}\"; this page has ${Object.keys(state.views).join(\", \")}`,\n );\n }\n\n applyView(camera, view, controls);\n overlay?.setView(name);\n\n if (page) {\n const url = new URL(page.location.href);\n\n url.searchParams.set(\"view\", name);\n page.history.replaceState(page.history.state, \"\", url);\n }\n };\n\n const shot = (path: string, shotOptions: SaveShotOptions = {}) =>\n saveShot(path, {\n canvas: renderer.domElement,\n render,\n stats,\n endpoint: options.shotEndpoint,\n ...shotOptions,\n });\n\n const overlayOptions = typeof options.overlay === \"object\" ? options.overlay : {};\n const overlay =\n options.overlay === false\n ? null\n : mountStudioOverlay({ ...overlayOptions, state, onView: setView });\n\n const auditOptions = typeof options.audit === \"object\" ? options.audit : {};\n const uninstallAudit =\n options.audit === false\n ? null\n : installAuditHelpers(scene, {\n ...auditOptions,\n renderer: renderer as unknown as AuditHelpersOptions[\"renderer\"],\n });\n\n const global: StudioGlobal = {\n state,\n scene,\n camera,\n renderer,\n views: Object.keys(state.views),\n setView,\n saveShot: shot,\n stats,\n installFrameShim,\n };\n\n if (page && options.globals !== false) {\n page.studio = global;\n page.saveShot = shot;\n }\n\n return {\n state,\n camera,\n controls,\n overlay,\n setView,\n saveShot: shot,\n stats,\n update: () => {\n overlay?.setStats(stats());\n\n if (state.wire) {\n setWireframe(scene, true);\n }\n },\n dispose: () => {\n controls?.dispose();\n overlay?.dispose();\n uninstallAudit?.();\n\n if (page?.studio === global) {\n delete page.studio;\n delete page.saveShot;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mount-DrLKxP3Q.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/overlay.ts","../src/ui/mount.ts"],"sourcesContent":["/** The overlay's stylesheet, injected once. Chips on a frosted pill, light or dark by the system. */\nexport const STUDIO_STYLES = /* css */ `\n.tks {\n --tks-bg: rgba(22, 24, 29, 0.78);\n --tks-fg: #e6e8eb;\n --tks-dim: #9aa0aa;\n --tks-on-bg: #f2f3f5;\n --tks-on-fg: #16181d;\n --tks-pick: #ffe27a;\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n pointer-events: none;\n font: 12px/1.3 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n color: var(--tks-fg);\n}\n@media (prefers-color-scheme: light) {\n .tks {\n --tks-bg: rgba(250, 250, 252, 0.86);\n --tks-fg: #1a1c21;\n --tks-dim: #6b7079;\n --tks-on-bg: #1a1c21;\n --tks-on-fg: #f6f7f9;\n --tks-pick: #8a6d00;\n }\n}\n.tks *, .tks *::before, .tks *::after { box-sizing: border-box; }\n.tks__col {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: calc(50vw - 18px);\n}\n.tks__col--start {\n top: calc(10px + env(safe-area-inset-top, 0px));\n left: calc(10px + env(safe-area-inset-left, 0px));\n align-items: flex-start;\n}\n.tks__col--end {\n top: calc(10px + env(safe-area-inset-top, 0px));\n right: calc(10px + env(safe-area-inset-right, 0px));\n align-items: flex-end;\n}\n.tks__row {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 3px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n pointer-events: auto;\n}\n.tks__col--end .tks__row { justify-content: flex-end; }\n.tks__chip {\n padding: 4px 8px;\n border-radius: 6px;\n color: inherit;\n text-decoration: none;\n white-space: nowrap;\n}\n.tks__chip:hover { background: rgba(127, 127, 127, 0.18); }\n.tks__chip:focus-visible { outline: 2px solid var(--tks-pick); outline-offset: 1px; }\n.tks__chip[aria-current=\"true\"] { background: var(--tks-on-bg); color: var(--tks-on-fg); }\n.tks__row--variants .tks__chip[aria-current=\"true\"] { box-shadow: inset 0 -2px 0 var(--tks-pick); }\n.tks__stats {\n padding: 5px 9px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n font-variant-numeric: tabular-nums;\n pointer-events: auto;\n}\n.tks__stats:empty { display: none; }\n.tks__note { color: var(--tks-dim); }\n`;\n\nconst STYLE_ID = \"three-studio-styles\";\n\nexport const injectStyles = (target: Document = document) => {\n if (target.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = target.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = STUDIO_STYLES;\n target.head.append(style);\n};\n","import { type StudioState, studioHref, titleOf } from \"../core/state.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type StudioOverlayOptions = {\n state: StudioState;\n /** Default `document.body`. */\n container?: HTMLElement | undefined;\n /**\n * Switches the view in place when a view chip is clicked (no reload). Without it the chips are\n * plain links. Area and variant chips always load their link, since they mount other things.\n */\n onView?: ((name: string) => void) | undefined;\n /** Default true. */\n injectStyles?: boolean | undefined;\n};\n\nexport type StudioOverlay = {\n element: HTMLElement;\n /** The readout line, e.g. `formatStats(frameStats(renderer))`. */\n setStats: (text: string) => void;\n /** Marks a view chip current, after a switch made from code. */\n setView: (name: string) => void;\n dispose: () => void;\n};\n\nconst chip = (text: string, href: string, current: boolean) => {\n const link = document.createElement(\"a\");\n\n link.className = \"tks__chip\";\n link.href = href;\n link.textContent = text;\n link.setAttribute(\"aria-current\", String(current));\n\n return link;\n};\n\nconst row = (label: string, kind: string, links: HTMLAnchorElement[]) => {\n const nav = document.createElement(\"nav\");\n\n nav.className = `tks__row tks__row--${kind}`;\n nav.setAttribute(\"aria-label\", label);\n nav.append(...links);\n\n return nav;\n};\n\n/**\n * The switchers over the canvas: areas and variants top left with the readout under them, views\n * top right. Rows with one entry are left out. Nothing shows under `?clean=1`. The overlay is DOM,\n * so `saveShot` never captures it.\n */\nexport function mountStudioOverlay(options: StudioOverlayOptions): StudioOverlay {\n const { state, onView } = options;\n const container = options.container ?? document.body;\n\n if (options.injectStyles !== false) {\n injectStyles(container.ownerDocument);\n }\n\n const element = document.createElement(\"div\");\n\n element.className = \"tks\";\n element.dataset.studio = state.tag;\n element.hidden = state.clean;\n\n const start = document.createElement(\"div\");\n const end = document.createElement(\"div\");\n const stats = document.createElement(\"div\");\n\n start.className = \"tks__col tks__col--start\";\n end.className = \"tks__col tks__col--end\";\n stats.className = \"tks__stats\";\n stats.setAttribute(\"aria-live\", \"off\");\n\n const areas = Object.entries(state.config.areas);\n const variants = Object.entries(state.area.variants);\n\n if (areas.length > 1) {\n start.append(\n row(\n \"Areas\",\n \"areas\",\n areas.map(([name, area]) =>\n chip(titleOf(name, area), studioHref(state, { area: name }), name === state.areaName),\n ),\n ),\n );\n }\n\n if (variants.length > 1) {\n start.append(\n row(\n \"Variants\",\n \"variants\",\n variants.map(([name, variant]) =>\n chip(\n titleOf(name, variant),\n studioHref(state, { variant: name }),\n name === state.variantName,\n ),\n ),\n ),\n );\n }\n\n start.append(stats);\n\n const viewLinks = Object.keys(state.views).map((name) => {\n const link = chip(name, studioHref(state, { view: name }), name === state.viewName);\n\n link.dataset.view = name;\n\n if (onView) {\n link.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {\n return;\n }\n\n event.preventDefault();\n onView(name);\n });\n }\n\n return link;\n });\n\n if (viewLinks.length > 1) {\n end.append(row(\"Views\", \"views\", viewLinks));\n }\n\n element.append(start, end);\n container.append(element);\n\n return {\n element,\n setStats: (text) => {\n if (stats.textContent !== text) {\n stats.textContent = text;\n }\n },\n setView: (name) => {\n for (const link of viewLinks) {\n link.setAttribute(\"aria-current\", String(link.dataset.view === name));\n }\n },\n dispose: () => element.remove(),\n };\n}\n","import { type AuditHelpersOptions, installAuditHelpers } from \"@zkmake/three-audit\";\nimport type { Camera, Object3D } from \"three\";\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\n\nimport { installFrameShim, type SaveShotOptions, saveShot } from \"../core/capture.ts\";\nimport { formatStats, frameStats, setWireframe, type StatsRenderer } from \"../core/frame.ts\";\nimport type { StudioState } from \"../core/state.ts\";\nimport { applyView } from \"../core/views.ts\";\nimport { mountStudioOverlay, type StudioOverlay, type StudioOverlayOptions } from \"./overlay.ts\";\n\n/** What the studio needs of a renderer: `WebGLRenderer` and `WebGPURenderer` both fit. */\nexport type StudioRenderer = StatsRenderer & {\n domElement: HTMLCanvasElement;\n render: (scene: Object3D, camera: Camera) => unknown;\n};\n\nexport type MountStudioOptions = {\n state: StudioState;\n renderer: StudioRenderer;\n scene: Object3D;\n camera: Camera;\n /**\n * Draws one frame, for `saveShot`. Default `renderer.render(scene, camera)`; pass the\n * composer's render when the page has post-processing.\n */\n render?: (() => void) | undefined;\n /** OrbitControls on the canvas, starting at the view. Default true. */\n controls?: boolean | undefined;\n /**\n * three-audit's console helpers (`summary()`, `zfight()`, `census()`…) on the scene, with the\n * variant's group as the `studioObject`. Default true; options pass through.\n */\n audit?: boolean | Omit<AuditHelpersOptions, \"renderer\"> | undefined;\n /** The switchers and readout. Default true; options pass through. */\n overlay?: boolean | Omit<StudioOverlayOptions, \"state\" | \"onView\"> | undefined;\n /** `window.studio` and `window.saveShot`, for agents and the console. Default true. */\n globals?: boolean | undefined;\n /** Where `saveShot` posts. Default `http://127.0.0.1:8787` (`three-studio serve`). */\n shotEndpoint?: string | undefined;\n};\n\nexport type StudioHandle = {\n state: StudioState;\n camera: Camera;\n controls: OrbitControls | null;\n overlay: StudioOverlay | null;\n /** Moves the camera to a named view in place and puts it in the URL, so a reload keeps it. */\n setView: (name: string) => void;\n /** Draws a frame and saves it as a PNG at `path` (see `three-studio serve`). */\n saveShot: (path: string, options?: SaveShotOptions) => Promise<string>;\n /** The last frame's readout: `\"12 calls · 34,567 tris\"`. */\n stats: () => string;\n /** Call after each render: refreshes the readout and keeps `?wire=1` on new meshes. */\n update: () => void;\n dispose: () => void;\n};\n\n/** The object `window.studio` holds while a studio is mounted. */\nexport type StudioGlobal = Pick<\n StudioHandle,\n \"state\" | \"camera\" | \"setView\" | \"saveShot\" | \"stats\"\n> & {\n scene: Object3D;\n renderer: StudioRenderer;\n /** The view names `setView` takes. */\n views: string[];\n /** For a hidden browser pane: see `installFrameShim`. */\n installFrameShim: () => boolean;\n};\n\ndeclare global {\n interface Window {\n studio?: StudioGlobal;\n saveShot?: StudioHandle[\"saveShot\"];\n }\n}\n\n/**\n * Wires a page around one renderer: camera at the URL's view, orbit controls, three-audit's console\n * helpers, the overlay, `window.studio`, and `?wire=1`. The page builds the scene itself and tags\n * what's under study with `userData.studioObject = state.tag`.\n *\n * const state = resolveStudio(config, location.search);\n * const studio = mountStudio({ state, renderer, scene, camera });\n * renderer.setAnimationLoop(() => { renderer.render(scene, camera); studio.update(); });\n */\nexport function mountStudio(options: MountStudioOptions): StudioHandle {\n const { state, renderer, scene, camera } = options;\n const render = options.render ?? (() => renderer.render(scene, camera));\n const controls =\n options.controls === false ? null : new OrbitControls(camera, renderer.domElement);\n // The page's window; none when rendered under Node (tests, SSR), where only the camera moves.\n const page = typeof window === \"undefined\" ? null : window;\n\n applyView(camera, state.view, controls);\n\n const stats = () => formatStats(frameStats(renderer));\n\n const setView = (name: string) => {\n const view = state.views[name];\n\n if (!view) {\n throw new Error(\n `three-studio: no view \"${name}\"; this page has ${Object.keys(state.views).join(\", \")}`,\n );\n }\n\n applyView(camera, view, controls);\n overlay?.setView(name);\n\n if (page) {\n const url = new URL(page.location.href);\n\n url.searchParams.set(\"view\", name);\n page.history.replaceState(page.history.state, \"\", url);\n }\n };\n\n const shot = (path: string, shotOptions: SaveShotOptions = {}) =>\n saveShot(path, {\n canvas: renderer.domElement,\n render,\n stats,\n endpoint:\n options.shotEndpoint ??\n (state.shots === null ? undefined : `http://127.0.0.1:${state.shots}`),\n ...shotOptions,\n });\n\n const overlayOptions = typeof options.overlay === \"object\" ? options.overlay : {};\n const overlay =\n options.overlay === false\n ? null\n : mountStudioOverlay({ ...overlayOptions, state, onView: setView });\n\n const auditOptions = typeof options.audit === \"object\" ? options.audit : {};\n const uninstallAudit =\n options.audit === false\n ? null\n : installAuditHelpers(scene, {\n ...auditOptions,\n renderer: renderer as unknown as AuditHelpersOptions[\"renderer\"],\n });\n\n const global: StudioGlobal = {\n state,\n scene,\n camera,\n renderer,\n views: Object.keys(state.views),\n setView,\n saveShot: shot,\n stats,\n installFrameShim,\n };\n\n if (page && options.globals !== false) {\n page.studio = global;\n page.saveShot = shot;\n }\n\n return {\n state,\n camera,\n controls,\n overlay,\n setView,\n saveShot: shot,\n stats,\n update: () => {\n overlay?.setStats(stats());\n\n if (state.wire) {\n setWireframe(scene, true);\n }\n },\n dispose: () => {\n controls?.dispose();\n overlay?.dispose();\n uninstallAudit?.();\n\n if (page?.studio === global) {\n delete page.studio;\n delete page.saveShot;\n }\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as StudioVariant, i as StudioState, n as StudioConfig } from "./state-
|
|
2
|
-
import { r as StudioHandle, t as MountStudioOptions } from "./mount-
|
|
1
|
+
import { a as StudioVariant, i as StudioState, n as StudioConfig } from "./state-KovV7yXI.js";
|
|
2
|
+
import { r as StudioHandle, t as MountStudioOptions } from "./mount-CeU8JUWr.js";
|
|
3
3
|
import { CanvasProps } from "@react-three/fiber";
|
|
4
4
|
import { CSSProperties, ReactNode } from "react";
|
|
5
5
|
/** A variant `<Studio>` can draw: what goes in the canvas, and optionally DOM over it. */
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as studioTime, r as resolveStudio } from "./views-
|
|
2
|
-
import { t as mountStudio } from "./mount-
|
|
1
|
+
import { a as studioTime, r as resolveStudio } from "./views-BH_N7aiA.js";
|
|
2
|
+
import { t as mountStudio } from "./mount-DrLKxP3Q.js";
|
|
3
3
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
|
4
4
|
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -64,7 +64,7 @@ declare function setWireframe(root: Object3D, on: boolean): void;
|
|
|
64
64
|
* the shot switches (`clean`, `wire`, `t`). Pure: the same config and query give the same state,
|
|
65
65
|
* so a link is a reproducible shot.
|
|
66
66
|
*
|
|
67
|
-
* ?area=<area>&v=<variant>&view=<view>&clean=1&wire=1&t=<seconds>
|
|
67
|
+
* ?area=<area>&v=<variant>&view=<view>&clean=1&wire=1&t=<seconds>&shots=<port>
|
|
68
68
|
*
|
|
69
69
|
* A bare `?<area>` (`?plate`) also picks an area, the way single-object model studios link.
|
|
70
70
|
*/
|
|
@@ -117,6 +117,11 @@ type StudioState<V extends StudioVariant = StudioVariant> = {
|
|
|
117
117
|
wire: boolean;
|
|
118
118
|
/** `?t=<s>`: animations frozen at this time, for steady shots; null when running. */
|
|
119
119
|
time: number | null;
|
|
120
|
+
/**
|
|
121
|
+
* `?shots=<port>`: the port `three-studio serve` listens on, when it isn't the default 8787
|
|
122
|
+
* (another receiver holds that one); `saveShot` posts there. Null for the default.
|
|
123
|
+
*/
|
|
124
|
+
shots: number | null;
|
|
120
125
|
/**
|
|
121
126
|
* `<area>-<variant>`, for `userData.studioObject` on the variant's group, so three-audit's
|
|
122
127
|
* `zfight()` and `summary()` audit only the thing under study.
|
|
@@ -137,7 +142,7 @@ type StudioLink = {
|
|
|
137
142
|
};
|
|
138
143
|
/**
|
|
139
144
|
* The query for the state with `change` applied, `?` included. A new area starts at its first
|
|
140
|
-
* variant; the view
|
|
145
|
+
* variant; the view, the `wire` / `t` switches and `shots` carry over, `clean` only when asked for.
|
|
141
146
|
*/
|
|
142
147
|
declare function studioHref(state: StudioState, change?: StudioLink): string;
|
|
143
148
|
/** The animation time to draw at: the frozen `?t=` when set, the clock's otherwise. */
|
|
@@ -148,4 +153,4 @@ declare const titleOf: (name: string, entry: {
|
|
|
148
153
|
label?: string | undefined;
|
|
149
154
|
}) => string;
|
|
150
155
|
export { saveShot as C, installFrameShim as S, setWireframe as _, StudioVariant as a, ShotSize as b, Vec3 as c, studioTime as d, titleOf as f, frameStats as g, formatStats as h, StudioState as i, resolveStudio as l, StatsRenderer as m, StudioConfig as n, StudioView as o, FrameStats as p, StudioLink as r, StudioViews as s, StudioArea as t, studioHref as u, SHOT_PORT as v, canvasPng as x, SaveShotOptions as y };
|
|
151
|
-
//# sourceMappingURL=state-
|
|
156
|
+
//# sourceMappingURL=state-KovV7yXI.d.ts.map
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as mountStudio, c as mountStudioOverlay, i as StudioRenderer, n as StudioGlobal, o as StudioOverlay, r as StudioHandle, s as StudioOverlayOptions, t as MountStudioOptions } from "./mount-
|
|
1
|
+
import { a as mountStudio, c as mountStudioOverlay, i as StudioRenderer, n as StudioGlobal, o as StudioOverlay, r as StudioHandle, s as StudioOverlayOptions, t as MountStudioOptions } from "./mount-CeU8JUWr.js";
|
|
2
2
|
/** The overlay's stylesheet, injected once. Chips on a frosted pill, light or dark by the system. */
|
|
3
3
|
export declare const STUDIO_STYLES = "\n.tks {\n --tks-bg: rgba(22, 24, 29, 0.78);\n --tks-fg: #e6e8eb;\n --tks-dim: #9aa0aa;\n --tks-on-bg: #f2f3f5;\n --tks-on-fg: #16181d;\n --tks-pick: #ffe27a;\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n pointer-events: none;\n font: 12px/1.3 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n color: var(--tks-fg);\n}\n@media (prefers-color-scheme: light) {\n .tks {\n --tks-bg: rgba(250, 250, 252, 0.86);\n --tks-fg: #1a1c21;\n --tks-dim: #6b7079;\n --tks-on-bg: #1a1c21;\n --tks-on-fg: #f6f7f9;\n --tks-pick: #8a6d00;\n }\n}\n.tks *, .tks *::before, .tks *::after { box-sizing: border-box; }\n.tks__col {\n position: absolute;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: calc(50vw - 18px);\n}\n.tks__col--start {\n top: calc(10px + env(safe-area-inset-top, 0px));\n left: calc(10px + env(safe-area-inset-left, 0px));\n align-items: flex-start;\n}\n.tks__col--end {\n top: calc(10px + env(safe-area-inset-top, 0px));\n right: calc(10px + env(safe-area-inset-right, 0px));\n align-items: flex-end;\n}\n.tks__row {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 3px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n pointer-events: auto;\n}\n.tks__col--end .tks__row { justify-content: flex-end; }\n.tks__chip {\n padding: 4px 8px;\n border-radius: 6px;\n color: inherit;\n text-decoration: none;\n white-space: nowrap;\n}\n.tks__chip:hover { background: rgba(127, 127, 127, 0.18); }\n.tks__chip:focus-visible { outline: 2px solid var(--tks-pick); outline-offset: 1px; }\n.tks__chip[aria-current=\"true\"] { background: var(--tks-on-bg); color: var(--tks-on-fg); }\n.tks__row--variants .tks__chip[aria-current=\"true\"] { box-shadow: inset 0 -2px 0 var(--tks-pick); }\n.tks__stats {\n padding: 5px 9px;\n border-radius: 9px;\n background: var(--tks-bg);\n backdrop-filter: blur(8px);\n font-variant-numeric: tabular-nums;\n pointer-events: auto;\n}\n.tks__stats:empty { display: none; }\n.tks__note { color: var(--tks-dim); }\n";
|
|
4
4
|
export declare const injectStyles: (target?: Document) => void;
|
package/dist/ui.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { r as STUDIO_STYLES, i as injectStyles, t as mountStudio, n as mountStudioOverlay } from "./mount-
|
|
1
|
+
export { r as STUDIO_STYLES, i as injectStyles, t as mountStudio, n as mountStudioOverlay } from "./mount-DrLKxP3Q.js";
|
|
@@ -84,6 +84,10 @@ function resolveStudio(config, search) {
|
|
|
84
84
|
if (askedView in views) viewName = askedView;
|
|
85
85
|
else unknown.push(`view=${askedView}`);
|
|
86
86
|
}
|
|
87
|
+
const shotsParam = params.get("shots");
|
|
88
|
+
const shots = shotsParam === null || shotsParam === "" ? null : Number(shotsParam);
|
|
89
|
+
const shotPort = shots !== null && Number.isInteger(shots) && shots > 0 && shots < 65536;
|
|
90
|
+
if (shots !== null && !shotPort) unknown.push(`shots=${shotsParam}`);
|
|
87
91
|
const time = params.get("t");
|
|
88
92
|
const seconds = time === null || time === "" ? null : Number(time);
|
|
89
93
|
if (seconds !== null && !Number.isFinite(seconds)) unknown.push(`t=${time}`);
|
|
@@ -99,6 +103,7 @@ function resolveStudio(config, search) {
|
|
|
99
103
|
clean: flag(params.get("clean")),
|
|
100
104
|
wire: flag(params.get("wire")),
|
|
101
105
|
time: seconds !== null && Number.isFinite(seconds) ? seconds : null,
|
|
106
|
+
shots: shotPort ? shots : null,
|
|
102
107
|
tag: `${areaName}-${variantName}`,
|
|
103
108
|
unknown
|
|
104
109
|
};
|
|
@@ -106,7 +111,7 @@ function resolveStudio(config, search) {
|
|
|
106
111
|
const flag = (value) => value !== null && value !== "0" && value !== "false";
|
|
107
112
|
/**
|
|
108
113
|
* The query for the state with `change` applied, `?` included. A new area starts at its first
|
|
109
|
-
* variant; the view
|
|
114
|
+
* variant; the view, the `wire` / `t` switches and `shots` carry over, `clean` only when asked for.
|
|
110
115
|
*/
|
|
111
116
|
function studioHref(state, change = {}) {
|
|
112
117
|
const area = change.area ?? state.areaName;
|
|
@@ -119,6 +124,7 @@ function studioHref(state, change = {}) {
|
|
|
119
124
|
if (change.clean) params.set("clean", "1");
|
|
120
125
|
const time = change.time === void 0 ? state.time : change.time;
|
|
121
126
|
if (time !== null) params.set("t", String(time));
|
|
127
|
+
if (state.shots !== null) params.set("shots", String(state.shots));
|
|
122
128
|
return `?${params}`;
|
|
123
129
|
}
|
|
124
130
|
/** The animation time to draw at: the frozen `?t=` when set, the clock's otherwise. */
|
|
@@ -160,7 +166,7 @@ const along = (centre, direction, distance, fov) => {
|
|
|
160
166
|
/**
|
|
161
167
|
* The views every studio wants around one object at `centre`: `play` (the game camera's exact
|
|
162
168
|
* bearing, distance and lens, when given), `close` (that bearing, `near` away), and `front`,
|
|
163
|
-
* `side`, `back` and `top` at `near`. Front looks down -x
|
|
169
|
+
* `side`, `back` and `top` at `near`. Front looks down -x by default (`options.front` turns it).
|
|
164
170
|
*/
|
|
165
171
|
function orbitViews(centre, near, options = {}) {
|
|
166
172
|
const fov = options.fov ?? 40;
|
|
@@ -184,20 +190,25 @@ function orbitViews(centre, near, options = {}) {
|
|
|
184
190
|
};
|
|
185
191
|
}
|
|
186
192
|
views.close = along(centre, bearing, near, fov);
|
|
187
|
-
|
|
193
|
+
const [fx, fy, fz] = options.front ?? [
|
|
188
194
|
1,
|
|
189
195
|
.25,
|
|
190
196
|
0
|
|
197
|
+
];
|
|
198
|
+
views.front = along(centre, [
|
|
199
|
+
fx,
|
|
200
|
+
fy,
|
|
201
|
+
fz
|
|
191
202
|
], near, fov);
|
|
192
203
|
views.side = along(centre, [
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
-fz,
|
|
205
|
+
fy,
|
|
206
|
+
fx
|
|
196
207
|
], near, fov);
|
|
197
208
|
views.back = along(centre, [
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
|
|
209
|
+
-fx,
|
|
210
|
+
fy,
|
|
211
|
+
-fz
|
|
201
212
|
], near, fov);
|
|
202
213
|
views.top = along(centre, [
|
|
203
214
|
.001,
|
|
@@ -208,4 +219,4 @@ function orbitViews(centre, near, options = {}) {
|
|
|
208
219
|
}
|
|
209
220
|
export { studioTime as a, frameStats as c, studioHref as i, setWireframe as l, orbitViews as n, titleOf as o, resolveStudio as r, formatStats as s, applyView as t };
|
|
210
221
|
|
|
211
|
-
//# sourceMappingURL=views-
|
|
222
|
+
//# sourceMappingURL=views-BH_N7aiA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views-BH_N7aiA.js","names":[],"sources":["../src/core/frame.ts","../src/core/state.ts","../src/core/views.ts"],"sourcesContent":["import type { Material, Object3D } from \"three\";\n\n/** What the readout needs of a renderer: `WebGLRenderer` and `WebGPURenderer` both fit. */\nexport type StatsRenderer = { info: { render: { calls: number; triangles: number } } };\n\nexport type FrameStats = { calls: number; triangles: number };\n\n/**\n * The draws and triangles of the renderer's last `render()` call, shadow passes included. A\n * composer that renders several passes reports its last one only; three-audit's `ledger()` counts\n * every pass.\n */\nexport const frameStats = (renderer: StatsRenderer): FrameStats => ({\n calls: renderer.info.render.calls,\n triangles: renderer.info.render.triangles,\n});\n\n/** `\"12 calls · 34,567 tris\"`, the line every studio page shows. */\nexport const formatStats = ({ calls, triangles }: FrameStats): string =>\n `${calls} calls · ${triangles.toLocaleString(\"en-US\")} tris`;\n\nconst materialsOf = (object: Object3D): Material[] => {\n const material = (object as Object3D & { material?: Material | Material[] }).material;\n\n return Array.isArray(material) ? material : material ? [material] : [];\n};\n\n/**\n * Draws every material under `root` as wireframe, or puts back what each had. Cheap enough to\n * call each frame, so meshes that mount later are covered too.\n */\nexport function setWireframe(root: Object3D, on: boolean) {\n root.traverse((object) => {\n for (const material of materialsOf(object)) {\n const wire = material as Material & { wireframe?: boolean };\n\n if (!(\"wireframe\" in wire)) {\n continue;\n }\n\n const saved = material.userData.studioWireframe as boolean | undefined;\n\n if (on && saved === undefined) {\n material.userData.studioWireframe = Boolean(wire.wireframe);\n wire.wireframe = true;\n } else if (!on && saved !== undefined) {\n wire.wireframe = saved;\n delete material.userData.studioWireframe;\n }\n }\n });\n}\n","/**\n * The page's state, read from its URL: which area, which variant of it, which camera view, and\n * the shot switches (`clean`, `wire`, `t`). Pure: the same config and query give the same state,\n * so a link is a reproducible shot.\n *\n * ?area=<area>&v=<variant>&view=<view>&clean=1&wire=1&t=<seconds>&shots=<port>\n *\n * A bare `?<area>` (`?plate`) also picks an area, the way single-object model studios link.\n */\n\nexport type Vec3 = readonly [number, number, number];\n\n/** A camera pose. `fov`, `near` and `far` apply to a perspective camera, `zoom` to any. */\nexport type StudioView = {\n position: Vec3;\n target: Vec3;\n fov?: number | undefined;\n near?: number | undefined;\n far?: number | undefined;\n zoom?: number | undefined;\n};\n\nexport type StudioViews = Readonly<Record<string, StudioView>>;\n\n/** One look for one area. Adapters add what they render (`render`, `hud`); views it brings win. */\nexport type StudioVariant = {\n /** Its chip's text; the key otherwise. */\n label?: string | undefined;\n /** Views of its own (a camera variant's poses), over the area's and the page's. */\n views?: StudioViews | undefined;\n};\n\nexport type StudioArea<V extends StudioVariant = StudioVariant> = {\n /** Its chip's text; the key otherwise. */\n title?: string | undefined;\n variants: Readonly<Record<string, V>>;\n /** Views for this area, over the page's. */\n views?: StudioViews | undefined;\n};\n\nexport type StudioConfig<V extends StudioVariant = StudioVariant> = {\n /** In chip order; the first is the default. */\n areas: Readonly<Record<string, StudioArea<V>>>;\n /** Views every area has (`play`, copied from the game camera, belongs here). */\n views?: StudioViews | undefined;\n /** The view a link without `view=` opens, and the fallback for an unknown one. Default `play`. */\n defaultView?: string | undefined;\n};\n\nexport type StudioState<V extends StudioVariant = StudioVariant> = {\n config: StudioConfig<V>;\n areaName: string;\n area: StudioArea<V>;\n variantName: string;\n variant: V;\n viewName: string;\n view: StudioView;\n /** Every view this variant can take: the page's, the area's, then its own. */\n views: StudioViews;\n /** `?clean=1`: no overlay, for shots taken of the whole window. */\n clean: boolean;\n /** `?wire=1`: every material drawn as wireframe, to read the topology. */\n wire: boolean;\n /** `?t=<s>`: animations frozen at this time, for steady shots; null when running. */\n time: number | null;\n /**\n * `?shots=<port>`: the port `three-studio serve` listens on, when it isn't the default 8787\n * (another receiver holds that one); `saveShot` posts there. Null for the default.\n */\n shots: number | null;\n /**\n * `<area>-<variant>`, for `userData.studioObject` on the variant's group, so three-audit's\n * `zfight()` and `summary()` audit only the thing under study.\n */\n tag: string;\n /** Names asked for in the URL that the config doesn't have; the defaults were used instead. */\n unknown: string[];\n};\n\nconst FALLBACK_VIEW: StudioView = { position: [4, 3, 6], target: [0, 0.5, 0], fov: 40 };\n\nconst first = <T>(record: Readonly<Record<string, T>>): [string, T] | undefined =>\n Object.entries(record)[0];\n\n/** Reads a `location.search` (or any query string) against the config. */\nexport function resolveStudio<V extends StudioVariant>(\n config: StudioConfig<V>,\n search: string,\n): StudioState<V> {\n const params = new URLSearchParams(search);\n const unknown: string[] = [];\n const firstArea = first(config.areas);\n\n if (!firstArea) {\n throw new Error(\"three-studio: the config has no areas\");\n }\n\n const bare = [...params.keys()].find((key) => params.get(key) === \"\" && key in config.areas);\n const askedArea = params.get(\"area\") ?? bare ?? null;\n let [areaName, area] = firstArea;\n\n if (askedArea !== null) {\n const found = config.areas[askedArea];\n\n if (found) {\n [areaName, area] = [askedArea, found];\n } else {\n unknown.push(`area=${askedArea}`);\n }\n }\n\n const firstVariant = first(area.variants);\n\n if (!firstVariant) {\n throw new Error(`three-studio: area \"${areaName}\" has no variants`);\n }\n\n const askedVariant = params.get(\"v\");\n let [variantName, variant] = firstVariant;\n\n if (askedVariant !== null) {\n const found = area.variants[askedVariant];\n\n if (found) {\n [variantName, variant] = [askedVariant, found];\n } else {\n unknown.push(`v=${askedVariant}`);\n }\n }\n\n const views: StudioViews = { ...config.views, ...area.views, ...variant.views };\n const defaultView = config.defaultView ?? \"play\";\n const askedView = params.get(\"view\");\n const fallbackName = defaultView in views ? defaultView : (first(views)?.[0] ?? defaultView);\n let viewName = fallbackName;\n\n if (askedView !== null) {\n if (askedView in views) {\n viewName = askedView;\n } else {\n unknown.push(`view=${askedView}`);\n }\n }\n\n const shotsParam = params.get(\"shots\");\n const shots = shotsParam === null || shotsParam === \"\" ? null : Number(shotsParam);\n const shotPort = shots !== null && Number.isInteger(shots) && shots > 0 && shots < 65536;\n\n if (shots !== null && !shotPort) {\n unknown.push(`shots=${shotsParam}`);\n }\n\n const time = params.get(\"t\");\n const seconds = time === null || time === \"\" ? null : Number(time);\n\n if (seconds !== null && !Number.isFinite(seconds)) {\n unknown.push(`t=${time}`);\n }\n\n return {\n config,\n areaName,\n area,\n variantName,\n variant,\n viewName,\n view: views[viewName] ?? FALLBACK_VIEW,\n views,\n clean: flag(params.get(\"clean\")),\n wire: flag(params.get(\"wire\")),\n time: seconds !== null && Number.isFinite(seconds) ? seconds : null,\n shots: shotPort ? shots : null,\n tag: `${areaName}-${variantName}`,\n unknown,\n };\n}\n\nconst flag = (value: string | null) => value !== null && value !== \"0\" && value !== \"false\";\n\nexport type StudioLink = {\n area?: string;\n variant?: string;\n view?: string;\n clean?: boolean;\n wire?: boolean;\n time?: number | null;\n};\n\n/**\n * The query for the state with `change` applied, `?` included. A new area starts at its first\n * variant; the view, the `wire` / `t` switches and `shots` carry over, `clean` only when asked for.\n */\nexport function studioHref(state: StudioState, change: StudioLink = {}): string {\n const area = change.area ?? state.areaName;\n const sameArea = area === state.areaName;\n const variant = change.variant ?? (sameArea ? state.variantName : undefined);\n const params = new URLSearchParams({ area });\n\n if (variant !== undefined) {\n params.set(\"v\", variant);\n }\n\n params.set(\"view\", change.view ?? state.viewName);\n\n if (change.wire ?? state.wire) {\n params.set(\"wire\", \"1\");\n }\n\n if (change.clean) {\n params.set(\"clean\", \"1\");\n }\n\n const time = change.time === undefined ? state.time : change.time;\n\n if (time !== null) {\n params.set(\"t\", String(time));\n }\n\n if (state.shots !== null) {\n params.set(\"shots\", String(state.shots));\n }\n\n return `?${params}`;\n}\n\n/** The animation time to draw at: the frozen `?t=` when set, the clock's otherwise. */\nexport const studioTime = (state: Pick<StudioState, \"time\">, elapsed: number): number =>\n state.time ?? elapsed;\n\n/** An area's or variant's chip text. */\nexport const titleOf = (\n name: string,\n entry: { title?: string | undefined; label?: string | undefined },\n) => entry.title ?? entry.label ?? name;\n","import type { Camera, Vector3 } from \"three\";\n\nimport type { StudioView, StudioViews, Vec3 } from \"./state.ts\";\n\n/** What `applyView` moves alongside the camera: OrbitControls and the like. */\nexport type ViewControls = { target: Vector3; update: () => unknown };\n\n/**\n * Puts the camera at the view and aims it, lens included, and moves the controls' target with it\n * so an orbit starts from there.\n */\nexport function applyView(camera: Camera, view: StudioView, controls?: ViewControls | null) {\n camera.position.set(...view.position);\n\n const lens = camera as Camera & {\n isPerspectiveCamera?: boolean;\n fov?: number;\n near?: number;\n far?: number;\n zoom?: number;\n updateProjectionMatrix?: () => void;\n };\n\n if (lens.isPerspectiveCamera && view.fov !== undefined) {\n lens.fov = view.fov;\n }\n\n if (view.near !== undefined && lens.near !== undefined) {\n lens.near = view.near;\n }\n\n if (view.far !== undefined && lens.far !== undefined) {\n lens.far = view.far;\n }\n\n if (view.zoom !== undefined && lens.zoom !== undefined) {\n lens.zoom = view.zoom;\n }\n\n lens.updateProjectionMatrix?.();\n camera.lookAt(...view.target);\n camera.updateMatrixWorld();\n\n if (controls) {\n controls.target.set(...view.target);\n controls.update();\n }\n}\n\nexport type OrbitViewsOptions = {\n /**\n * The game camera: its offset from what it looks at, and its lens. Gives the `play` view, and\n * the bearing `close` looks along. Copy it from the game's camera code, never by eye.\n */\n play?: { offset: Vec3; fov: number } | undefined;\n /** The lens of every view but `play`. Default 40. */\n fov?: number | undefined;\n /**\n * Where `front` looks from: the direction from the object to a camera facing its front. Default\n * `[1, 0.25, 0]`, for kits whose front is +x; `[0, 0.25, -1]` for one facing -z, like a vehicle\n * driving down -z. `side` is a quarter turn round from it and `back` opposite, at its height.\n */\n front?: Vec3 | undefined;\n};\n\nconst along = (centre: Vec3, direction: Vec3, distance: number, fov: number): StudioView => {\n const length = Math.hypot(...direction) || 1;\n\n return {\n position: [\n centre[0] + (direction[0] / length) * distance,\n centre[1] + (direction[1] / length) * distance,\n centre[2] + (direction[2] / length) * distance,\n ],\n target: centre,\n fov,\n };\n};\n\n/**\n * The views every studio wants around one object at `centre`: `play` (the game camera's exact\n * bearing, distance and lens, when given), `close` (that bearing, `near` away), and `front`,\n * `side`, `back` and `top` at `near`. Front looks down -x by default (`options.front` turns it).\n */\nexport function orbitViews(\n centre: Vec3,\n near: number,\n options: OrbitViewsOptions = {},\n): StudioViews {\n const fov = options.fov ?? 40;\n const play = options.play;\n const bearing: Vec3 = play ? play.offset : [1, 0.6, 1.2];\n const views: Record<string, StudioView> = {};\n\n if (play) {\n const [x, y, z] = play.offset;\n\n views.play = {\n position: [centre[0] + x, centre[1] + y, centre[2] + z],\n target: centre,\n fov: play.fov,\n };\n }\n\n views.close = along(centre, bearing, near, fov);\n const [fx, fy, fz] = options.front ?? [1, 0.25, 0];\n\n views.front = along(centre, [fx, fy, fz], near, fov);\n views.side = along(centre, [-fz, fy, fx], near, fov);\n views.back = along(centre, [-fx, fy, -fz], near, fov);\n // Not straight down: lookAt from directly above spins on its up vector.\n views.top = along(centre, [0.001, 1, 0.001], near, fov);\n\n return views;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zkmake/three-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A dev page for three.js scenes: areas, variants and camera views from the URL, an orbit rig, a draw-call readout, audit helpers and screenshots an agent can take, plus a CLI to save and diff them. Vanilla three and React Three Fiber.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devtools",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"directory": "packages/three-studio"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
|
-
"three-studio": "
|
|
26
|
+
"three-studio": "dist/cli.js"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typecheck": "tsc"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@zkmake/three-audit": "^0.
|
|
54
|
+
"@zkmake/three-audit": "^0.4.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@arethetypeswrong/core": "catalog:",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"views-suF0RWQc.js","names":[],"sources":["../src/core/frame.ts","../src/core/state.ts","../src/core/views.ts"],"sourcesContent":["import type { Material, Object3D } from \"three\";\n\n/** What the readout needs of a renderer: `WebGLRenderer` and `WebGPURenderer` both fit. */\nexport type StatsRenderer = { info: { render: { calls: number; triangles: number } } };\n\nexport type FrameStats = { calls: number; triangles: number };\n\n/**\n * The draws and triangles of the renderer's last `render()` call, shadow passes included. A\n * composer that renders several passes reports its last one only; three-audit's `ledger()` counts\n * every pass.\n */\nexport const frameStats = (renderer: StatsRenderer): FrameStats => ({\n calls: renderer.info.render.calls,\n triangles: renderer.info.render.triangles,\n});\n\n/** `\"12 calls · 34,567 tris\"`, the line every studio page shows. */\nexport const formatStats = ({ calls, triangles }: FrameStats): string =>\n `${calls} calls · ${triangles.toLocaleString(\"en-US\")} tris`;\n\nconst materialsOf = (object: Object3D): Material[] => {\n const material = (object as Object3D & { material?: Material | Material[] }).material;\n\n return Array.isArray(material) ? material : material ? [material] : [];\n};\n\n/**\n * Draws every material under `root` as wireframe, or puts back what each had. Cheap enough to\n * call each frame, so meshes that mount later are covered too.\n */\nexport function setWireframe(root: Object3D, on: boolean) {\n root.traverse((object) => {\n for (const material of materialsOf(object)) {\n const wire = material as Material & { wireframe?: boolean };\n\n if (!(\"wireframe\" in wire)) {\n continue;\n }\n\n const saved = material.userData.studioWireframe as boolean | undefined;\n\n if (on && saved === undefined) {\n material.userData.studioWireframe = Boolean(wire.wireframe);\n wire.wireframe = true;\n } else if (!on && saved !== undefined) {\n wire.wireframe = saved;\n delete material.userData.studioWireframe;\n }\n }\n });\n}\n","/**\n * The page's state, read from its URL: which area, which variant of it, which camera view, and\n * the shot switches (`clean`, `wire`, `t`). Pure: the same config and query give the same state,\n * so a link is a reproducible shot.\n *\n * ?area=<area>&v=<variant>&view=<view>&clean=1&wire=1&t=<seconds>\n *\n * A bare `?<area>` (`?plate`) also picks an area, the way single-object model studios link.\n */\n\nexport type Vec3 = readonly [number, number, number];\n\n/** A camera pose. `fov`, `near` and `far` apply to a perspective camera, `zoom` to any. */\nexport type StudioView = {\n position: Vec3;\n target: Vec3;\n fov?: number | undefined;\n near?: number | undefined;\n far?: number | undefined;\n zoom?: number | undefined;\n};\n\nexport type StudioViews = Readonly<Record<string, StudioView>>;\n\n/** One look for one area. Adapters add what they render (`render`, `hud`); views it brings win. */\nexport type StudioVariant = {\n /** Its chip's text; the key otherwise. */\n label?: string | undefined;\n /** Views of its own (a camera variant's poses), over the area's and the page's. */\n views?: StudioViews | undefined;\n};\n\nexport type StudioArea<V extends StudioVariant = StudioVariant> = {\n /** Its chip's text; the key otherwise. */\n title?: string | undefined;\n variants: Readonly<Record<string, V>>;\n /** Views for this area, over the page's. */\n views?: StudioViews | undefined;\n};\n\nexport type StudioConfig<V extends StudioVariant = StudioVariant> = {\n /** In chip order; the first is the default. */\n areas: Readonly<Record<string, StudioArea<V>>>;\n /** Views every area has (`play`, copied from the game camera, belongs here). */\n views?: StudioViews | undefined;\n /** The view a link without `view=` opens, and the fallback for an unknown one. Default `play`. */\n defaultView?: string | undefined;\n};\n\nexport type StudioState<V extends StudioVariant = StudioVariant> = {\n config: StudioConfig<V>;\n areaName: string;\n area: StudioArea<V>;\n variantName: string;\n variant: V;\n viewName: string;\n view: StudioView;\n /** Every view this variant can take: the page's, the area's, then its own. */\n views: StudioViews;\n /** `?clean=1`: no overlay, for shots taken of the whole window. */\n clean: boolean;\n /** `?wire=1`: every material drawn as wireframe, to read the topology. */\n wire: boolean;\n /** `?t=<s>`: animations frozen at this time, for steady shots; null when running. */\n time: number | null;\n /**\n * `<area>-<variant>`, for `userData.studioObject` on the variant's group, so three-audit's\n * `zfight()` and `summary()` audit only the thing under study.\n */\n tag: string;\n /** Names asked for in the URL that the config doesn't have; the defaults were used instead. */\n unknown: string[];\n};\n\nconst FALLBACK_VIEW: StudioView = { position: [4, 3, 6], target: [0, 0.5, 0], fov: 40 };\n\nconst first = <T>(record: Readonly<Record<string, T>>): [string, T] | undefined =>\n Object.entries(record)[0];\n\n/** Reads a `location.search` (or any query string) against the config. */\nexport function resolveStudio<V extends StudioVariant>(\n config: StudioConfig<V>,\n search: string,\n): StudioState<V> {\n const params = new URLSearchParams(search);\n const unknown: string[] = [];\n const firstArea = first(config.areas);\n\n if (!firstArea) {\n throw new Error(\"three-studio: the config has no areas\");\n }\n\n const bare = [...params.keys()].find((key) => params.get(key) === \"\" && key in config.areas);\n const askedArea = params.get(\"area\") ?? bare ?? null;\n let [areaName, area] = firstArea;\n\n if (askedArea !== null) {\n const found = config.areas[askedArea];\n\n if (found) {\n [areaName, area] = [askedArea, found];\n } else {\n unknown.push(`area=${askedArea}`);\n }\n }\n\n const firstVariant = first(area.variants);\n\n if (!firstVariant) {\n throw new Error(`three-studio: area \"${areaName}\" has no variants`);\n }\n\n const askedVariant = params.get(\"v\");\n let [variantName, variant] = firstVariant;\n\n if (askedVariant !== null) {\n const found = area.variants[askedVariant];\n\n if (found) {\n [variantName, variant] = [askedVariant, found];\n } else {\n unknown.push(`v=${askedVariant}`);\n }\n }\n\n const views: StudioViews = { ...config.views, ...area.views, ...variant.views };\n const defaultView = config.defaultView ?? \"play\";\n const askedView = params.get(\"view\");\n const fallbackName = defaultView in views ? defaultView : (first(views)?.[0] ?? defaultView);\n let viewName = fallbackName;\n\n if (askedView !== null) {\n if (askedView in views) {\n viewName = askedView;\n } else {\n unknown.push(`view=${askedView}`);\n }\n }\n\n const time = params.get(\"t\");\n const seconds = time === null || time === \"\" ? null : Number(time);\n\n if (seconds !== null && !Number.isFinite(seconds)) {\n unknown.push(`t=${time}`);\n }\n\n return {\n config,\n areaName,\n area,\n variantName,\n variant,\n viewName,\n view: views[viewName] ?? FALLBACK_VIEW,\n views,\n clean: flag(params.get(\"clean\")),\n wire: flag(params.get(\"wire\")),\n time: seconds !== null && Number.isFinite(seconds) ? seconds : null,\n tag: `${areaName}-${variantName}`,\n unknown,\n };\n}\n\nconst flag = (value: string | null) => value !== null && value !== \"0\" && value !== \"false\";\n\nexport type StudioLink = {\n area?: string;\n variant?: string;\n view?: string;\n clean?: boolean;\n wire?: boolean;\n time?: number | null;\n};\n\n/**\n * The query for the state with `change` applied, `?` included. A new area starts at its first\n * variant; the view and the `wire` / `t` switches carry over, `clean` only when asked for.\n */\nexport function studioHref(state: StudioState, change: StudioLink = {}): string {\n const area = change.area ?? state.areaName;\n const sameArea = area === state.areaName;\n const variant = change.variant ?? (sameArea ? state.variantName : undefined);\n const params = new URLSearchParams({ area });\n\n if (variant !== undefined) {\n params.set(\"v\", variant);\n }\n\n params.set(\"view\", change.view ?? state.viewName);\n\n if (change.wire ?? state.wire) {\n params.set(\"wire\", \"1\");\n }\n\n if (change.clean) {\n params.set(\"clean\", \"1\");\n }\n\n const time = change.time === undefined ? state.time : change.time;\n\n if (time !== null) {\n params.set(\"t\", String(time));\n }\n\n return `?${params}`;\n}\n\n/** The animation time to draw at: the frozen `?t=` when set, the clock's otherwise. */\nexport const studioTime = (state: Pick<StudioState, \"time\">, elapsed: number): number =>\n state.time ?? elapsed;\n\n/** An area's or variant's chip text. */\nexport const titleOf = (\n name: string,\n entry: { title?: string | undefined; label?: string | undefined },\n) => entry.title ?? entry.label ?? name;\n","import type { Camera, Vector3 } from \"three\";\n\nimport type { StudioView, StudioViews, Vec3 } from \"./state.ts\";\n\n/** What `applyView` moves alongside the camera: OrbitControls and the like. */\nexport type ViewControls = { target: Vector3; update: () => unknown };\n\n/**\n * Puts the camera at the view and aims it, lens included, and moves the controls' target with it\n * so an orbit starts from there.\n */\nexport function applyView(camera: Camera, view: StudioView, controls?: ViewControls | null) {\n camera.position.set(...view.position);\n\n const lens = camera as Camera & {\n isPerspectiveCamera?: boolean;\n fov?: number;\n near?: number;\n far?: number;\n zoom?: number;\n updateProjectionMatrix?: () => void;\n };\n\n if (lens.isPerspectiveCamera && view.fov !== undefined) {\n lens.fov = view.fov;\n }\n\n if (view.near !== undefined && lens.near !== undefined) {\n lens.near = view.near;\n }\n\n if (view.far !== undefined && lens.far !== undefined) {\n lens.far = view.far;\n }\n\n if (view.zoom !== undefined && lens.zoom !== undefined) {\n lens.zoom = view.zoom;\n }\n\n lens.updateProjectionMatrix?.();\n camera.lookAt(...view.target);\n camera.updateMatrixWorld();\n\n if (controls) {\n controls.target.set(...view.target);\n controls.update();\n }\n}\n\nexport type OrbitViewsOptions = {\n /**\n * The game camera: its offset from what it looks at, and its lens. Gives the `play` view, and\n * the bearing `close` looks along. Copy it from the game's camera code, never by eye.\n */\n play?: { offset: Vec3; fov: number } | undefined;\n /** The lens of every view but `play`. Default 40. */\n fov?: number | undefined;\n};\n\nconst along = (centre: Vec3, direction: Vec3, distance: number, fov: number): StudioView => {\n const length = Math.hypot(...direction) || 1;\n\n return {\n position: [\n centre[0] + (direction[0] / length) * distance,\n centre[1] + (direction[1] / length) * distance,\n centre[2] + (direction[2] / length) * distance,\n ],\n target: centre,\n fov,\n };\n};\n\n/**\n * The views every studio wants around one object at `centre`: `play` (the game camera's exact\n * bearing, distance and lens, when given), `close` (that bearing, `near` away), and `front`,\n * `side`, `back` and `top` at `near`. Front looks down -x like most kits' fronts; rename to taste.\n */\nexport function orbitViews(\n centre: Vec3,\n near: number,\n options: OrbitViewsOptions = {},\n): StudioViews {\n const fov = options.fov ?? 40;\n const play = options.play;\n const bearing: Vec3 = play ? play.offset : [1, 0.6, 1.2];\n const views: Record<string, StudioView> = {};\n\n if (play) {\n const [x, y, z] = play.offset;\n\n views.play = {\n position: [centre[0] + x, centre[1] + y, centre[2] + z],\n target: centre,\n fov: play.fov,\n };\n }\n\n views.close = along(centre, bearing, near, fov);\n views.front = along(centre, [1, 0.25, 0], near, fov);\n views.side = along(centre, [0, 0.25, 1], near, fov);\n views.back = along(centre, [-1, 0.25, 0], near, fov);\n // Not straight down: lookAt from directly above spins on its up vector.\n views.top = along(centre, [0.001, 1, 0.001], near, fov);\n\n return views;\n}\n"],"mappings":"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"}
|