@scenography/plugin-floor 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/dist/data-BgdlM_kL.cjs +38 -0
- package/dist/data-BgdlM_kL.cjs.map +1 -0
- package/dist/data-CTGlghAE.mjs +27 -0
- package/dist/data-CTGlghAE.mjs.map +1 -0
- package/dist/data.cjs +4 -0
- package/dist/data.d.cts +2 -0
- package/dist/data.d.mts +2 -0
- package/dist/data.mjs +2 -0
- package/dist/index-1ydrP4eD.d.cts +52 -0
- package/dist/index-1ydrP4eD.d.mts +52 -0
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.mjs +138 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
let _scenography_scene = require("@scenography/scene");
|
|
2
|
+
//#region src/data/validate.ts
|
|
3
|
+
const validateFloor = (floor) => (0, _scenography_scene.validate)([
|
|
4
|
+
(0, _scenography_scene.existsProp)("size", "Floor is missing a size."),
|
|
5
|
+
(0, _scenography_scene.propIs)("size", _scenography_scene.isVec2, "size must be [width, depth]."),
|
|
6
|
+
(0, _scenography_scene.propIs)("center", _scenography_scene.isVec2, "center must be [x, z]."),
|
|
7
|
+
(0, _scenography_scene.propIs)("y", _scenography_scene.isFiniteNumber, "y must be a finite number."),
|
|
8
|
+
(0, _scenography_scene.propIs)("heightScale", _scenography_scene.isFiniteNumber, "heightScale must be a finite number."),
|
|
9
|
+
(0, _scenography_scene.propIs)("segments", (v) => (0, _scenography_scene.isIntegerNumber)(v) && v >= 1, "segments must be a positive integer."),
|
|
10
|
+
(0, _scenography_scene.nested)("texture", _scenography_scene.validateTexture)
|
|
11
|
+
])(floor);
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/data/plugin.ts
|
|
14
|
+
/**
|
|
15
|
+
* The data half of the floor plugin: owns the `floor` scene key (a singleton)
|
|
16
|
+
* and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with
|
|
17
|
+
* a floor validate without loading the runtime.
|
|
18
|
+
*/
|
|
19
|
+
const floorData = {
|
|
20
|
+
kind: "floor",
|
|
21
|
+
cardinality: "single",
|
|
22
|
+
validate: (floor) => validateFloor(floor)
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
Object.defineProperty(exports, "floorData", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function() {
|
|
28
|
+
return floorData;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(exports, "validateFloor", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get: function() {
|
|
34
|
+
return validateFloor;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=data-BgdlM_kL.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-BgdlM_kL.cjs","names":["isVec2","isFiniteNumber","validateTexture"],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n isFiniteNumber,\n isIntegerNumber,\n isVec2,\n nested,\n propIs,\n validate,\n validateTexture,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Floor } from \"./floor\";\n\nexport const validateFloor = (floor: Floor): ValidationResult =>\n validate<Floor>([\n existsProp(\"size\", \"Floor is missing a size.\"),\n propIs(\"size\", isVec2, \"size must be [width, depth].\"),\n propIs(\"center\", isVec2, \"center must be [x, z].\"),\n propIs(\"y\", isFiniteNumber, \"y must be a finite number.\"),\n propIs(\"heightScale\", isFiniteNumber, \"heightScale must be a finite number.\"),\n propIs(\n \"segments\",\n (v) => isIntegerNumber(v) && v >= 1,\n \"segments must be a positive integer.\",\n ),\n nested(\"texture\", validateTexture),\n ])(floor);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateFloor } from \"./validate\";\nimport type { Floor } from \"./floor\";\n\n/**\n * The data half of the floor plugin: owns the `floor` scene key (a singleton)\n * and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with\n * a floor validate without loading the runtime.\n */\nexport const floorData: DataPlugin<Floor> = {\n kind: \"floor\",\n cardinality: \"single\",\n validate: (floor) => validateFloor(floor),\n};\n"],"mappings":";;AAcA,MAAa,iBAAiB,WAAA,GAAA,mBAAA,SAAA,CACZ;oCACH,QAAQ,0BAA0B;gCACtC,QAAQA,mBAAAA,QAAQ,8BAA8B;gCAC9C,UAAUA,mBAAAA,QAAQ,wBAAwB;gCAC1C,KAAKC,mBAAAA,gBAAgB,4BAA4B;gCACjD,eAAeA,mBAAAA,gBAAgB,sCAAsC;gCAE1E,aACC,OAAA,GAAA,mBAAA,gBAAA,CAAsB,CAAC,KAAK,KAAK,GAClC,sCACF;gCACO,WAAWC,mBAAAA,eAAe;AACnC,CAAC,CAAC,CAAC,KAAK;;;;;;;;ACjBV,MAAa,YAA+B;CAC1C,MAAM;CACN,aAAa;CACb,WAAW,UAAU,cAAc,KAAK;AAC1C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsProp, isFiniteNumber, isIntegerNumber, isVec2, nested, propIs, validate, validateTexture } from "@scenography/scene";
|
|
2
|
+
//#region src/data/validate.ts
|
|
3
|
+
const validateFloor = (floor) => validate([
|
|
4
|
+
existsProp("size", "Floor is missing a size."),
|
|
5
|
+
propIs("size", isVec2, "size must be [width, depth]."),
|
|
6
|
+
propIs("center", isVec2, "center must be [x, z]."),
|
|
7
|
+
propIs("y", isFiniteNumber, "y must be a finite number."),
|
|
8
|
+
propIs("heightScale", isFiniteNumber, "heightScale must be a finite number."),
|
|
9
|
+
propIs("segments", (v) => isIntegerNumber(v) && v >= 1, "segments must be a positive integer."),
|
|
10
|
+
nested("texture", validateTexture)
|
|
11
|
+
])(floor);
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/data/plugin.ts
|
|
14
|
+
/**
|
|
15
|
+
* The data half of the floor plugin: owns the `floor` scene key (a singleton)
|
|
16
|
+
* and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with
|
|
17
|
+
* a floor validate without loading the runtime.
|
|
18
|
+
*/
|
|
19
|
+
const floorData = {
|
|
20
|
+
kind: "floor",
|
|
21
|
+
cardinality: "single",
|
|
22
|
+
validate: (floor) => validateFloor(floor)
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { validateFloor as n, floorData as t };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=data-CTGlghAE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-CTGlghAE.mjs","names":[],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n isFiniteNumber,\n isIntegerNumber,\n isVec2,\n nested,\n propIs,\n validate,\n validateTexture,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Floor } from \"./floor\";\n\nexport const validateFloor = (floor: Floor): ValidationResult =>\n validate<Floor>([\n existsProp(\"size\", \"Floor is missing a size.\"),\n propIs(\"size\", isVec2, \"size must be [width, depth].\"),\n propIs(\"center\", isVec2, \"center must be [x, z].\"),\n propIs(\"y\", isFiniteNumber, \"y must be a finite number.\"),\n propIs(\"heightScale\", isFiniteNumber, \"heightScale must be a finite number.\"),\n propIs(\n \"segments\",\n (v) => isIntegerNumber(v) && v >= 1,\n \"segments must be a positive integer.\",\n ),\n nested(\"texture\", validateTexture),\n ])(floor);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateFloor } from \"./validate\";\nimport type { Floor } from \"./floor\";\n\n/**\n * The data half of the floor plugin: owns the `floor` scene key (a singleton)\n * and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with\n * a floor validate without loading the runtime.\n */\nexport const floorData: DataPlugin<Floor> = {\n kind: \"floor\",\n cardinality: \"single\",\n validate: (floor) => validateFloor(floor),\n};\n"],"mappings":";;AAcA,MAAa,iBAAiB,UAC5B,SAAgB;CACd,WAAW,QAAQ,0BAA0B;CAC7C,OAAO,QAAQ,QAAQ,8BAA8B;CACrD,OAAO,UAAU,QAAQ,wBAAwB;CACjD,OAAO,KAAK,gBAAgB,4BAA4B;CACxD,OAAO,eAAe,gBAAgB,sCAAsC;CAC5E,OACE,aACC,MAAM,gBAAgB,CAAC,KAAK,KAAK,GAClC,sCACF;CACA,OAAO,WAAW,eAAe;AACnC,CAAC,CAAC,CAAC,KAAK;;;;;;;;ACjBV,MAAa,YAA+B;CAC1C,MAAM;CACN,aAAa;CACb,WAAW,UAAU,cAAc,KAAK;AAC1C"}
|
package/dist/data.cjs
ADDED
package/dist/data.d.cts
ADDED
package/dist/data.d.mts
ADDED
package/dist/data.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DataPlugin, SurfaceTexture, ValidationResult, Vec2 } from "@scenography/scene";
|
|
2
|
+
|
|
3
|
+
//#region src/data/floor.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The gallery floor: a rectangular ground plane, optionally displaced into
|
|
6
|
+
* relief by a heightmap. A minimal scene can omit it; when present the engine
|
|
7
|
+
* lays it at `y` and the visitor walks over it, following its height.
|
|
8
|
+
*/
|
|
9
|
+
interface Floor {
|
|
10
|
+
/** Extent on the ground plane, `[width (x), depth (z)]` in world units. */
|
|
11
|
+
size: Vec2;
|
|
12
|
+
/** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */
|
|
13
|
+
center?: Vec2;
|
|
14
|
+
/** Base vertical position (world Y). Defaults to `0`. */
|
|
15
|
+
y?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Grayscale heightmap URL that displaces the whole ground (distinct from the
|
|
18
|
+
* tiling `texture`): paint dunes, a shoreline berm, a gentle grade toward
|
|
19
|
+
* water. Black = `0`, white = `heightScale`, so height at `(x, z)` is
|
|
20
|
+
* `y + sample(x, z)·heightScale`. The visitor follows it and a waterline
|
|
21
|
+
* becomes an irregular contour instead of a straight cut.
|
|
22
|
+
*/
|
|
23
|
+
heightmap?: string;
|
|
24
|
+
/** Max displacement (world units) at white in the heightmap. Default `0`. */
|
|
25
|
+
heightScale?: number;
|
|
26
|
+
/** Grid resolution the ground is tessellated into for displacement. Default `200`. */
|
|
27
|
+
segments?: number;
|
|
28
|
+
/** CSS-style color. Tints the texture when both are set. Defaults to white. */
|
|
29
|
+
color?: string;
|
|
30
|
+
/** Image/PBR maps mapped onto the surface. */
|
|
31
|
+
texture?: SurfaceTexture;
|
|
32
|
+
}
|
|
33
|
+
declare module "@scenography/scene" {
|
|
34
|
+
interface Scene {
|
|
35
|
+
/** The gallery floor — provided by `@scenography/plugin-floor`. */
|
|
36
|
+
floor?: Floor;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/data/validate.d.ts
|
|
41
|
+
declare const validateFloor: (floor: Floor) => ValidationResult;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/data/plugin.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* The data half of the floor plugin: owns the `floor` scene key (a singleton)
|
|
46
|
+
* and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with
|
|
47
|
+
* a floor validate without loading the runtime.
|
|
48
|
+
*/
|
|
49
|
+
declare const floorData: DataPlugin<Floor>;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { validateFloor as n, Floor as r, floorData as t };
|
|
52
|
+
//# sourceMappingURL=index-1ydrP4eD.d.cts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DataPlugin, SurfaceTexture, ValidationResult, Vec2 } from "@scenography/scene";
|
|
2
|
+
|
|
3
|
+
//#region src/data/floor.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The gallery floor: a rectangular ground plane, optionally displaced into
|
|
6
|
+
* relief by a heightmap. A minimal scene can omit it; when present the engine
|
|
7
|
+
* lays it at `y` and the visitor walks over it, following its height.
|
|
8
|
+
*/
|
|
9
|
+
interface Floor {
|
|
10
|
+
/** Extent on the ground plane, `[width (x), depth (z)]` in world units. */
|
|
11
|
+
size: Vec2;
|
|
12
|
+
/** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */
|
|
13
|
+
center?: Vec2;
|
|
14
|
+
/** Base vertical position (world Y). Defaults to `0`. */
|
|
15
|
+
y?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Grayscale heightmap URL that displaces the whole ground (distinct from the
|
|
18
|
+
* tiling `texture`): paint dunes, a shoreline berm, a gentle grade toward
|
|
19
|
+
* water. Black = `0`, white = `heightScale`, so height at `(x, z)` is
|
|
20
|
+
* `y + sample(x, z)·heightScale`. The visitor follows it and a waterline
|
|
21
|
+
* becomes an irregular contour instead of a straight cut.
|
|
22
|
+
*/
|
|
23
|
+
heightmap?: string;
|
|
24
|
+
/** Max displacement (world units) at white in the heightmap. Default `0`. */
|
|
25
|
+
heightScale?: number;
|
|
26
|
+
/** Grid resolution the ground is tessellated into for displacement. Default `200`. */
|
|
27
|
+
segments?: number;
|
|
28
|
+
/** CSS-style color. Tints the texture when both are set. Defaults to white. */
|
|
29
|
+
color?: string;
|
|
30
|
+
/** Image/PBR maps mapped onto the surface. */
|
|
31
|
+
texture?: SurfaceTexture;
|
|
32
|
+
}
|
|
33
|
+
declare module "@scenography/scene" {
|
|
34
|
+
interface Scene {
|
|
35
|
+
/** The gallery floor — provided by `@scenography/plugin-floor`. */
|
|
36
|
+
floor?: Floor;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/data/validate.d.ts
|
|
41
|
+
declare const validateFloor: (floor: Floor) => ValidationResult;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/data/plugin.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* The data half of the floor plugin: owns the `floor` scene key (a singleton)
|
|
46
|
+
* and validates it. Pure — no three.js. Pass it to `defineScene` so scenes with
|
|
47
|
+
* a floor validate without loading the runtime.
|
|
48
|
+
*/
|
|
49
|
+
declare const floorData: DataPlugin<Floor>;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { validateFloor as n, Floor as r, floorData as t };
|
|
52
|
+
//# sourceMappingURL=index-1ydrP4eD.d.mts.map
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_data = require("./data-BgdlM_kL.cjs");
|
|
3
|
+
let _scenography_engine = require("@scenography/engine");
|
|
4
|
+
let three = require("three");
|
|
5
|
+
//#region src/buildFloor.ts
|
|
6
|
+
/** Bilinear sample of a height field at UV `[0,1]`; `0` until the image loads. */
|
|
7
|
+
function sampleField(field, u, v) {
|
|
8
|
+
const { data, w, h } = field;
|
|
9
|
+
if (!data) return 0;
|
|
10
|
+
const x = Math.min(Math.max(u, 0), 1) * (w - 1);
|
|
11
|
+
const y = Math.min(Math.max(v, 0), 1) * (h - 1);
|
|
12
|
+
const x0 = Math.floor(x);
|
|
13
|
+
const y0 = Math.floor(y);
|
|
14
|
+
const x1 = Math.min(x0 + 1, w - 1);
|
|
15
|
+
const y1 = Math.min(y0 + 1, h - 1);
|
|
16
|
+
const fx = x - x0;
|
|
17
|
+
const fy = y - y0;
|
|
18
|
+
const a = data[y0 * w + x0] ?? 0;
|
|
19
|
+
const b = data[y0 * w + x1] ?? 0;
|
|
20
|
+
const c = data[y1 * w + x0] ?? 0;
|
|
21
|
+
const d = data[y1 * w + x1] ?? 0;
|
|
22
|
+
return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy;
|
|
23
|
+
}
|
|
24
|
+
/** Map world `(x, z)` to heightmap UV for a floor (matches the plane orientation). */
|
|
25
|
+
const floorUV = (floor, x, z) => {
|
|
26
|
+
const [w, d] = floor.size;
|
|
27
|
+
const [cx, cz] = floor.center ?? [0, 0];
|
|
28
|
+
return [(x - cx + w / 2) / w, (cz - z + d / 2) / d];
|
|
29
|
+
};
|
|
30
|
+
/** Ground height at `(x, z)`: base `y` + heightmap displacement. */
|
|
31
|
+
function heightAtWith(floor, field) {
|
|
32
|
+
const base = floor.y ?? 0;
|
|
33
|
+
const scale = floor.heightScale ?? 0;
|
|
34
|
+
if (scale === 0) return () => base;
|
|
35
|
+
return (x, z) => {
|
|
36
|
+
const [u, v] = floorUV(floor, x, z);
|
|
37
|
+
return base + sampleField(field, u, v) * scale;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,
|
|
42
|
+
* and (when a `heightmap` is given) displaced into dunes/berms by sampling that
|
|
43
|
+
* grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a
|
|
44
|
+
* `heightAt(x, z)` the camera controls use to follow the terrain.
|
|
45
|
+
*/
|
|
46
|
+
function buildFloor(floor) {
|
|
47
|
+
const [width, depth] = floor.size;
|
|
48
|
+
const [cx, cz] = floor.center ?? [0, 0];
|
|
49
|
+
const segments = floor.heightmap ? floor.segments ?? 200 : 1;
|
|
50
|
+
const material = new three.MeshStandardMaterial({ color: floor.color ?? "#ffffff" });
|
|
51
|
+
const needsUv1 = floor.texture ? (0, _scenography_engine.applyTexture)(material, floor.texture) : false;
|
|
52
|
+
const geometry = new three.PlaneGeometry(width, depth, segments, segments);
|
|
53
|
+
if (needsUv1) {
|
|
54
|
+
const uv = geometry.getAttribute("uv");
|
|
55
|
+
if (uv) geometry.setAttribute("uv1", uv);
|
|
56
|
+
}
|
|
57
|
+
const field = {
|
|
58
|
+
data: null,
|
|
59
|
+
w: 0,
|
|
60
|
+
h: 0
|
|
61
|
+
};
|
|
62
|
+
const scale = floor.heightScale ?? 0;
|
|
63
|
+
const pos = geometry.getAttribute("position");
|
|
64
|
+
const applyHeights = () => {
|
|
65
|
+
if (!pos || !field.data || scale === 0) return;
|
|
66
|
+
for (let i = 0; i < pos.count; i++) {
|
|
67
|
+
const px = pos.getX(i);
|
|
68
|
+
const py = pos.getY(i);
|
|
69
|
+
const u = (px + width / 2) / width;
|
|
70
|
+
const v = (py + depth / 2) / depth;
|
|
71
|
+
pos.setZ(i, sampleField(field, u, v) * scale);
|
|
72
|
+
}
|
|
73
|
+
pos.needsUpdate = true;
|
|
74
|
+
geometry.computeVertexNormals();
|
|
75
|
+
};
|
|
76
|
+
const mesh = new three.Mesh(geometry, material);
|
|
77
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
78
|
+
mesh.position.set(cx, floor.y ?? 0, cz);
|
|
79
|
+
mesh.receiveShadow = true;
|
|
80
|
+
mesh.name = "floor";
|
|
81
|
+
let ready = Promise.resolve();
|
|
82
|
+
const src = floor.heightmap;
|
|
83
|
+
if (src && scale !== 0) ready = new Promise((resolve) => {
|
|
84
|
+
const img = new Image();
|
|
85
|
+
img.crossOrigin = "anonymous";
|
|
86
|
+
img.onload = () => {
|
|
87
|
+
const canvas = document.createElement("canvas");
|
|
88
|
+
canvas.width = img.naturalWidth;
|
|
89
|
+
canvas.height = img.naturalHeight;
|
|
90
|
+
const ctx = canvas.getContext("2d");
|
|
91
|
+
if (ctx) {
|
|
92
|
+
ctx.drawImage(img, 0, 0);
|
|
93
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
94
|
+
const heights = new Float32Array(canvas.width * canvas.height);
|
|
95
|
+
for (let i = 0; i < heights.length; i++) heights[i] = (data[i * 4] ?? 0) / 255;
|
|
96
|
+
field.data = heights;
|
|
97
|
+
field.w = canvas.width;
|
|
98
|
+
field.h = canvas.height;
|
|
99
|
+
applyHeights();
|
|
100
|
+
}
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
img.onerror = () => resolve();
|
|
104
|
+
img.src = src;
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
object: mesh,
|
|
108
|
+
heightAt: heightAtWith(floor, field),
|
|
109
|
+
ready
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/plugin.ts
|
|
114
|
+
/**
|
|
115
|
+
* The floor plugin: lays the ground plane (displaced by a heightmap when given)
|
|
116
|
+
* and publishes the `terrain` capability — the live height function the camera
|
|
117
|
+
* follows and the definition the water shader samples. Rebuilds revoke and
|
|
118
|
+
* re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.
|
|
119
|
+
*/
|
|
120
|
+
const floor = () => ({
|
|
121
|
+
data: require_data.floorData,
|
|
122
|
+
build: (definition, ctx) => {
|
|
123
|
+
const built = buildFloor(definition);
|
|
124
|
+
const terrain = {
|
|
125
|
+
definition,
|
|
126
|
+
heightAt: built.heightAt,
|
|
127
|
+
ready: built.ready
|
|
128
|
+
};
|
|
129
|
+
ctx.capabilities.provide(_scenography_engine.TERRAIN, terrain);
|
|
130
|
+
return {
|
|
131
|
+
object: built.object,
|
|
132
|
+
dispose: () => ctx.capabilities.revoke(_scenography_engine.TERRAIN)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
//#endregion
|
|
137
|
+
exports.buildFloor = buildFloor;
|
|
138
|
+
exports.floor = floor;
|
|
139
|
+
exports.floorData = require_data.floorData;
|
|
140
|
+
exports.validateFloor = require_data.validateFloor;
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["MeshStandardMaterial","PlaneGeometry","Mesh","floorData","TERRAIN"],"sources":["../src/buildFloor.ts","../src/plugin.ts"],"sourcesContent":["import { applyTexture } from \"@scenography/engine\";\nimport { Mesh, MeshStandardMaterial, PlaneGeometry, type BufferAttribute, type Object3D } from \"three\";\n\nimport type { Floor } from \"./data\";\n\n/** A sampled grayscale heightmap, in `[0,1]`, populated once the image loads. */\ninterface HeightField {\n data: Float32Array | null;\n w: number;\n h: number;\n}\n\n/** Bilinear sample of a height field at UV `[0,1]`; `0` until the image loads. */\nfunction sampleField(field: HeightField, u: number, v: number): number {\n const { data, w, h } = field;\n if (!data) return 0;\n const x = Math.min(Math.max(u, 0), 1) * (w - 1);\n const y = Math.min(Math.max(v, 0), 1) * (h - 1);\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const x1 = Math.min(x0 + 1, w - 1);\n const y1 = Math.min(y0 + 1, h - 1);\n const fx = x - x0;\n const fy = y - y0;\n const a = data[y0 * w + x0] ?? 0;\n const b = data[y0 * w + x1] ?? 0;\n const c = data[y1 * w + x0] ?? 0;\n const d = data[y1 * w + x1] ?? 0;\n return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy;\n}\n\n/** A built floor plus a function giving ground height at any world `(x, z)`. */\nexport interface BuiltFloor {\n object: Object3D;\n heightAt: (x: number, z: number) => number;\n /**\n * Resolves once `heightAt` reflects the heightmap (immediately when there is\n * none). Until then `heightAt` returns the flat base `y`.\n */\n ready: Promise<void>;\n}\n\n/** Map world `(x, z)` to heightmap UV for a floor (matches the plane orientation). */\nconst floorUV = (floor: Floor, x: number, z: number): [number, number] => {\n const [w, d] = floor.size;\n const [cx, cz] = floor.center ?? [0, 0];\n const u = (x - cx + w / 2) / w;\n const v = (cz - z + d / 2) / d;\n return [u, v];\n};\n\n/** Ground height at `(x, z)`: base `y` + heightmap displacement. */\nfunction heightAtWith(floor: Floor, field: HeightField): (x: number, z: number) => number {\n const base = floor.y ?? 0;\n const scale = floor.heightScale ?? 0;\n if (scale === 0) return () => base;\n return (x, z) => {\n const [u, v] = floorUV(floor, x, z);\n return base + sampleField(field, u, v) * scale;\n };\n}\n\n/**\n * Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,\n * and (when a `heightmap` is given) displaced into dunes/berms by sampling that\n * grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a\n * `heightAt(x, z)` the camera controls use to follow the terrain.\n */\nexport function buildFloor(floor: Floor): BuiltFloor {\n const [width, depth] = floor.size;\n const [cx, cz] = floor.center ?? [0, 0];\n const segments = floor.heightmap ? floor.segments ?? 200 : 1;\n\n const material = new MeshStandardMaterial({ color: floor.color ?? \"#ffffff\" });\n const needsUv1 = floor.texture ? applyTexture(material, floor.texture) : false;\n\n const geometry = new PlaneGeometry(width, depth, segments, segments);\n if (needsUv1) {\n const uv = geometry.getAttribute(\"uv\");\n if (uv) geometry.setAttribute(\"uv1\", uv);\n }\n\n const field: HeightField = { data: null, w: 0, h: 0 };\n const scale = floor.heightScale ?? 0;\n const pos = geometry.getAttribute(\"position\") as BufferAttribute | undefined;\n\n // Displace the vertices once the heightmap loads. A vertex at local (px, py)\n // is raised along local Z = world +Y after the plane is laid flat.\n const applyHeights = (): void => {\n if (!pos || !field.data || scale === 0) return;\n for (let i = 0; i < pos.count; i++) {\n const px = pos.getX(i);\n const py = pos.getY(i);\n // Local (px, py) → world (cx+px, cz-py) → heightmap UV.\n const u = (px + width / 2) / width;\n const v = (py + depth / 2) / depth;\n pos.setZ(i, sampleField(field, u, v) * scale);\n }\n pos.needsUpdate = true;\n geometry.computeVertexNormals();\n };\n\n const mesh = new Mesh(geometry, material);\n // PlaneGeometry faces +Z; lay it flat so it faces up (+Y).\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.set(cx, floor.y ?? 0, cz);\n mesh.receiveShadow = true;\n mesh.name = \"floor\";\n\n // Load the heightmap, read its pixels, then displace the mesh and fill the\n // sampler so terrain-following (heightAt) matches the visible geometry.\n let ready: Promise<void> = Promise.resolve();\n const src = floor.heightmap;\n if (src && scale !== 0) {\n ready = new Promise((resolve) => {\n const img = new Image();\n img.crossOrigin = \"anonymous\";\n img.onload = (): void => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.drawImage(img, 0, 0);\n const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);\n const heights = new Float32Array(canvas.width * canvas.height);\n for (let i = 0; i < heights.length; i++) heights[i] = (data[i * 4] ?? 0) / 255;\n field.data = heights;\n field.w = canvas.width;\n field.h = canvas.height;\n applyHeights();\n }\n resolve();\n };\n img.onerror = (): void => resolve();\n img.src = src;\n });\n }\n\n return { object: mesh, heightAt: heightAtWith(floor, field), ready };\n}\n","import { TERRAIN, type EnginePlugin, type TerrainCapability } from \"@scenography/engine\";\n\nimport { buildFloor } from \"./buildFloor\";\nimport { floorData, type Floor } from \"./data\";\n\n/**\n * The floor plugin: lays the ground plane (displaced by a heightmap when given)\n * and publishes the `terrain` capability — the live height function the camera\n * follows and the definition the water shader samples. Rebuilds revoke and\n * re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.\n */\nexport const floor = (): EnginePlugin<Floor> => ({\n data: floorData,\n build: (definition, ctx) => {\n const built = buildFloor(definition);\n const terrain: TerrainCapability = {\n definition,\n heightAt: built.heightAt,\n ready: built.ready,\n };\n ctx.capabilities.provide(TERRAIN, terrain);\n return {\n object: built.object,\n dispose: () => ctx.capabilities.revoke(TERRAIN),\n };\n },\n});\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { n as validateFloor, r as Floor, t as floorData } from "./index-1ydrP4eD.cjs";
|
|
2
|
+
import { EnginePlugin } from "@scenography/engine";
|
|
3
|
+
import { Object3D } from "three";
|
|
4
|
+
|
|
5
|
+
//#region src/plugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The floor plugin: lays the ground plane (displaced by a heightmap when given)
|
|
8
|
+
* and publishes the `terrain` capability — the live height function the camera
|
|
9
|
+
* follows and the definition the water shader samples. Rebuilds revoke and
|
|
10
|
+
* re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.
|
|
11
|
+
*/
|
|
12
|
+
declare const floor: () => EnginePlugin<Floor>;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/buildFloor.d.ts
|
|
15
|
+
/** A built floor plus a function giving ground height at any world `(x, z)`. */
|
|
16
|
+
interface BuiltFloor {
|
|
17
|
+
object: Object3D;
|
|
18
|
+
heightAt: (x: number, z: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves once `heightAt` reflects the heightmap (immediately when there is
|
|
21
|
+
* none). Until then `heightAt` returns the flat base `y`.
|
|
22
|
+
*/
|
|
23
|
+
ready: Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,
|
|
27
|
+
* and (when a `heightmap` is given) displaced into dunes/berms by sampling that
|
|
28
|
+
* grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a
|
|
29
|
+
* `heightAt(x, z)` the camera controls use to follow the terrain.
|
|
30
|
+
*/
|
|
31
|
+
declare function buildFloor(floor: Floor): BuiltFloor;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type BuiltFloor, type Floor, buildFloor, floor, floorData, validateFloor };
|
|
34
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { n as validateFloor, r as Floor, t as floorData } from "./index-1ydrP4eD.mjs";
|
|
2
|
+
import { EnginePlugin } from "@scenography/engine";
|
|
3
|
+
import { Object3D } from "three";
|
|
4
|
+
|
|
5
|
+
//#region src/plugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The floor plugin: lays the ground plane (displaced by a heightmap when given)
|
|
8
|
+
* and publishes the `terrain` capability — the live height function the camera
|
|
9
|
+
* follows and the definition the water shader samples. Rebuilds revoke and
|
|
10
|
+
* re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.
|
|
11
|
+
*/
|
|
12
|
+
declare const floor: () => EnginePlugin<Floor>;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/buildFloor.d.ts
|
|
15
|
+
/** A built floor plus a function giving ground height at any world `(x, z)`. */
|
|
16
|
+
interface BuiltFloor {
|
|
17
|
+
object: Object3D;
|
|
18
|
+
heightAt: (x: number, z: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves once `heightAt` reflects the heightmap (immediately when there is
|
|
21
|
+
* none). Until then `heightAt` returns the flat base `y`.
|
|
22
|
+
*/
|
|
23
|
+
ready: Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,
|
|
27
|
+
* and (when a `heightmap` is given) displaced into dunes/berms by sampling that
|
|
28
|
+
* grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a
|
|
29
|
+
* `heightAt(x, z)` the camera controls use to follow the terrain.
|
|
30
|
+
*/
|
|
31
|
+
declare function buildFloor(floor: Floor): BuiltFloor;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type BuiltFloor, type Floor, buildFloor, floor, floorData, validateFloor };
|
|
34
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { n as validateFloor, t as floorData } from "./data-CTGlghAE.mjs";
|
|
2
|
+
import { TERRAIN, applyTexture } from "@scenography/engine";
|
|
3
|
+
import { Mesh, MeshStandardMaterial, PlaneGeometry } from "three";
|
|
4
|
+
//#region src/buildFloor.ts
|
|
5
|
+
/** Bilinear sample of a height field at UV `[0,1]`; `0` until the image loads. */
|
|
6
|
+
function sampleField(field, u, v) {
|
|
7
|
+
const { data, w, h } = field;
|
|
8
|
+
if (!data) return 0;
|
|
9
|
+
const x = Math.min(Math.max(u, 0), 1) * (w - 1);
|
|
10
|
+
const y = Math.min(Math.max(v, 0), 1) * (h - 1);
|
|
11
|
+
const x0 = Math.floor(x);
|
|
12
|
+
const y0 = Math.floor(y);
|
|
13
|
+
const x1 = Math.min(x0 + 1, w - 1);
|
|
14
|
+
const y1 = Math.min(y0 + 1, h - 1);
|
|
15
|
+
const fx = x - x0;
|
|
16
|
+
const fy = y - y0;
|
|
17
|
+
const a = data[y0 * w + x0] ?? 0;
|
|
18
|
+
const b = data[y0 * w + x1] ?? 0;
|
|
19
|
+
const c = data[y1 * w + x0] ?? 0;
|
|
20
|
+
const d = data[y1 * w + x1] ?? 0;
|
|
21
|
+
return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy;
|
|
22
|
+
}
|
|
23
|
+
/** Map world `(x, z)` to heightmap UV for a floor (matches the plane orientation). */
|
|
24
|
+
const floorUV = (floor, x, z) => {
|
|
25
|
+
const [w, d] = floor.size;
|
|
26
|
+
const [cx, cz] = floor.center ?? [0, 0];
|
|
27
|
+
return [(x - cx + w / 2) / w, (cz - z + d / 2) / d];
|
|
28
|
+
};
|
|
29
|
+
/** Ground height at `(x, z)`: base `y` + heightmap displacement. */
|
|
30
|
+
function heightAtWith(floor, field) {
|
|
31
|
+
const base = floor.y ?? 0;
|
|
32
|
+
const scale = floor.heightScale ?? 0;
|
|
33
|
+
if (scale === 0) return () => base;
|
|
34
|
+
return (x, z) => {
|
|
35
|
+
const [u, v] = floorUV(floor, x, z);
|
|
36
|
+
return base + sampleField(field, u, v) * scale;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,
|
|
41
|
+
* and (when a `heightmap` is given) displaced into dunes/berms by sampling that
|
|
42
|
+
* grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a
|
|
43
|
+
* `heightAt(x, z)` the camera controls use to follow the terrain.
|
|
44
|
+
*/
|
|
45
|
+
function buildFloor(floor) {
|
|
46
|
+
const [width, depth] = floor.size;
|
|
47
|
+
const [cx, cz] = floor.center ?? [0, 0];
|
|
48
|
+
const segments = floor.heightmap ? floor.segments ?? 200 : 1;
|
|
49
|
+
const material = new MeshStandardMaterial({ color: floor.color ?? "#ffffff" });
|
|
50
|
+
const needsUv1 = floor.texture ? applyTexture(material, floor.texture) : false;
|
|
51
|
+
const geometry = new PlaneGeometry(width, depth, segments, segments);
|
|
52
|
+
if (needsUv1) {
|
|
53
|
+
const uv = geometry.getAttribute("uv");
|
|
54
|
+
if (uv) geometry.setAttribute("uv1", uv);
|
|
55
|
+
}
|
|
56
|
+
const field = {
|
|
57
|
+
data: null,
|
|
58
|
+
w: 0,
|
|
59
|
+
h: 0
|
|
60
|
+
};
|
|
61
|
+
const scale = floor.heightScale ?? 0;
|
|
62
|
+
const pos = geometry.getAttribute("position");
|
|
63
|
+
const applyHeights = () => {
|
|
64
|
+
if (!pos || !field.data || scale === 0) return;
|
|
65
|
+
for (let i = 0; i < pos.count; i++) {
|
|
66
|
+
const px = pos.getX(i);
|
|
67
|
+
const py = pos.getY(i);
|
|
68
|
+
const u = (px + width / 2) / width;
|
|
69
|
+
const v = (py + depth / 2) / depth;
|
|
70
|
+
pos.setZ(i, sampleField(field, u, v) * scale);
|
|
71
|
+
}
|
|
72
|
+
pos.needsUpdate = true;
|
|
73
|
+
geometry.computeVertexNormals();
|
|
74
|
+
};
|
|
75
|
+
const mesh = new Mesh(geometry, material);
|
|
76
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
77
|
+
mesh.position.set(cx, floor.y ?? 0, cz);
|
|
78
|
+
mesh.receiveShadow = true;
|
|
79
|
+
mesh.name = "floor";
|
|
80
|
+
let ready = Promise.resolve();
|
|
81
|
+
const src = floor.heightmap;
|
|
82
|
+
if (src && scale !== 0) ready = new Promise((resolve) => {
|
|
83
|
+
const img = new Image();
|
|
84
|
+
img.crossOrigin = "anonymous";
|
|
85
|
+
img.onload = () => {
|
|
86
|
+
const canvas = document.createElement("canvas");
|
|
87
|
+
canvas.width = img.naturalWidth;
|
|
88
|
+
canvas.height = img.naturalHeight;
|
|
89
|
+
const ctx = canvas.getContext("2d");
|
|
90
|
+
if (ctx) {
|
|
91
|
+
ctx.drawImage(img, 0, 0);
|
|
92
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
93
|
+
const heights = new Float32Array(canvas.width * canvas.height);
|
|
94
|
+
for (let i = 0; i < heights.length; i++) heights[i] = (data[i * 4] ?? 0) / 255;
|
|
95
|
+
field.data = heights;
|
|
96
|
+
field.w = canvas.width;
|
|
97
|
+
field.h = canvas.height;
|
|
98
|
+
applyHeights();
|
|
99
|
+
}
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
img.onerror = () => resolve();
|
|
103
|
+
img.src = src;
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
object: mesh,
|
|
107
|
+
heightAt: heightAtWith(floor, field),
|
|
108
|
+
ready
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/plugin.ts
|
|
113
|
+
/**
|
|
114
|
+
* The floor plugin: lays the ground plane (displaced by a heightmap when given)
|
|
115
|
+
* and publishes the `terrain` capability — the live height function the camera
|
|
116
|
+
* follows and the definition the water shader samples. Rebuilds revoke and
|
|
117
|
+
* re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.
|
|
118
|
+
*/
|
|
119
|
+
const floor = () => ({
|
|
120
|
+
data: floorData,
|
|
121
|
+
build: (definition, ctx) => {
|
|
122
|
+
const built = buildFloor(definition);
|
|
123
|
+
const terrain = {
|
|
124
|
+
definition,
|
|
125
|
+
heightAt: built.heightAt,
|
|
126
|
+
ready: built.ready
|
|
127
|
+
};
|
|
128
|
+
ctx.capabilities.provide(TERRAIN, terrain);
|
|
129
|
+
return {
|
|
130
|
+
object: built.object,
|
|
131
|
+
dispose: () => ctx.capabilities.revoke(TERRAIN)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
//#endregion
|
|
136
|
+
export { buildFloor, floor, floorData, validateFloor };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/buildFloor.ts","../src/plugin.ts"],"sourcesContent":["import { applyTexture } from \"@scenography/engine\";\nimport { Mesh, MeshStandardMaterial, PlaneGeometry, type BufferAttribute, type Object3D } from \"three\";\n\nimport type { Floor } from \"./data\";\n\n/** A sampled grayscale heightmap, in `[0,1]`, populated once the image loads. */\ninterface HeightField {\n data: Float32Array | null;\n w: number;\n h: number;\n}\n\n/** Bilinear sample of a height field at UV `[0,1]`; `0` until the image loads. */\nfunction sampleField(field: HeightField, u: number, v: number): number {\n const { data, w, h } = field;\n if (!data) return 0;\n const x = Math.min(Math.max(u, 0), 1) * (w - 1);\n const y = Math.min(Math.max(v, 0), 1) * (h - 1);\n const x0 = Math.floor(x);\n const y0 = Math.floor(y);\n const x1 = Math.min(x0 + 1, w - 1);\n const y1 = Math.min(y0 + 1, h - 1);\n const fx = x - x0;\n const fy = y - y0;\n const a = data[y0 * w + x0] ?? 0;\n const b = data[y0 * w + x1] ?? 0;\n const c = data[y1 * w + x0] ?? 0;\n const d = data[y1 * w + x1] ?? 0;\n return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy;\n}\n\n/** A built floor plus a function giving ground height at any world `(x, z)`. */\nexport interface BuiltFloor {\n object: Object3D;\n heightAt: (x: number, z: number) => number;\n /**\n * Resolves once `heightAt` reflects the heightmap (immediately when there is\n * none). Until then `heightAt` returns the flat base `y`.\n */\n ready: Promise<void>;\n}\n\n/** Map world `(x, z)` to heightmap UV for a floor (matches the plane orientation). */\nconst floorUV = (floor: Floor, x: number, z: number): [number, number] => {\n const [w, d] = floor.size;\n const [cx, cz] = floor.center ?? [0, 0];\n const u = (x - cx + w / 2) / w;\n const v = (cz - z + d / 2) / d;\n return [u, v];\n};\n\n/** Ground height at `(x, z)`: base `y` + heightmap displacement. */\nfunction heightAtWith(floor: Floor, field: HeightField): (x: number, z: number) => number {\n const base = floor.y ?? 0;\n const scale = floor.heightScale ?? 0;\n if (scale === 0) return () => base;\n return (x, z) => {\n const [u, v] = floorUV(floor, x, z);\n return base + sampleField(field, u, v) * scale;\n };\n}\n\n/**\n * Build the three.js object for a `Floor`: a plane at `y`, centered on `center`,\n * and (when a `heightmap` is given) displaced into dunes/berms by sampling that\n * grayscale image. `size` is `[width (x), depth (z)]`. Returns the mesh plus a\n * `heightAt(x, z)` the camera controls use to follow the terrain.\n */\nexport function buildFloor(floor: Floor): BuiltFloor {\n const [width, depth] = floor.size;\n const [cx, cz] = floor.center ?? [0, 0];\n const segments = floor.heightmap ? floor.segments ?? 200 : 1;\n\n const material = new MeshStandardMaterial({ color: floor.color ?? \"#ffffff\" });\n const needsUv1 = floor.texture ? applyTexture(material, floor.texture) : false;\n\n const geometry = new PlaneGeometry(width, depth, segments, segments);\n if (needsUv1) {\n const uv = geometry.getAttribute(\"uv\");\n if (uv) geometry.setAttribute(\"uv1\", uv);\n }\n\n const field: HeightField = { data: null, w: 0, h: 0 };\n const scale = floor.heightScale ?? 0;\n const pos = geometry.getAttribute(\"position\") as BufferAttribute | undefined;\n\n // Displace the vertices once the heightmap loads. A vertex at local (px, py)\n // is raised along local Z = world +Y after the plane is laid flat.\n const applyHeights = (): void => {\n if (!pos || !field.data || scale === 0) return;\n for (let i = 0; i < pos.count; i++) {\n const px = pos.getX(i);\n const py = pos.getY(i);\n // Local (px, py) → world (cx+px, cz-py) → heightmap UV.\n const u = (px + width / 2) / width;\n const v = (py + depth / 2) / depth;\n pos.setZ(i, sampleField(field, u, v) * scale);\n }\n pos.needsUpdate = true;\n geometry.computeVertexNormals();\n };\n\n const mesh = new Mesh(geometry, material);\n // PlaneGeometry faces +Z; lay it flat so it faces up (+Y).\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.set(cx, floor.y ?? 0, cz);\n mesh.receiveShadow = true;\n mesh.name = \"floor\";\n\n // Load the heightmap, read its pixels, then displace the mesh and fill the\n // sampler so terrain-following (heightAt) matches the visible geometry.\n let ready: Promise<void> = Promise.resolve();\n const src = floor.heightmap;\n if (src && scale !== 0) {\n ready = new Promise((resolve) => {\n const img = new Image();\n img.crossOrigin = \"anonymous\";\n img.onload = (): void => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.drawImage(img, 0, 0);\n const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);\n const heights = new Float32Array(canvas.width * canvas.height);\n for (let i = 0; i < heights.length; i++) heights[i] = (data[i * 4] ?? 0) / 255;\n field.data = heights;\n field.w = canvas.width;\n field.h = canvas.height;\n applyHeights();\n }\n resolve();\n };\n img.onerror = (): void => resolve();\n img.src = src;\n });\n }\n\n return { object: mesh, heightAt: heightAtWith(floor, field), ready };\n}\n","import { TERRAIN, type EnginePlugin, type TerrainCapability } from \"@scenography/engine\";\n\nimport { buildFloor } from \"./buildFloor\";\nimport { floorData, type Floor } from \"./data\";\n\n/**\n * The floor plugin: lays the ground plane (displaced by a heightmap when given)\n * and publishes the `terrain` capability — the live height function the camera\n * follows and the definition the water shader samples. Rebuilds revoke and\n * re-provide terrain, so consumers (`dependsOn: [TERRAIN]`) refresh in step.\n */\nexport const floor = (): EnginePlugin<Floor> => ({\n data: floorData,\n build: (definition, ctx) => {\n const built = buildFloor(definition);\n const terrain: TerrainCapability = {\n definition,\n heightAt: built.heightAt,\n ready: built.ready,\n };\n ctx.capabilities.provide(TERRAIN, terrain);\n return {\n object: built.object,\n dispose: () => ctx.capabilities.revoke(TERRAIN),\n };\n },\n});\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scenography/plugin-floor",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Floor plugin for scenography: a ground plane optionally displaced into relief by a heightmap; publishes the terrain capability the camera and water consume. Data half importable without three.js.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.cts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"require": {
|
|
20
|
+
"types": "./dist/index.d.cts",
|
|
21
|
+
"default": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"./data": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/data.d.mts",
|
|
27
|
+
"default": "./dist/data.mjs"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/data.d.cts",
|
|
31
|
+
"default": "./dist/data.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@scenography/scene": "0.2.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/three": "^0.160.0",
|
|
40
|
+
"three": "^0.160.0",
|
|
41
|
+
"@scenography/engine": "0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"three": ">=0.160.0",
|
|
45
|
+
"@scenography/engine": "0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"dev": "tsdown --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|