@scenography/plugin-light 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,41 @@
1
+ let _scenography_scene = require("@scenography/scene");
2
+ //#region src/data/validate.ts
3
+ const POSITIONED = new Set([
4
+ "directional",
5
+ "point",
6
+ "spot"
7
+ ]);
8
+ const LIGHT_TYPES = new Set([
9
+ ...POSITIONED,
10
+ "ambient",
11
+ "hemisphere"
12
+ ]);
13
+ const validateLight = (light) => (0, _scenography_scene.validate)([
14
+ (0, _scenography_scene.existsProp)("id", "Light is missing an id."),
15
+ (0, _scenography_scene.propIs)("intensity", _scenography_scene.isFiniteNumber, "intensity must be a finite number."),
16
+ (0, _scenography_scene.when)((l) => POSITIONED.has(l.type), (0, _scenography_scene.existsProp)("position"), (0, _scenography_scene.propIs)("position", _scenography_scene.isVec3, "position must be [x, y, z]."), (0, _scenography_scene.propIs)("target", _scenography_scene.isVec3, "target must be [x, y, z].")),
17
+ (0, _scenography_scene.when)((l) => !(0, _scenography_scene.isOneOf)(LIGHT_TYPES)(l.type), (0, _scenography_scene.fail)("type", "unknown light type."))
18
+ ])(light);
19
+ //#endregion
20
+ //#region src/data/plugin.ts
21
+ /** The data half of the light plugin: owns the `lights` scene key (a collection). */
22
+ const lightData = {
23
+ kind: "lights",
24
+ cardinality: "many",
25
+ validate: (light) => validateLight(light)
26
+ };
27
+ //#endregion
28
+ Object.defineProperty(exports, "lightData", {
29
+ enumerable: true,
30
+ get: function() {
31
+ return lightData;
32
+ }
33
+ });
34
+ Object.defineProperty(exports, "validateLight", {
35
+ enumerable: true,
36
+ get: function() {
37
+ return validateLight;
38
+ }
39
+ });
40
+
41
+ //# sourceMappingURL=data-CcMZWQIz.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-CcMZWQIz.cjs","names":["isFiniteNumber","isVec3"],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n fail,\n isFiniteNumber,\n isOneOf,\n isVec3,\n propIs,\n validate,\n when,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Light } from \"./light\";\n\nconst POSITIONED: ReadonlySet<string> = new Set([\"directional\", \"point\", \"spot\"]);\nconst LIGHT_TYPES: ReadonlySet<string> = new Set([...POSITIONED, \"ambient\", \"hemisphere\"]);\n\nexport const validateLight = (light: Light): ValidationResult =>\n validate<Light>([\n existsProp(\"id\", \"Light is missing an id.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n when(\n (l: Light) => POSITIONED.has(l.type),\n existsProp(\"position\"),\n propIs(\"position\", isVec3, \"position must be [x, y, z].\"),\n propIs(\"target\", isVec3, \"target must be [x, y, z].\"),\n ),\n when(\n (l: Light) => !isOneOf(LIGHT_TYPES)(l.type),\n fail(\"type\", \"unknown light type.\"),\n ),\n ])(light);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateLight } from \"./validate\";\nimport type { Light } from \"./light\";\n\n/** The data half of the light plugin: owns the `lights` scene key (a collection). */\nexport const lightData: DataPlugin<Light> = {\n kind: \"lights\",\n cardinality: \"many\",\n validate: (light) => validateLight(light),\n};\n"],"mappings":";;AAcA,MAAM,aAAkC,IAAI,IAAI;CAAC;CAAe;CAAS;AAAM,CAAC;AAChF,MAAM,cAAmC,IAAI,IAAI;CAAC,GAAG;CAAY;CAAW;AAAY,CAAC;AAEzF,MAAa,iBAAiB,WAAA,GAAA,mBAAA,SAAA,CACZ;oCACH,MAAM,yBAAyB;gCACnC,aAAaA,mBAAAA,gBAAgB,oCAAoC;+BAErE,MAAa,WAAW,IAAI,EAAE,IAAI,IAAA,GAAA,mBAAA,WAAA,CACxB,UAAU,IAAA,GAAA,mBAAA,OAAA,CACd,YAAYC,mBAAAA,QAAQ,6BAA6B,IAAA,GAAA,mBAAA,OAAA,CACjD,UAAUA,mBAAAA,QAAQ,2BAA2B,CACtD;+BAEG,MAAa,EAAA,GAAA,mBAAA,QAAA,CAAS,WAAW,CAAC,CAAC,EAAE,IAAI,IAAA,GAAA,mBAAA,KAAA,CACrC,QAAQ,qBAAqB,CACpC;AACF,CAAC,CAAC,CAAC,KAAK;;;;ACzBV,MAAa,YAA+B;CAC1C,MAAM;CACN,aAAa;CACb,WAAW,UAAU,cAAc,KAAK;AAC1C"}
@@ -0,0 +1,30 @@
1
+ import { existsProp, fail, isFiniteNumber, isOneOf, isVec3, propIs, validate, when } from "@scenography/scene";
2
+ //#region src/data/validate.ts
3
+ const POSITIONED = new Set([
4
+ "directional",
5
+ "point",
6
+ "spot"
7
+ ]);
8
+ const LIGHT_TYPES = new Set([
9
+ ...POSITIONED,
10
+ "ambient",
11
+ "hemisphere"
12
+ ]);
13
+ const validateLight = (light) => validate([
14
+ existsProp("id", "Light is missing an id."),
15
+ propIs("intensity", isFiniteNumber, "intensity must be a finite number."),
16
+ when((l) => POSITIONED.has(l.type), existsProp("position"), propIs("position", isVec3, "position must be [x, y, z]."), propIs("target", isVec3, "target must be [x, y, z].")),
17
+ when((l) => !isOneOf(LIGHT_TYPES)(l.type), fail("type", "unknown light type."))
18
+ ])(light);
19
+ //#endregion
20
+ //#region src/data/plugin.ts
21
+ /** The data half of the light plugin: owns the `lights` scene key (a collection). */
22
+ const lightData = {
23
+ kind: "lights",
24
+ cardinality: "many",
25
+ validate: (light) => validateLight(light)
26
+ };
27
+ //#endregion
28
+ export { validateLight as n, lightData as t };
29
+
30
+ //# sourceMappingURL=data-bkULec9x.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-bkULec9x.mjs","names":[],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n fail,\n isFiniteNumber,\n isOneOf,\n isVec3,\n propIs,\n validate,\n when,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Light } from \"./light\";\n\nconst POSITIONED: ReadonlySet<string> = new Set([\"directional\", \"point\", \"spot\"]);\nconst LIGHT_TYPES: ReadonlySet<string> = new Set([...POSITIONED, \"ambient\", \"hemisphere\"]);\n\nexport const validateLight = (light: Light): ValidationResult =>\n validate<Light>([\n existsProp(\"id\", \"Light is missing an id.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n when(\n (l: Light) => POSITIONED.has(l.type),\n existsProp(\"position\"),\n propIs(\"position\", isVec3, \"position must be [x, y, z].\"),\n propIs(\"target\", isVec3, \"target must be [x, y, z].\"),\n ),\n when(\n (l: Light) => !isOneOf(LIGHT_TYPES)(l.type),\n fail(\"type\", \"unknown light type.\"),\n ),\n ])(light);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateLight } from \"./validate\";\nimport type { Light } from \"./light\";\n\n/** The data half of the light plugin: owns the `lights` scene key (a collection). */\nexport const lightData: DataPlugin<Light> = {\n kind: \"lights\",\n cardinality: \"many\",\n validate: (light) => validateLight(light),\n};\n"],"mappings":";;AAcA,MAAM,aAAkC,IAAI,IAAI;CAAC;CAAe;CAAS;AAAM,CAAC;AAChF,MAAM,cAAmC,IAAI,IAAI;CAAC,GAAG;CAAY;CAAW;AAAY,CAAC;AAEzF,MAAa,iBAAiB,UAC5B,SAAgB;CACd,WAAW,MAAM,yBAAyB;CAC1C,OAAO,aAAa,gBAAgB,oCAAoC;CACxE,MACG,MAAa,WAAW,IAAI,EAAE,IAAI,GACnC,WAAW,UAAU,GACrB,OAAO,YAAY,QAAQ,6BAA6B,GACxD,OAAO,UAAU,QAAQ,2BAA2B,CACtD;CACA,MACG,MAAa,CAAC,QAAQ,WAAW,CAAC,CAAC,EAAE,IAAI,GAC1C,KAAK,QAAQ,qBAAqB,CACpC;AACF,CAAC,CAAC,CAAC,KAAK;;;;ACzBV,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-CcMZWQIz.cjs");
3
+ exports.lightData = require_data.lightData;
4
+ exports.validateLight = require_data.validateLight;
@@ -0,0 +1,2 @@
1
+ import { a as LightType, i as LightShadow, n as validateLight, r as Light, t as lightData } from "./index-CfFuhLoP.cjs";
2
+ export { type Light, type LightShadow, type LightType, lightData, validateLight };
@@ -0,0 +1,2 @@
1
+ import { a as LightType, i as LightShadow, n as validateLight, r as Light, t as lightData } from "./index-CfFuhLoP.mjs";
2
+ export { type Light, type LightShadow, type LightType, lightData, validateLight };
package/dist/data.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as validateLight, t as lightData } from "./data-bkULec9x.mjs";
2
+ export { lightData, validateLight };
@@ -0,0 +1,86 @@
1
+ import { DataPlugin, ValidationResult, Vec3 } from "@scenography/scene";
2
+
3
+ //#region src/data/light.d.ts
4
+ /** Fields shared by every light variant. */
5
+ interface LightBase {
6
+ id: string;
7
+ /** CSS-style color. Defaults to white. */
8
+ color?: string;
9
+ /** Brightness multiplier. Sensible defaults vary by `type`. */
10
+ intensity?: number;
11
+ }
12
+ /**
13
+ * Shadow-casting settings for a light. Only `directional`, `spot` and `point`
14
+ * lights can cast. `true` enables shadows with engine defaults; the object form
15
+ * tunes quality and the shadow camera. Runtime detail — but declared as data so
16
+ * the look is part of the serializable scene.
17
+ */
18
+ interface LightShadow {
19
+ /** Shadow map resolution in px (square). Defaults to 1024. */
20
+ mapSize?: number;
21
+ /** Depth bias to fight shadow acne. Defaults to 0. */
22
+ bias?: number;
23
+ /** Normal-offset bias, softer fix for acne/peter-panning. Defaults to 0. */
24
+ normalBias?: number;
25
+ /** Near plane of the shadow camera. */
26
+ near?: number;
27
+ /** Far plane of the shadow camera. */
28
+ far?: number;
29
+ /**
30
+ * `directional` only: half-size of the orthographic frustum in world units —
31
+ * make it large enough to cover the area that should receive shadows.
32
+ */
33
+ size?: number;
34
+ }
35
+ /** Shorthand: `true` for defaults, or a settings object. */
36
+ type ShadowConfig = boolean | LightShadow;
37
+ /**
38
+ * A light in the scene. Extensible via `type`; covers uniform fill (`ambient`,
39
+ * `hemisphere`), a sun-like source (`directional`), an omni source (`point`) and
40
+ * a cone spotlight (`spot`). Behavior (dimming, animation) is wired by `id` in
41
+ * the runtime through `ctx.lights` — the definition stays pure data.
42
+ */
43
+ type Light = (LightBase & {
44
+ type: "ambient";
45
+ }) | (LightBase & {
46
+ type: "hemisphere"; /** Color bounced from below. Defaults to a dark gray. */
47
+ groundColor?: string;
48
+ }) | (LightBase & {
49
+ type: "directional"; /** World position the parallel rays emanate from. */
50
+ position: Vec3; /** Point the light aims at. Defaults to the origin. */
51
+ target?: Vec3; /** Cast shadows from this light. */
52
+ shadow?: ShadowConfig;
53
+ }) | (LightBase & {
54
+ type: "point"; /** World position of the source. */
55
+ position: Vec3; /** Range in world units; `0` (default) is infinite. */
56
+ distance?: number; /** Falloff exponent. Defaults to `2` (physically correct). */
57
+ decay?: number; /** Cast shadows from this light. */
58
+ shadow?: ShadowConfig;
59
+ }) | (LightBase & {
60
+ type: "spot"; /** World position of the source. */
61
+ position: Vec3; /** Point the cone aims at. Defaults to the origin. */
62
+ target?: Vec3; /** Cone half-angle in radians. */
63
+ angle?: number; /** Soft edge, `0`..`1`. Defaults to `0`. */
64
+ penumbra?: number; /** Range in world units; `0` (default) is infinite. */
65
+ distance?: number; /** Falloff exponent. Defaults to `2`. */
66
+ decay?: number; /** Cast shadows from this light. */
67
+ shadow?: ShadowConfig;
68
+ });
69
+ /** The discriminant values a `Light` can take. */
70
+ type LightType = Light["type"];
71
+ declare module "@scenography/scene" {
72
+ interface Scene {
73
+ /** Lights in the space — provided by `@scenography/plugin-light`. */
74
+ lights?: Light[];
75
+ }
76
+ }
77
+ //#endregion
78
+ //#region src/data/validate.d.ts
79
+ declare const validateLight: (light: Light) => ValidationResult;
80
+ //#endregion
81
+ //#region src/data/plugin.d.ts
82
+ /** The data half of the light plugin: owns the `lights` scene key (a collection). */
83
+ declare const lightData: DataPlugin<Light>;
84
+ //#endregion
85
+ export { LightType as a, LightShadow as i, validateLight as n, Light as r, lightData as t };
86
+ //# sourceMappingURL=index-CfFuhLoP.d.cts.map
@@ -0,0 +1,86 @@
1
+ import { DataPlugin, ValidationResult, Vec3 } from "@scenography/scene";
2
+
3
+ //#region src/data/light.d.ts
4
+ /** Fields shared by every light variant. */
5
+ interface LightBase {
6
+ id: string;
7
+ /** CSS-style color. Defaults to white. */
8
+ color?: string;
9
+ /** Brightness multiplier. Sensible defaults vary by `type`. */
10
+ intensity?: number;
11
+ }
12
+ /**
13
+ * Shadow-casting settings for a light. Only `directional`, `spot` and `point`
14
+ * lights can cast. `true` enables shadows with engine defaults; the object form
15
+ * tunes quality and the shadow camera. Runtime detail — but declared as data so
16
+ * the look is part of the serializable scene.
17
+ */
18
+ interface LightShadow {
19
+ /** Shadow map resolution in px (square). Defaults to 1024. */
20
+ mapSize?: number;
21
+ /** Depth bias to fight shadow acne. Defaults to 0. */
22
+ bias?: number;
23
+ /** Normal-offset bias, softer fix for acne/peter-panning. Defaults to 0. */
24
+ normalBias?: number;
25
+ /** Near plane of the shadow camera. */
26
+ near?: number;
27
+ /** Far plane of the shadow camera. */
28
+ far?: number;
29
+ /**
30
+ * `directional` only: half-size of the orthographic frustum in world units —
31
+ * make it large enough to cover the area that should receive shadows.
32
+ */
33
+ size?: number;
34
+ }
35
+ /** Shorthand: `true` for defaults, or a settings object. */
36
+ type ShadowConfig = boolean | LightShadow;
37
+ /**
38
+ * A light in the scene. Extensible via `type`; covers uniform fill (`ambient`,
39
+ * `hemisphere`), a sun-like source (`directional`), an omni source (`point`) and
40
+ * a cone spotlight (`spot`). Behavior (dimming, animation) is wired by `id` in
41
+ * the runtime through `ctx.lights` — the definition stays pure data.
42
+ */
43
+ type Light = (LightBase & {
44
+ type: "ambient";
45
+ }) | (LightBase & {
46
+ type: "hemisphere"; /** Color bounced from below. Defaults to a dark gray. */
47
+ groundColor?: string;
48
+ }) | (LightBase & {
49
+ type: "directional"; /** World position the parallel rays emanate from. */
50
+ position: Vec3; /** Point the light aims at. Defaults to the origin. */
51
+ target?: Vec3; /** Cast shadows from this light. */
52
+ shadow?: ShadowConfig;
53
+ }) | (LightBase & {
54
+ type: "point"; /** World position of the source. */
55
+ position: Vec3; /** Range in world units; `0` (default) is infinite. */
56
+ distance?: number; /** Falloff exponent. Defaults to `2` (physically correct). */
57
+ decay?: number; /** Cast shadows from this light. */
58
+ shadow?: ShadowConfig;
59
+ }) | (LightBase & {
60
+ type: "spot"; /** World position of the source. */
61
+ position: Vec3; /** Point the cone aims at. Defaults to the origin. */
62
+ target?: Vec3; /** Cone half-angle in radians. */
63
+ angle?: number; /** Soft edge, `0`..`1`. Defaults to `0`. */
64
+ penumbra?: number; /** Range in world units; `0` (default) is infinite. */
65
+ distance?: number; /** Falloff exponent. Defaults to `2`. */
66
+ decay?: number; /** Cast shadows from this light. */
67
+ shadow?: ShadowConfig;
68
+ });
69
+ /** The discriminant values a `Light` can take. */
70
+ type LightType = Light["type"];
71
+ declare module "@scenography/scene" {
72
+ interface Scene {
73
+ /** Lights in the space — provided by `@scenography/plugin-light`. */
74
+ lights?: Light[];
75
+ }
76
+ }
77
+ //#endregion
78
+ //#region src/data/validate.d.ts
79
+ declare const validateLight: (light: Light) => ValidationResult;
80
+ //#endregion
81
+ //#region src/data/plugin.d.ts
82
+ /** The data half of the light plugin: owns the `lights` scene key (a collection). */
83
+ declare const lightData: DataPlugin<Light>;
84
+ //#endregion
85
+ export { LightType as a, LightShadow as i, validateLight as n, Light as r, lightData as t };
86
+ //# sourceMappingURL=index-CfFuhLoP.d.mts.map
package/dist/index.cjs ADDED
@@ -0,0 +1,177 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_data = require("./data-CcMZWQIz.cjs");
3
+ let three = require("three");
4
+ //#region src/buildLight.ts
5
+ const DEFAULT_COLOR = "#ffffff";
6
+ const DEFAULT_SHADOW_MAP = 1024;
7
+ /** Enable and configure shadow casting on a light from its `shadow` data. */
8
+ const applyShadow = (light, config) => {
9
+ light.castShadow = true;
10
+ const s = typeof config === "object" ? config : {};
11
+ light.shadow.mapSize.setScalar(s.mapSize ?? DEFAULT_SHADOW_MAP);
12
+ if (s.bias !== void 0) light.shadow.bias = s.bias;
13
+ if (s.normalBias !== void 0) light.shadow.normalBias = s.normalBias;
14
+ const cam = light.shadow.camera;
15
+ if (s.near !== void 0) cam.near = s.near;
16
+ if (s.far !== void 0) cam.far = s.far;
17
+ if (cam instanceof three.OrthographicCamera && s.size !== void 0) {
18
+ cam.left = -s.size;
19
+ cam.right = s.size;
20
+ cam.top = s.size;
21
+ cam.bottom = -s.size;
22
+ }
23
+ cam.updateProjectionMatrix();
24
+ };
25
+ /**
26
+ * Build the three.js object for a `Light`. Returns a `Group` (even for a single
27
+ * light) so directional/spot lights can carry their `target` in the same node,
28
+ * and so every light is addressed uniformly by `id` for reconciliation.
29
+ */
30
+ function buildLight(light) {
31
+ const color = light.color ?? DEFAULT_COLOR;
32
+ const group = new three.Group();
33
+ switch (light.type) {
34
+ case "ambient":
35
+ group.add(new three.AmbientLight(color, light.intensity ?? 1.4));
36
+ break;
37
+ case "hemisphere":
38
+ group.add(new three.HemisphereLight(color, light.groundColor ?? "#444444", light.intensity ?? 1));
39
+ break;
40
+ case "directional": {
41
+ const l = new three.DirectionalLight(color, light.intensity ?? 1);
42
+ l.position.set(...light.position);
43
+ l.target.position.set(...light.target ?? [
44
+ 0,
45
+ 0,
46
+ 0
47
+ ]);
48
+ if (light.shadow) applyShadow(l, light.shadow);
49
+ group.add(l, l.target);
50
+ break;
51
+ }
52
+ case "point": {
53
+ const l = new three.PointLight(color, light.intensity ?? 1, light.distance ?? 0, light.decay ?? 2);
54
+ l.position.set(...light.position);
55
+ if (light.shadow) applyShadow(l, light.shadow);
56
+ group.add(l);
57
+ break;
58
+ }
59
+ case "spot": {
60
+ const l = new three.SpotLight(color, light.intensity ?? 1, light.distance ?? 0, light.angle ?? Math.PI / 6, light.penumbra ?? 0, light.decay ?? 2);
61
+ l.position.set(...light.position);
62
+ l.target.position.set(...light.target ?? [
63
+ 0,
64
+ 0,
65
+ 0
66
+ ]);
67
+ if (light.shadow) applyShadow(l, light.shadow);
68
+ group.add(l, l.target);
69
+ break;
70
+ }
71
+ }
72
+ group.name = `light:${light.id}`;
73
+ group.userData.scenographyId = light.id;
74
+ return group;
75
+ }
76
+ //#endregion
77
+ //#region src/LightController.ts
78
+ const isThreeLight = (o) => o.isLight === true;
79
+ /**
80
+ * High-level light control surfaced as `ctx.lights`. Works over the live light
81
+ * objects (queried fresh each call so it keeps working across hot-swaps),
82
+ * tweening intensity in the render loop.
83
+ */
84
+ var LightController = class {
85
+ getLights;
86
+ tweens = /* @__PURE__ */ new Map();
87
+ /** Intensity captured before a dim, so `restore` can bring it back. */
88
+ baseline = /* @__PURE__ */ new WeakMap();
89
+ constructor(getLights) {
90
+ this.getLights = getLights;
91
+ }
92
+ lightsOf(group) {
93
+ const found = [];
94
+ group.traverse((o) => {
95
+ if (isThreeLight(o)) found.push(o);
96
+ });
97
+ return found;
98
+ }
99
+ all() {
100
+ return [...this.getLights().values()].flatMap((g) => this.lightsOf(g));
101
+ }
102
+ fade(light, to, duration) {
103
+ if (!duration || duration <= 0) {
104
+ this.tweens.delete(light);
105
+ light.intensity = to;
106
+ return;
107
+ }
108
+ this.tweens.set(light, {
109
+ from: light.intensity,
110
+ to,
111
+ elapsed: 0,
112
+ duration
113
+ });
114
+ }
115
+ /** Set intensity and/or color of every light under `id`. */
116
+ set(id, props, opts = {}) {
117
+ const group = this.getLights().get(id);
118
+ if (!group) return;
119
+ for (const light of this.lightsOf(group)) {
120
+ if (props.color !== void 0) light.color.set(props.color);
121
+ if (props.intensity !== void 0) this.fade(light, props.intensity, opts.duration ?? 0);
122
+ }
123
+ }
124
+ /** Fade every light toward `factor` of its current intensity (default off). */
125
+ dimAll(opts = {}) {
126
+ const factor = opts.factor ?? 0;
127
+ for (const light of this.all()) {
128
+ const base = this.baseline.get(light) ?? light.intensity;
129
+ this.baseline.set(light, base);
130
+ this.fade(light, base * factor, opts.duration ?? 0);
131
+ }
132
+ }
133
+ /** Fade every light back to the intensity captured before the last dim. */
134
+ restore(opts = {}) {
135
+ for (const light of this.all()) {
136
+ const base = this.baseline.get(light);
137
+ if (base === void 0) continue;
138
+ this.fade(light, base, opts.duration ?? 0);
139
+ this.baseline.delete(light);
140
+ }
141
+ }
142
+ /** Advance active fades. Called once per frame with the frame delta in ms. */
143
+ update(deltaMs) {
144
+ for (const [light, t] of this.tweens) {
145
+ t.elapsed += deltaMs;
146
+ const k = t.duration > 0 ? Math.min(t.elapsed / t.duration, 1) : 1;
147
+ light.intensity = t.from + (t.to - t.from) * k;
148
+ if (k >= 1) this.tweens.delete(light);
149
+ }
150
+ }
151
+ };
152
+ //#endregion
153
+ //#region src/plugin.ts
154
+ /**
155
+ * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/
156
+ * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets
157
+ * intensity and color over the live lights, tweening in the render loop.
158
+ */
159
+ const light = () => ({
160
+ data: require_data.lightData,
161
+ build: (definition) => ({ object: buildLight(definition) }),
162
+ controller: (elements) => {
163
+ const controller = new LightController(elements);
164
+ return {
165
+ api: controller,
166
+ update: (deltaMs) => controller.update(deltaMs)
167
+ };
168
+ }
169
+ });
170
+ //#endregion
171
+ exports.LightController = LightController;
172
+ exports.buildLight = buildLight;
173
+ exports.light = light;
174
+ exports.lightData = require_data.lightData;
175
+ exports.validateLight = require_data.validateLight;
176
+
177
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["OrthographicCamera","Group","AmbientLight","HemisphereLight","DirectionalLight","PointLight","SpotLight","lightData"],"sources":["../src/buildLight.ts","../src/LightController.ts","../src/plugin.ts"],"sourcesContent":["import {\n AmbientLight,\n DirectionalLight,\n Group,\n HemisphereLight,\n OrthographicCamera,\n PointLight,\n SpotLight,\n type Object3D,\n} from \"three\";\nimport type { Light, LightShadow } from \"./data\";\n\nconst DEFAULT_COLOR = \"#ffffff\";\nconst DEFAULT_SHADOW_MAP = 1024;\n\n/** Enable and configure shadow casting on a light from its `shadow` data. */\nconst applyShadow = (\n light: DirectionalLight | SpotLight | PointLight,\n config: boolean | LightShadow,\n): void => {\n light.castShadow = true;\n const s: LightShadow = typeof config === \"object\" ? config : {};\n light.shadow.mapSize.setScalar(s.mapSize ?? DEFAULT_SHADOW_MAP);\n if (s.bias !== undefined) light.shadow.bias = s.bias;\n if (s.normalBias !== undefined) light.shadow.normalBias = s.normalBias;\n\n const cam = light.shadow.camera;\n if (s.near !== undefined) cam.near = s.near;\n if (s.far !== undefined) cam.far = s.far;\n // Directional lights use an orthographic frustum; size it to the lit area.\n if (cam instanceof OrthographicCamera && s.size !== undefined) {\n cam.left = -s.size;\n cam.right = s.size;\n cam.top = s.size;\n cam.bottom = -s.size;\n }\n cam.updateProjectionMatrix();\n};\n\n/**\n * Build the three.js object for a `Light`. Returns a `Group` (even for a single\n * light) so directional/spot lights can carry their `target` in the same node,\n * and so every light is addressed uniformly by `id` for reconciliation.\n */\nexport function buildLight(light: Light): Object3D {\n const color = light.color ?? DEFAULT_COLOR;\n const group = new Group();\n\n switch (light.type) {\n case \"ambient\":\n group.add(new AmbientLight(color, light.intensity ?? 1.4));\n break;\n case \"hemisphere\":\n group.add(\n new HemisphereLight(color, light.groundColor ?? \"#444444\", light.intensity ?? 1),\n );\n break;\n case \"directional\": {\n const l = new DirectionalLight(color, light.intensity ?? 1);\n l.position.set(...light.position);\n l.target.position.set(...(light.target ?? [0, 0, 0]));\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l, l.target);\n break;\n }\n case \"point\": {\n const l = new PointLight(color, light.intensity ?? 1, light.distance ?? 0, light.decay ?? 2);\n l.position.set(...light.position);\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l);\n break;\n }\n case \"spot\": {\n const l = new SpotLight(\n color,\n light.intensity ?? 1,\n light.distance ?? 0,\n light.angle ?? Math.PI / 6,\n light.penumbra ?? 0,\n light.decay ?? 2,\n );\n l.position.set(...light.position);\n l.target.position.set(...(light.target ?? [0, 0, 0]));\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l, l.target);\n break;\n }\n }\n\n group.name = `light:${light.id}`;\n group.userData.scenographyId = light.id;\n return group;\n}\n","import type { LightSet, LightSetOptions, LightsApi } from \"@scenography/engine\";\nimport type { Color, Light as ThreeLight, Object3D } from \"three\";\n\ninterface Tween {\n from: number;\n to: number;\n elapsed: number;\n duration: number;\n}\n\nconst isThreeLight = (o: Object3D): o is ThreeLight => (o as ThreeLight).isLight === true;\n\n/**\n * High-level light control surfaced as `ctx.lights`. Works over the live light\n * objects (queried fresh each call so it keeps working across hot-swaps),\n * tweening intensity in the render loop.\n */\nexport class LightController implements LightsApi {\n private readonly getLights: () => ReadonlyMap<string, Object3D>;\n private readonly tweens = new Map<ThreeLight, Tween>();\n /** Intensity captured before a dim, so `restore` can bring it back. */\n private readonly baseline = new WeakMap<ThreeLight, number>();\n\n constructor(getLights: () => ReadonlyMap<string, Object3D>) {\n this.getLights = getLights;\n }\n\n private lightsOf(group: Object3D): ThreeLight[] {\n const found: ThreeLight[] = [];\n group.traverse((o) => {\n if (isThreeLight(o)) found.push(o);\n });\n return found;\n }\n\n private all(): ThreeLight[] {\n return [...this.getLights().values()].flatMap((g) => this.lightsOf(g));\n }\n\n private fade(light: ThreeLight, to: number, duration: number): void {\n if (!duration || duration <= 0) {\n this.tweens.delete(light);\n light.intensity = to;\n return;\n }\n this.tweens.set(light, { from: light.intensity, to, elapsed: 0, duration });\n }\n\n /** Set intensity and/or color of every light under `id`. */\n set(id: string, props: LightSet, opts: LightSetOptions = {}): void {\n const group = this.getLights().get(id);\n if (!group) return;\n for (const light of this.lightsOf(group)) {\n if (props.color !== undefined) (light.color as Color).set(props.color);\n if (props.intensity !== undefined) this.fade(light, props.intensity, opts.duration ?? 0);\n }\n }\n\n /** Fade every light toward `factor` of its current intensity (default off). */\n dimAll(opts: LightSetOptions & { factor?: number } = {}): void {\n const factor = opts.factor ?? 0;\n for (const light of this.all()) {\n const base = this.baseline.get(light) ?? light.intensity;\n this.baseline.set(light, base);\n this.fade(light, base * factor, opts.duration ?? 0);\n }\n }\n\n /** Fade every light back to the intensity captured before the last dim. */\n restore(opts: LightSetOptions = {}): void {\n for (const light of this.all()) {\n const base = this.baseline.get(light);\n if (base === undefined) continue;\n this.fade(light, base, opts.duration ?? 0);\n this.baseline.delete(light);\n }\n }\n\n /** Advance active fades. Called once per frame with the frame delta in ms. */\n update(deltaMs: number): void {\n for (const [light, t] of this.tweens) {\n t.elapsed += deltaMs;\n const k = t.duration > 0 ? Math.min(t.elapsed / t.duration, 1) : 1;\n light.intensity = t.from + (t.to - t.from) * k;\n if (k >= 1) this.tweens.delete(light);\n }\n }\n}\n","import type { EnginePlugin } from \"@scenography/engine\";\n\nimport { buildLight } from \"./buildLight\";\nimport { LightController } from \"./LightController\";\nimport { lightData, type Light } from \"./data\";\n\n/**\n * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/\n * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets\n * intensity and color over the live lights, tweening in the render loop.\n */\nexport const light = (): EnginePlugin<Light> => ({\n data: lightData,\n build: (definition) => ({ object: buildLight(definition) }),\n controller: (elements) => {\n const controller = new LightController(elements);\n return { api: controller, update: (deltaMs) => controller.update(deltaMs) };\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { a as LightType, i as LightShadow, n as validateLight, r as Light, t as lightData } from "./index-CfFuhLoP.cjs";
2
+ import { EnginePlugin, LightSet, LightSetOptions, LightsApi } from "@scenography/engine";
3
+ import { Object3D } from "three";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/
8
+ * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets
9
+ * intensity and color over the live lights, tweening in the render loop.
10
+ */
11
+ declare const light: () => EnginePlugin<Light>;
12
+ //#endregion
13
+ //#region src/buildLight.d.ts
14
+ /**
15
+ * Build the three.js object for a `Light`. Returns a `Group` (even for a single
16
+ * light) so directional/spot lights can carry their `target` in the same node,
17
+ * and so every light is addressed uniformly by `id` for reconciliation.
18
+ */
19
+ declare function buildLight(light: Light): Object3D;
20
+ //#endregion
21
+ //#region src/LightController.d.ts
22
+ /**
23
+ * High-level light control surfaced as `ctx.lights`. Works over the live light
24
+ * objects (queried fresh each call so it keeps working across hot-swaps),
25
+ * tweening intensity in the render loop.
26
+ */
27
+ declare class LightController implements LightsApi {
28
+ private readonly getLights;
29
+ private readonly tweens;
30
+ /** Intensity captured before a dim, so `restore` can bring it back. */
31
+ private readonly baseline;
32
+ constructor(getLights: () => ReadonlyMap<string, Object3D>);
33
+ private lightsOf;
34
+ private all;
35
+ private fade;
36
+ /** Set intensity and/or color of every light under `id`. */
37
+ set(id: string, props: LightSet, opts?: LightSetOptions): void;
38
+ /** Fade every light toward `factor` of its current intensity (default off). */
39
+ dimAll(opts?: LightSetOptions & {
40
+ factor?: number;
41
+ }): void;
42
+ /** Fade every light back to the intensity captured before the last dim. */
43
+ restore(opts?: LightSetOptions): void;
44
+ /** Advance active fades. Called once per frame with the frame delta in ms. */
45
+ update(deltaMs: number): void;
46
+ }
47
+ //#endregion
48
+ export { type Light, LightController, type LightShadow, type LightType, buildLight, light, lightData, validateLight };
49
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,49 @@
1
+ import { a as LightType, i as LightShadow, n as validateLight, r as Light, t as lightData } from "./index-CfFuhLoP.mjs";
2
+ import { Object3D } from "three";
3
+ import { EnginePlugin, LightSet, LightSetOptions, LightsApi } from "@scenography/engine";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/
8
+ * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets
9
+ * intensity and color over the live lights, tweening in the render loop.
10
+ */
11
+ declare const light: () => EnginePlugin<Light>;
12
+ //#endregion
13
+ //#region src/buildLight.d.ts
14
+ /**
15
+ * Build the three.js object for a `Light`. Returns a `Group` (even for a single
16
+ * light) so directional/spot lights can carry their `target` in the same node,
17
+ * and so every light is addressed uniformly by `id` for reconciliation.
18
+ */
19
+ declare function buildLight(light: Light): Object3D;
20
+ //#endregion
21
+ //#region src/LightController.d.ts
22
+ /**
23
+ * High-level light control surfaced as `ctx.lights`. Works over the live light
24
+ * objects (queried fresh each call so it keeps working across hot-swaps),
25
+ * tweening intensity in the render loop.
26
+ */
27
+ declare class LightController implements LightsApi {
28
+ private readonly getLights;
29
+ private readonly tweens;
30
+ /** Intensity captured before a dim, so `restore` can bring it back. */
31
+ private readonly baseline;
32
+ constructor(getLights: () => ReadonlyMap<string, Object3D>);
33
+ private lightsOf;
34
+ private all;
35
+ private fade;
36
+ /** Set intensity and/or color of every light under `id`. */
37
+ set(id: string, props: LightSet, opts?: LightSetOptions): void;
38
+ /** Fade every light toward `factor` of its current intensity (default off). */
39
+ dimAll(opts?: LightSetOptions & {
40
+ factor?: number;
41
+ }): void;
42
+ /** Fade every light back to the intensity captured before the last dim. */
43
+ restore(opts?: LightSetOptions): void;
44
+ /** Advance active fades. Called once per frame with the frame delta in ms. */
45
+ update(deltaMs: number): void;
46
+ }
47
+ //#endregion
48
+ export { type Light, LightController, type LightShadow, type LightType, buildLight, light, lightData, validateLight };
49
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,172 @@
1
+ import { n as validateLight, t as lightData } from "./data-bkULec9x.mjs";
2
+ import { AmbientLight, DirectionalLight, Group, HemisphereLight, OrthographicCamera, PointLight, SpotLight } from "three";
3
+ //#region src/buildLight.ts
4
+ const DEFAULT_COLOR = "#ffffff";
5
+ const DEFAULT_SHADOW_MAP = 1024;
6
+ /** Enable and configure shadow casting on a light from its `shadow` data. */
7
+ const applyShadow = (light, config) => {
8
+ light.castShadow = true;
9
+ const s = typeof config === "object" ? config : {};
10
+ light.shadow.mapSize.setScalar(s.mapSize ?? DEFAULT_SHADOW_MAP);
11
+ if (s.bias !== void 0) light.shadow.bias = s.bias;
12
+ if (s.normalBias !== void 0) light.shadow.normalBias = s.normalBias;
13
+ const cam = light.shadow.camera;
14
+ if (s.near !== void 0) cam.near = s.near;
15
+ if (s.far !== void 0) cam.far = s.far;
16
+ if (cam instanceof OrthographicCamera && s.size !== void 0) {
17
+ cam.left = -s.size;
18
+ cam.right = s.size;
19
+ cam.top = s.size;
20
+ cam.bottom = -s.size;
21
+ }
22
+ cam.updateProjectionMatrix();
23
+ };
24
+ /**
25
+ * Build the three.js object for a `Light`. Returns a `Group` (even for a single
26
+ * light) so directional/spot lights can carry their `target` in the same node,
27
+ * and so every light is addressed uniformly by `id` for reconciliation.
28
+ */
29
+ function buildLight(light) {
30
+ const color = light.color ?? DEFAULT_COLOR;
31
+ const group = new Group();
32
+ switch (light.type) {
33
+ case "ambient":
34
+ group.add(new AmbientLight(color, light.intensity ?? 1.4));
35
+ break;
36
+ case "hemisphere":
37
+ group.add(new HemisphereLight(color, light.groundColor ?? "#444444", light.intensity ?? 1));
38
+ break;
39
+ case "directional": {
40
+ const l = new DirectionalLight(color, light.intensity ?? 1);
41
+ l.position.set(...light.position);
42
+ l.target.position.set(...light.target ?? [
43
+ 0,
44
+ 0,
45
+ 0
46
+ ]);
47
+ if (light.shadow) applyShadow(l, light.shadow);
48
+ group.add(l, l.target);
49
+ break;
50
+ }
51
+ case "point": {
52
+ const l = new PointLight(color, light.intensity ?? 1, light.distance ?? 0, light.decay ?? 2);
53
+ l.position.set(...light.position);
54
+ if (light.shadow) applyShadow(l, light.shadow);
55
+ group.add(l);
56
+ break;
57
+ }
58
+ case "spot": {
59
+ const l = new SpotLight(color, light.intensity ?? 1, light.distance ?? 0, light.angle ?? Math.PI / 6, light.penumbra ?? 0, light.decay ?? 2);
60
+ l.position.set(...light.position);
61
+ l.target.position.set(...light.target ?? [
62
+ 0,
63
+ 0,
64
+ 0
65
+ ]);
66
+ if (light.shadow) applyShadow(l, light.shadow);
67
+ group.add(l, l.target);
68
+ break;
69
+ }
70
+ }
71
+ group.name = `light:${light.id}`;
72
+ group.userData.scenographyId = light.id;
73
+ return group;
74
+ }
75
+ //#endregion
76
+ //#region src/LightController.ts
77
+ const isThreeLight = (o) => o.isLight === true;
78
+ /**
79
+ * High-level light control surfaced as `ctx.lights`. Works over the live light
80
+ * objects (queried fresh each call so it keeps working across hot-swaps),
81
+ * tweening intensity in the render loop.
82
+ */
83
+ var LightController = class {
84
+ getLights;
85
+ tweens = /* @__PURE__ */ new Map();
86
+ /** Intensity captured before a dim, so `restore` can bring it back. */
87
+ baseline = /* @__PURE__ */ new WeakMap();
88
+ constructor(getLights) {
89
+ this.getLights = getLights;
90
+ }
91
+ lightsOf(group) {
92
+ const found = [];
93
+ group.traverse((o) => {
94
+ if (isThreeLight(o)) found.push(o);
95
+ });
96
+ return found;
97
+ }
98
+ all() {
99
+ return [...this.getLights().values()].flatMap((g) => this.lightsOf(g));
100
+ }
101
+ fade(light, to, duration) {
102
+ if (!duration || duration <= 0) {
103
+ this.tweens.delete(light);
104
+ light.intensity = to;
105
+ return;
106
+ }
107
+ this.tweens.set(light, {
108
+ from: light.intensity,
109
+ to,
110
+ elapsed: 0,
111
+ duration
112
+ });
113
+ }
114
+ /** Set intensity and/or color of every light under `id`. */
115
+ set(id, props, opts = {}) {
116
+ const group = this.getLights().get(id);
117
+ if (!group) return;
118
+ for (const light of this.lightsOf(group)) {
119
+ if (props.color !== void 0) light.color.set(props.color);
120
+ if (props.intensity !== void 0) this.fade(light, props.intensity, opts.duration ?? 0);
121
+ }
122
+ }
123
+ /** Fade every light toward `factor` of its current intensity (default off). */
124
+ dimAll(opts = {}) {
125
+ const factor = opts.factor ?? 0;
126
+ for (const light of this.all()) {
127
+ const base = this.baseline.get(light) ?? light.intensity;
128
+ this.baseline.set(light, base);
129
+ this.fade(light, base * factor, opts.duration ?? 0);
130
+ }
131
+ }
132
+ /** Fade every light back to the intensity captured before the last dim. */
133
+ restore(opts = {}) {
134
+ for (const light of this.all()) {
135
+ const base = this.baseline.get(light);
136
+ if (base === void 0) continue;
137
+ this.fade(light, base, opts.duration ?? 0);
138
+ this.baseline.delete(light);
139
+ }
140
+ }
141
+ /** Advance active fades. Called once per frame with the frame delta in ms. */
142
+ update(deltaMs) {
143
+ for (const [light, t] of this.tweens) {
144
+ t.elapsed += deltaMs;
145
+ const k = t.duration > 0 ? Math.min(t.elapsed / t.duration, 1) : 1;
146
+ light.intensity = t.from + (t.to - t.from) * k;
147
+ if (k >= 1) this.tweens.delete(light);
148
+ }
149
+ }
150
+ };
151
+ //#endregion
152
+ //#region src/plugin.ts
153
+ /**
154
+ * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/
155
+ * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets
156
+ * intensity and color over the live lights, tweening in the render loop.
157
+ */
158
+ const light = () => ({
159
+ data: lightData,
160
+ build: (definition) => ({ object: buildLight(definition) }),
161
+ controller: (elements) => {
162
+ const controller = new LightController(elements);
163
+ return {
164
+ api: controller,
165
+ update: (deltaMs) => controller.update(deltaMs)
166
+ };
167
+ }
168
+ });
169
+ //#endregion
170
+ export { LightController, buildLight, light, lightData, validateLight };
171
+
172
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/buildLight.ts","../src/LightController.ts","../src/plugin.ts"],"sourcesContent":["import {\n AmbientLight,\n DirectionalLight,\n Group,\n HemisphereLight,\n OrthographicCamera,\n PointLight,\n SpotLight,\n type Object3D,\n} from \"three\";\nimport type { Light, LightShadow } from \"./data\";\n\nconst DEFAULT_COLOR = \"#ffffff\";\nconst DEFAULT_SHADOW_MAP = 1024;\n\n/** Enable and configure shadow casting on a light from its `shadow` data. */\nconst applyShadow = (\n light: DirectionalLight | SpotLight | PointLight,\n config: boolean | LightShadow,\n): void => {\n light.castShadow = true;\n const s: LightShadow = typeof config === \"object\" ? config : {};\n light.shadow.mapSize.setScalar(s.mapSize ?? DEFAULT_SHADOW_MAP);\n if (s.bias !== undefined) light.shadow.bias = s.bias;\n if (s.normalBias !== undefined) light.shadow.normalBias = s.normalBias;\n\n const cam = light.shadow.camera;\n if (s.near !== undefined) cam.near = s.near;\n if (s.far !== undefined) cam.far = s.far;\n // Directional lights use an orthographic frustum; size it to the lit area.\n if (cam instanceof OrthographicCamera && s.size !== undefined) {\n cam.left = -s.size;\n cam.right = s.size;\n cam.top = s.size;\n cam.bottom = -s.size;\n }\n cam.updateProjectionMatrix();\n};\n\n/**\n * Build the three.js object for a `Light`. Returns a `Group` (even for a single\n * light) so directional/spot lights can carry their `target` in the same node,\n * and so every light is addressed uniformly by `id` for reconciliation.\n */\nexport function buildLight(light: Light): Object3D {\n const color = light.color ?? DEFAULT_COLOR;\n const group = new Group();\n\n switch (light.type) {\n case \"ambient\":\n group.add(new AmbientLight(color, light.intensity ?? 1.4));\n break;\n case \"hemisphere\":\n group.add(\n new HemisphereLight(color, light.groundColor ?? \"#444444\", light.intensity ?? 1),\n );\n break;\n case \"directional\": {\n const l = new DirectionalLight(color, light.intensity ?? 1);\n l.position.set(...light.position);\n l.target.position.set(...(light.target ?? [0, 0, 0]));\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l, l.target);\n break;\n }\n case \"point\": {\n const l = new PointLight(color, light.intensity ?? 1, light.distance ?? 0, light.decay ?? 2);\n l.position.set(...light.position);\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l);\n break;\n }\n case \"spot\": {\n const l = new SpotLight(\n color,\n light.intensity ?? 1,\n light.distance ?? 0,\n light.angle ?? Math.PI / 6,\n light.penumbra ?? 0,\n light.decay ?? 2,\n );\n l.position.set(...light.position);\n l.target.position.set(...(light.target ?? [0, 0, 0]));\n if (light.shadow) applyShadow(l, light.shadow);\n group.add(l, l.target);\n break;\n }\n }\n\n group.name = `light:${light.id}`;\n group.userData.scenographyId = light.id;\n return group;\n}\n","import type { LightSet, LightSetOptions, LightsApi } from \"@scenography/engine\";\nimport type { Color, Light as ThreeLight, Object3D } from \"three\";\n\ninterface Tween {\n from: number;\n to: number;\n elapsed: number;\n duration: number;\n}\n\nconst isThreeLight = (o: Object3D): o is ThreeLight => (o as ThreeLight).isLight === true;\n\n/**\n * High-level light control surfaced as `ctx.lights`. Works over the live light\n * objects (queried fresh each call so it keeps working across hot-swaps),\n * tweening intensity in the render loop.\n */\nexport class LightController implements LightsApi {\n private readonly getLights: () => ReadonlyMap<string, Object3D>;\n private readonly tweens = new Map<ThreeLight, Tween>();\n /** Intensity captured before a dim, so `restore` can bring it back. */\n private readonly baseline = new WeakMap<ThreeLight, number>();\n\n constructor(getLights: () => ReadonlyMap<string, Object3D>) {\n this.getLights = getLights;\n }\n\n private lightsOf(group: Object3D): ThreeLight[] {\n const found: ThreeLight[] = [];\n group.traverse((o) => {\n if (isThreeLight(o)) found.push(o);\n });\n return found;\n }\n\n private all(): ThreeLight[] {\n return [...this.getLights().values()].flatMap((g) => this.lightsOf(g));\n }\n\n private fade(light: ThreeLight, to: number, duration: number): void {\n if (!duration || duration <= 0) {\n this.tweens.delete(light);\n light.intensity = to;\n return;\n }\n this.tweens.set(light, { from: light.intensity, to, elapsed: 0, duration });\n }\n\n /** Set intensity and/or color of every light under `id`. */\n set(id: string, props: LightSet, opts: LightSetOptions = {}): void {\n const group = this.getLights().get(id);\n if (!group) return;\n for (const light of this.lightsOf(group)) {\n if (props.color !== undefined) (light.color as Color).set(props.color);\n if (props.intensity !== undefined) this.fade(light, props.intensity, opts.duration ?? 0);\n }\n }\n\n /** Fade every light toward `factor` of its current intensity (default off). */\n dimAll(opts: LightSetOptions & { factor?: number } = {}): void {\n const factor = opts.factor ?? 0;\n for (const light of this.all()) {\n const base = this.baseline.get(light) ?? light.intensity;\n this.baseline.set(light, base);\n this.fade(light, base * factor, opts.duration ?? 0);\n }\n }\n\n /** Fade every light back to the intensity captured before the last dim. */\n restore(opts: LightSetOptions = {}): void {\n for (const light of this.all()) {\n const base = this.baseline.get(light);\n if (base === undefined) continue;\n this.fade(light, base, opts.duration ?? 0);\n this.baseline.delete(light);\n }\n }\n\n /** Advance active fades. Called once per frame with the frame delta in ms. */\n update(deltaMs: number): void {\n for (const [light, t] of this.tweens) {\n t.elapsed += deltaMs;\n const k = t.duration > 0 ? Math.min(t.elapsed / t.duration, 1) : 1;\n light.intensity = t.from + (t.to - t.from) * k;\n if (k >= 1) this.tweens.delete(light);\n }\n }\n}\n","import type { EnginePlugin } from \"@scenography/engine\";\n\nimport { buildLight } from \"./buildLight\";\nimport { LightController } from \"./LightController\";\nimport { lightData, type Light } from \"./data\";\n\n/**\n * The light plugin: builds each `Light` (ambient/hemisphere/directional/point/\n * spot, with shadows) and exposes `ctx.lights` — a controller that dims/sets\n * intensity and color over the live lights, tweening in the render loop.\n */\nexport const light = (): EnginePlugin<Light> => ({\n data: lightData,\n build: (definition) => ({ object: buildLight(definition) }),\n controller: (elements) => {\n const controller = new LightController(elements);\n return { api: controller, update: (deltaMs) => controller.update(deltaMs) };\n },\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@scenography/plugin-light",
3
+ "version": "0.2.0",
4
+ "description": "Light plugin for scenography: ambient/hemisphere/directional/point/spot lights with shadows, and ctx.lights runtime control.",
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
+ }