@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.
@@ -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
@@ -0,0 +1,4 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_data = require("./data-BgdlM_kL.cjs");
3
+ exports.floorData = require_data.floorData;
4
+ exports.validateFloor = require_data.validateFloor;
@@ -0,0 +1,2 @@
1
+ import { n as validateFloor, r as Floor, t as floorData } from "./index-1ydrP4eD.cjs";
2
+ export { type Floor, floorData, validateFloor };
@@ -0,0 +1,2 @@
1
+ import { n as validateFloor, r as Floor, t as floorData } from "./index-1ydrP4eD.mjs";
2
+ export { type Floor, floorData, validateFloor };
package/dist/data.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as validateFloor, t as floorData } from "./data-CTGlghAE.mjs";
2
+ export { floorData, validateFloor };
@@ -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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"}
@@ -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
@@ -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
+ }