mudlet-map-renderer 2.3.1 → 2.5.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,12 @@
1
+ import { RoomLens } from './RoomLens';
2
+ /**
3
+ * Wrap a lens so Mudlet-hidden rooms (`settings.hiddenRooms === "hide"`) count
4
+ * as not-visible: the room filter drops them via `isVisible`, and any exit
5
+ * touching one is treated as `"hidden"` via `getExitTreatment`. This reuses the
6
+ * existing lens machinery so every consumer (scene build, current-room overlay)
7
+ * hides hidden rooms and their exits identically.
8
+ *
9
+ * Returns `lens` unchanged when `hideHidden` is false (the "show"/"faded" modes
10
+ * keep hidden rooms visible — "faded" fades them in the room pass instead).
11
+ */
12
+ export declare function hiddenAwareLens(lens: RoomLens, hideHidden: boolean): RoomLens;
@@ -1,4 +1,4 @@
1
- import { E as e, T as t, _ as n, a as r, b as i, g as a, h as o, k as s, m as c, o as l, r as u, t as d, v as f, w as p, x as m, y as h } from "./flushSceneShapes-Duhi0OQv.js";
1
+ import { A as e, C as t, E as n, P as r, S as i, T as a, a as o, h as s, j as c, k as l, m as u, o as d, r as f, t as p, w as m, x as h } from "./flushSceneShapes-c7ttw7D-.js";
2
2
  import g from "konva";
3
3
  //#region src/rendering/offscreen/serializeTransform.ts
4
4
  function _(e) {
@@ -26,33 +26,33 @@ function b() {
26
26
  return typeof document < "u" && typeof HTMLCanvasElement < "u" && typeof HTMLCanvasElement.prototype.transferControlToOffscreen == "function";
27
27
  }
28
28
  var x = class {
29
- constructor(n, i, a = {}) {
30
- this.hitTester = new l(), this.currentStyle = r, this._coordinateTransform = t, this.coordinateInverse = t, this.lastHitShapes = [], this.boundsByShape = /* @__PURE__ */ new Map(), this.sceneOverlays = /* @__PURE__ */ new Map(), this.viewportSubscribers = /* @__PURE__ */ new Set(), this.liveEffects = /* @__PURE__ */ new Map(), this.pendingExports = /* @__PURE__ */ new Map(), this.exportSeq = 0, this.destroyed = !1, this.state = n, this.container = i, this.pipeline = new o(n.mapReader, n.settings), this.dpr = a.devicePixelRatio ?? (typeof devicePixelRatio < "u" ? devicePixelRatio : 1), this.transport = a.transport ?? a.createTransport?.();
31
- let u = i?.clientWidth ?? 1, d = i?.clientHeight ?? 1;
32
- if (this.camera = new e(u, d), this.events = new s(i), this.culling = new p(n.settings, () => {
29
+ constructor(t, n, i = {}) {
30
+ this.hitTester = new d(), this.currentStyle = o, this._coordinateTransform = e, this.coordinateInverse = e, this.lastHitShapes = [], this.boundsByShape = /* @__PURE__ */ new Map(), this.sceneOverlays = /* @__PURE__ */ new Map(), this.viewportSubscribers = /* @__PURE__ */ new Set(), this.liveEffects = /* @__PURE__ */ new Map(), this.pendingExports = /* @__PURE__ */ new Map(), this.exportSeq = 0, this.destroyed = !1, this.state = t, this.container = n, this.pipeline = new s(t.mapReader, t.settings), this.dpr = i.devicePixelRatio ?? (typeof devicePixelRatio < "u" ? devicePixelRatio : 1), this.transport = i.transport ?? i.createTransport?.();
31
+ let a = n?.clientWidth ?? 1, f = n?.clientHeight ?? 1;
32
+ if (this.camera = new c(a, f), this.events = new r(n), this.culling = new l(t.settings, () => {
33
33
  this.postSettings(), this.postCamera();
34
34
  }), this.transport) {
35
35
  let e = (e) => this.handleWorkerMessage(e.data);
36
36
  this.transport.addEventListener ? this.transport.addEventListener("message", e) : this.transport.onmessage = e;
37
37
  }
38
- if (i && this.transport && b()) {
39
- getComputedStyle(i).position === "static" && (i.style.position = "relative");
38
+ if (n && this.transport && b()) {
39
+ getComputedStyle(n).position === "static" && (n.style.position = "relative");
40
40
  let e = document.createElement("canvas");
41
- e.style.position = "absolute", e.style.inset = "0", e.style.width = "100%", e.style.height = "100%", e.style.display = "block", i.appendChild(e), i.style.backgroundColor = n.settings.backgroundColor;
42
- let t = e.transferControlToOffscreen();
41
+ e.style.position = "absolute", e.style.inset = "0", e.style.width = "100%", e.style.height = "100%", e.style.display = "block", n.appendChild(e), n.style.backgroundColor = t.settings.backgroundColor;
42
+ let r = e.transferControlToOffscreen();
43
43
  this.transport.postMessage({
44
44
  type: "init",
45
- canvas: t,
46
- width: u,
47
- height: d,
45
+ canvas: r,
46
+ width: a,
47
+ height: f,
48
48
  dpr: this.dpr,
49
- settings: n.settings
50
- }, [t]), this.interactionHandler = new c(i, this.camera, n, {
51
- clientToMapPoint: (e, t) => this.camera.clientToMapPoint(e, t, i.getBoundingClientRect()),
49
+ settings: t.settings
50
+ }, [r]), this.interactionHandler = new u(n, this.camera, t, {
51
+ clientToMapPoint: (e, t) => this.camera.clientToMapPoint(e, t, n.getBoundingClientRect()),
52
52
  pickAtPoint: (e, t) => this.hitTester.pick(e, t)
53
53
  }, this.events);
54
54
  }
55
- this.cameraChangeHandler = () => this.onCameraChange(), this.camera.on("change", this.cameraChangeHandler), this.applyStyleTransforms(), this.subscribeToState(n), this.postCamera();
55
+ this.cameraChangeHandler = () => this.onCameraChange(), this.camera.on("change", this.cameraChangeHandler), this.applyStyleTransforms(), this.subscribeToState(t), this.postCamera();
56
56
  }
57
57
  get coordinateTransform() {
58
58
  return this._coordinateTransform;
@@ -109,7 +109,7 @@ var x = class {
109
109
  postOverlays() {
110
110
  let e = this.styleContext(), t = [];
111
111
  for (let n of this.buildCurrentRoomOverlayShapes()) t.push(...this.styleOne(n, e, !1));
112
- for (let n of d(this.state)) t.push(...this.styleOne(n, e, !1));
112
+ for (let n of p(this.state)) t.push(...this.styleOne(n, e, !1));
113
113
  for (let n of this.sceneOverlays.values()) {
114
114
  let r = n.render(this.state, this.camera.getViewportBounds());
115
115
  if (!r) continue;
@@ -128,20 +128,20 @@ var x = class {
128
128
  };
129
129
  }
130
130
  styleOne(e, t, n) {
131
- return n || this.currentStyle === r ? [e] : u([e], this.currentStyle, t);
131
+ return n || this.currentStyle === o ? [e] : f([e], this.currentStyle, t);
132
132
  }
133
133
  styleShapes(e, t) {
134
- return this.currentStyle === r ? e : u(e, this.currentStyle, t);
134
+ return this.currentStyle === o ? e : f(e, this.currentStyle, t);
135
135
  }
136
136
  styleLayer(e, t) {
137
- let n = [], i = [];
138
- for (let a of e) {
139
- let e = this.boundsByShape.get(a) ?? null, o = this.currentStyle === r ? [a] : u([a], this.currentStyle, t);
140
- for (let t of o) n.push(t), i.push(e);
137
+ let n = [], r = [];
138
+ for (let i of e) {
139
+ let e = this.boundsByShape.get(i) ?? null, a = this.currentStyle === o ? [i] : f([i], this.currentStyle, t);
140
+ for (let t of a) n.push(t), r.push(e);
141
141
  }
142
142
  return {
143
143
  shapes: n,
144
- bounds: i
144
+ bounds: r
145
145
  };
146
146
  }
147
147
  refresh() {
@@ -171,38 +171,38 @@ var x = class {
171
171
  return t;
172
172
  }
173
173
  buildCurrentRoomOverlayShapes() {
174
- let e = this.state, t = e.positionRoomId;
175
- if (t === void 0) return [];
176
- let r = e.mapReader.getRoom(t);
177
- if (!r || r.area !== e.currentArea || r.z !== e.currentZIndex) return [];
178
- let o = e.settings;
179
- if (!o.highlightCurrentRoom) return [];
180
- let s = [], c = this.pipeline.exitRenderer, l = e.lens, u = /* @__PURE__ */ new Map();
181
- if (u.set(r.id, r), e.currentAreaInstance && e.currentZIndex !== void 0) {
182
- let t = e.currentAreaInstance.getLinkExits(e.currentZIndex).filter((e) => e.a === r.id || e.b === r.id);
174
+ let e = this.state, r = e.positionRoomId;
175
+ if (r === void 0) return [];
176
+ let o = e.mapReader.getRoom(r);
177
+ if (!o || o.area !== e.currentArea || o.z !== e.currentZIndex) return [];
178
+ let s = e.settings;
179
+ if (!s.highlightCurrentRoom) return [];
180
+ let c = [], l = this.pipeline.exitRenderer, u = e.lens, d = /* @__PURE__ */ new Map();
181
+ if (d.set(o.id, o), e.currentAreaInstance && e.currentZIndex !== void 0) {
182
+ let t = e.currentAreaInstance.getLinkExits(e.currentZIndex).filter((e) => e.a === o.id || e.b === o.id);
183
183
  for (let n of t) {
184
- let t = c.renderDataWithColor(n, y, e.currentZIndex);
185
- t && s.push(this.pipeline.buildExitShape(t));
184
+ let t = l.renderDataWithColor(n, y, e.currentZIndex);
185
+ t && c.push(this.pipeline.buildExitShape(t));
186
186
  }
187
187
  }
188
- for (let e of i(r, o, y)) s.push(n(e, r.id));
189
- for (let e of m(r, o, y)) s.push(a(e));
190
- return [...Object.values(r.exits), ...Object.values(r.specialExits)].forEach((t) => {
188
+ for (let e of a(o, s, y)) c.push(i(e, o.id));
189
+ for (let e of n(o, s, y)) c.push(h(e));
190
+ return [...Object.values(o.exits), ...Object.values(o.specialExits)].forEach((t) => {
191
191
  let n = e.mapReader.getRoom(t);
192
- n && n.area === e.currentArea && n.z === e.currentZIndex && l.isVisible(n) && u.set(n.id, n);
193
- }), u.forEach((t, n) => {
194
- let i = n === r.id, a = h(t, e.mapReader, o, {
195
- strokeOverride: i ? y : o.lineColor,
192
+ n && n.area === e.currentArea && n.z === e.currentZIndex && u.isVisible(n) && d.set(n.id, n);
193
+ }), d.forEach((n, r) => {
194
+ let i = r === o.id, a = m(n, e.mapReader, s, {
195
+ strokeOverride: i ? y : s.lineColor,
196
196
  flatPipeline: !0
197
197
  });
198
- a.children.push(...f(t, e.mapReader, o)), s.push(a);
199
- }), s;
198
+ a.children.push(...t(n, e.mapReader, s)), c.push(a);
199
+ }), c;
200
200
  }
201
201
  setStyle(e) {
202
202
  this.currentStyle = e, this.lastHitShapes = [], this.hitTester.clear(), this.applyStyleTransforms(), this.refresh();
203
203
  }
204
204
  applyStyleTransforms() {
205
- let e = this.currentStyle, n = e.worldToScene ? (t, n) => e.worldToScene(t, n) : t, r = e.sceneToWorld ? (t, n) => e.sceneToWorld(t, n) : t, i = this.coordinateInverse, a = e.sceneLayerOffset ? (t) => e.sceneLayerOffset(t) : void 0;
205
+ let t = this.currentStyle, n = t.worldToScene ? (e, n) => t.worldToScene(e, n) : e, r = t.sceneToWorld ? (e, n) => t.sceneToWorld(e, n) : e, i = this.coordinateInverse, a = t.sceneLayerOffset ? (e) => t.sceneLayerOffset(e) : void 0;
206
206
  this._coordinateTransform = n, this.coordinateInverse = r, this.coordinateLayerOffset = a, this.culling.setCoordinateTransform(n), this.lastHitShapes.length > 0 && this.hitTester.build(this.lastHitShapes, this.state.settings.roomSize, n, a);
207
207
  let o = this.camera.getScale(), s = this.camera.width / 2, c = this.camera.height / 2, l = i((s - this.camera.position.x) / o, (c - this.camera.position.y) / o), u = n(l.x, l.y);
208
208
  this.camera.position = {
@@ -1,5 +1,5 @@
1
1
  import { default as Konva } from 'konva';
2
- import { FillStyle } from '../scene/Shape';
2
+ import { FillStyle, LayerId } from '../scene/Shape';
3
3
  type RectCommand = {
4
4
  type: 'rect';
5
5
  x: number;
@@ -78,6 +78,8 @@ export declare class RecordingGroupNode {
78
78
  y: number;
79
79
  _visible: boolean;
80
80
  noScaling: boolean;
81
+ /** Scene layer this group belongs to; drives coalesced draw ordering. */
82
+ layer?: LayerId;
81
83
  readonly commands: RecordingDrawCommand[];
82
84
  /** Lazily created when this group is materialized for a Konva.Layer. */
83
85
  _konvaGroup?: Konva.Group;
@@ -107,20 +109,33 @@ export type DrawEntry = {
107
109
  x: number;
108
110
  y: number;
109
111
  noScaling: boolean;
112
+ layer?: LayerId;
110
113
  readonly commands: RecordingDrawCommand[];
111
114
  visible: boolean;
112
115
  };
113
- /**
114
- * LayerNode backed by a single Konva.Shape whose sceneFunc replays
115
- * {@link DrawEntry} objects. Used for the main scene layer where individual
116
- * shapes need cull toggling.
117
- */
118
116
  export declare class DrawCommandLayerNode {
117
+ private readonly coalesce;
119
118
  private readonly entries;
120
119
  private readonly nodeToEntry;
121
120
  private readonly konvaLayer;
122
121
  private konvaShape;
123
- constructor(konvaLayer: Konva.Layer);
122
+ /** Reusable bucket map for coalesced draws — cleared per frame, never reallocated. */
123
+ private readonly buckets;
124
+ constructor(konvaLayer: Konva.Layer, coalesce?: () => boolean);
125
+ /** Original path: one setTransform + replay per visible entry, in order. */
126
+ private drawPerEntry;
127
+ /**
128
+ * Fast path: same-style primitives are accumulated into one path per style
129
+ * and drawn with a single `fill()`/`stroke()`. Done in two sweeps — link
130
+ * entries (exit lines) first, then room entries — so rooms stay painted on
131
+ * top of exits, matching the per-entry insertion order. Within a sweep,
132
+ * non-batchable entries (one-way arrows, doors, symbols, emboss, inner-exit
133
+ * triangles…) replay per-entry; batched primitives flush afterwards. Exit
134
+ * lines and room bodies don't meaningfully overlap same-layer siblings, so
135
+ * reordering them within a sweep is visually immaterial.
136
+ */
137
+ private drawCoalesced;
138
+ private sweep;
124
139
  addNode(node: RecordingGroupNode): void;
125
140
  /** Return the DrawEntry created when `node` was added, or undefined if not found. */
126
141
  getEntry(node: RecordingGroupNode): DrawEntry | undefined;
@@ -53,6 +53,13 @@ export declare class KonvaRenderBackend implements InteractiveBackend {
53
53
  private positionMarker?;
54
54
  private highlightShapes;
55
55
  private pathShapes;
56
+ /**
57
+ * Projected positions of the current build's spilled neighbouring-area
58
+ * rooms (roomId → current-area-frame x/y). Empty when neighbour spill is
59
+ * off or yields nothing. Drives {@link overlayReader} so highlights/paths
60
+ * on spilled rooms land at their projected spots.
61
+ */
62
+ private spilledRoomPositions;
56
63
  private currentRoomOverlay;
57
64
  private interactionHandler?;
58
65
  private origSetSize?;
@@ -94,6 +101,19 @@ export declare class KonvaRenderBackend implements InteractiveBackend {
94
101
  private clearSceneOverlayNodes;
95
102
  private subscribeToState;
96
103
  private buildScene;
104
+ /**
105
+ * Compute neighbouring-area spill for the current build, or `undefined` when
106
+ * the feature is off / the player isn't on this area+plane. Driven by the
107
+ * live player position so it updates as the player nears a boundary.
108
+ */
109
+ private computeNeighborSpill;
110
+ /**
111
+ * The map reader the overlay code (highlights, paths) should use. When
112
+ * neighbour spill produced rooms, this is a {@link ProjectedMapReader} that
113
+ * makes those spilled rooms look like current-area rooms at their projected
114
+ * positions; otherwise it's the plain reader.
115
+ */
116
+ private overlayReader;
97
117
  /**
98
118
  * Run the active {@link Style} over `shape`, walk the (possibly multi-shape)
99
119
  * result into a single {@link RecordingGroupNode}, and add it to `layerNode`.
@@ -8,6 +8,7 @@ import { GroupShape, Shape } from '../scene/Shape';
8
8
  import { IMapReader } from '../reader/MapReader';
9
9
  import { RoomLens } from '../lens/RoomLens';
10
10
  import { ExitDrawData } from '../ExitRenderer';
11
+ import { NeighborSpill } from '../scene/NeighborProjector';
11
12
  export interface CullStats {
12
13
  visibleRooms: number;
13
14
  totalRooms: number;
@@ -39,7 +40,7 @@ export declare class SceneManager {
39
40
  get drawnStubs(): readonly DrawnStubEntry[];
40
41
  get areaExitHitZones(): readonly AreaExitHitZone[];
41
42
  get hitShapes(): readonly Shape[];
42
- rebuild(area: IArea, plane: IPlane, zIndex: number, lens?: RoomLens): SceneBuildResult;
43
+ rebuild(area: IArea, plane: IPlane, zIndex: number, lens?: RoomLens, spill?: NeighborSpill): SceneBuildResult;
43
44
  buildExitShape(data: ExitDrawData): GroupShape;
44
45
  reset(): void;
45
46
  resetPipeline(mapReader: IMapReader): void;
@@ -0,0 +1,82 @@
1
+ import { IMapReader } from '../reader/MapReader';
2
+ import { IArea } from '../reader/Area';
3
+ /**
4
+ * Clone a room placed at projected `(x, y)` in the current area+plane, shifting
5
+ * its custom-line points by the same delta so they follow the room. Custom-line
6
+ * points are stored with world-y negated (`world = -point.y`), so a world shift
7
+ * of `dy` maps to `point.y -= dy`.
8
+ */
9
+ export declare function projectRoom(room: MapData.Room, x: number, y: number, area: number, z: number): MapData.Room;
10
+ /** One neighbouring-area room, with its position projected into the current area's frame. */
11
+ export interface ProjectedRoom {
12
+ /** The real room (its `id` is preserved so the rendered shape stays clickable). */
13
+ room: MapData.Room;
14
+ /** Projected X in the current area's coordinate space. */
15
+ x: number;
16
+ /** Projected Y in the current area's coordinate space. */
17
+ y: number;
18
+ }
19
+ /** A connector line between two projected room centres (current-area frame). */
20
+ export interface ProjectedEdge {
21
+ ax: number;
22
+ ay: number;
23
+ bx: number;
24
+ by: number;
25
+ }
26
+ /** Result of {@link computeNeighborSpill}: faded preview geometry for neighbouring areas. */
27
+ export interface NeighborSpill {
28
+ rooms: ProjectedRoom[];
29
+ edges: ProjectedEdge[];
30
+ }
31
+ /**
32
+ * Walk the exit graph outward from the player's room (BFS, depth ≤ `maxSteps`)
33
+ * and project the positions of rooms that live in a *different* area onto the
34
+ * current area's coordinate plane, so they can be drawn across the boundary.
35
+ *
36
+ * Areas use independent (area-local) coordinates, so we can't trust a
37
+ * neighbour room's raw x/y to align with the current area. Instead we carry a
38
+ * projected position through the BFS:
39
+ * - within one area, we advance by the real coordinate delta between rooms;
40
+ * - crossing a boundary, we place the target one grid step away in the exit's
41
+ * planar direction (the only geometric anchor we have).
42
+ *
43
+ * Only planar exits (n/s/e/w + diagonals) are followed — non-planar links
44
+ * (up/down/in/out, special exits) can't be placed on the plane. Only rooms on
45
+ * the current z-level (`currentZ`) are collected.
46
+ *
47
+ * Returns `undefined` when there's nothing to draw.
48
+ */
49
+ export declare function computeNeighborSpill(mapReader: IMapReader, currentAreaId: number, currentZ: number, playerRoomId: number, maxSteps: number, isVisible?: (room: MapData.Room) => boolean): NeighborSpill | undefined;
50
+ /**
51
+ * Wraps an {@link IMapReader} so that spilled neighbouring-area rooms appear to
52
+ * live in the current area at their projected positions. {@link getRoom}
53
+ * returns a clone of a spilled room with `x`/`y` swapped for the projected
54
+ * coordinates, `area`/`z` rewritten to the current area+plane, and custom-line
55
+ * points shifted by the same delta so they follow the room; every other room and
56
+ * method passes straight through to the base reader.
57
+ *
58
+ * This lets the overlay code (highlights, paths) treat spilled rooms exactly
59
+ * like current-area rooms: existing area/visibility checks pass for them and
60
+ * all coordinate maths lands on the projected positions, with no special cases.
61
+ */
62
+ export declare class ProjectedMapReader implements IMapReader {
63
+ private readonly base;
64
+ private readonly currentArea;
65
+ private readonly currentZ;
66
+ private readonly projected;
67
+ constructor(base: IMapReader, currentArea: number, currentZ: number, projected: Map<number, {
68
+ x: number;
69
+ y: number;
70
+ }>);
71
+ getRoom(roomId: number): MapData.Room;
72
+ getArea(areaId: number): IArea;
73
+ getAreas(): IArea[];
74
+ getRooms(): MapData.Room[];
75
+ getColorValue(envId: number): string;
76
+ getSymbolColor(envId: number, opacity?: number): string;
77
+ }
78
+ /** Build a roomId → projected-position map from a {@link NeighborSpill}. */
79
+ export declare function spillPositionMap(spill: NeighborSpill): Map<number, {
80
+ x: number;
81
+ y: number;
82
+ }>;
@@ -0,0 +1,36 @@
1
+ import { HiddenRoomMode } from '../types/Settings';
2
+ /**
3
+ * userData key Mudlet uses for a room's hidden state on pre-v21 map formats
4
+ * (case-insensitive `"true"`). On v21+ formats Mudlet stores this as a binary
5
+ * `hidden` field instead — which the JSON pipeline does not currently surface,
6
+ * so only maps carrying this fallback key are detected as hidden here.
7
+ */
8
+ export declare const ROOM_UI_HIDDEN = "system.fallback_hidden";
9
+ /** userData key for a room's custom border colour. */
10
+ export declare const ROOM_UI_BORDER_COLOR = "room.ui_borderColor";
11
+ /** userData key for a room's custom border thickness (Mudlet clamps it to 1..10). */
12
+ export declare const ROOM_UI_BORDER_THICKNESS = "room.ui_borderThickness";
13
+ /** Whether Mudlet has marked this room hidden on the 2D map. */
14
+ export declare function isRoomHidden(room: MapData.Room): boolean;
15
+ /**
16
+ * The room's custom border colour as a CSS colour, or `undefined` when none is
17
+ * set. Mudlet stores it as a Qt `#AARRGGBB` string (see {@link qtColorToCss}).
18
+ */
19
+ export declare function getRoomBorderColor(room: MapData.Room): string | undefined;
20
+ /** Opacity applied to hidden rooms in {@link HiddenRoomMode} `"faded"`. */
21
+ export declare const HIDDEN_ROOM_FADE = 0.35;
22
+ /**
23
+ * The {@link layoutRoom} options that render a room's hidden state for the given
24
+ * mode (`{}` when the room isn't hidden / the mode draws it normally). Shared by
25
+ * the scene build and the current-room overlay so a redrawn hidden room keeps
26
+ * its fade / dashed border.
27
+ */
28
+ export declare function hiddenRoomLayoutOptions(room: MapData.Room, mode: HiddenRoomMode): {
29
+ fade?: number;
30
+ dashedBorder?: boolean;
31
+ };
32
+ /**
33
+ * The room's custom border thickness clamped to Mudlet's 1..10 range, or
34
+ * `undefined` when unset or unparseable.
35
+ */
36
+ export declare function getRoomBorderThickness(room: MapData.Room): number | undefined;
@@ -6,6 +6,8 @@ export type RoomColors = {
6
6
  borderWidth: number;
7
7
  symbolColor: string;
8
8
  envColor: string;
9
+ /** True when the room carries an explicit per-room border colour (userData). */
10
+ customBorder: boolean;
9
11
  };
10
12
  export type EmbossEdge = {
11
13
  points: number[];
@@ -10,6 +10,18 @@ export interface RoomLayoutOptions {
10
10
  * as flat diamonds outside the cube footprint).
11
11
  */
12
12
  flatPipeline: boolean;
13
+ /**
14
+ * When set (0..1), the room's fill, border, and symbol colours are drawn at
15
+ * this opacity — used to render hidden rooms faded. Baked into the colour
16
+ * strings so every renderer (Konva, canvas, SVG) honours it identically.
17
+ */
18
+ fade?: number;
19
+ /**
20
+ * Draw the room with a dashed border at full opacity — a more distinct
21
+ * "hidden" marker than {@link fade}. Suppresses emboss (the dashed outline
22
+ * is the signal) and forces a visible border even when borders are off.
23
+ */
24
+ dashedBorder?: boolean;
13
25
  }
14
26
  /**
15
27
  * Pure layout for a single room. Returns a {@link GroupShape} positioned at
@@ -1,6 +1,15 @@
1
1
  export type LabelRenderMode = "image" | "data" | "none";
2
2
  export type CullingMode = "none" | "basic" | "indexed";
3
3
  export type RoomShape = "rectangle" | "circle" | "roundedRectangle";
4
+ /**
5
+ * How to render rooms Mudlet has marked hidden (see {@link isRoomHidden}):
6
+ * - `"hide"` — omit them from the scene entirely (Mudlet's default).
7
+ * - `"show"` — draw them exactly like any other room.
8
+ * - `"faded"` — draw them at reduced opacity so they read as de-emphasised.
9
+ * - `"dashed"` — draw them at full opacity but with a dashed border, a more
10
+ * distinct "hidden" marker than a fade (which can read as just another shade).
11
+ */
12
+ export type HiddenRoomMode = "hide" | "show" | "faded" | "dashed";
4
13
  export type RoomContextMenuEventDetail = {
5
14
  roomId: number;
6
15
  position: {
@@ -171,12 +180,25 @@ export type Settings = {
171
180
  width: number;
172
181
  height: number;
173
182
  } | null;
183
+ /**
184
+ * When true, room bodies that share the same fill/stroke/shape are drawn in
185
+ * a single batched path (one `fill()` + one `stroke()` per style) instead of
186
+ * one draw per room. Big win on large areas with few environment colours;
187
+ * see {@link DrawCommandLayerNode}. Rooms with symbols, emboss, gradient
188
+ * fills, or coloured multi-rings fall back to per-room replay automatically.
189
+ *
190
+ * Off by default: coalescing reorders same-style rooms, which can shift
191
+ * sub-pixel border overlaps where rooms touch. Default: false
192
+ */
193
+ coalesceRooms: boolean;
174
194
  /** How to render room labels: "image" | "data". Default: "image" */
175
195
  labelRenderMode: LabelRenderMode;
176
196
  /** When true, room labels have transparent backgrounds. Default: false */
177
197
  transparentLabels: boolean;
178
198
  /** Shape used to render rooms: "rectangle" | "circle" | "roundedRectangle". Default: "rectangle" */
179
199
  roomShape: RoomShape;
200
+ /** How to render rooms Mudlet has marked hidden: "hide" | "show" | "faded". Default: "hide" */
201
+ hiddenRooms: HiddenRoomMode;
180
202
  /** Style configuration for the player position marker. */
181
203
  playerMarker: PlayerMarkerStyle;
182
204
  /** Style configuration for room highlights (added via {@link MapRenderer.renderHighlight}). */
@@ -209,6 +231,13 @@ export type Settings = {
209
231
  /** Font size (in map units) for area-exit labels. Padding, corner radius, and stroke
210
232
  * scale proportionally. Default: 0.3 */
211
233
  areaExitLabelFontSize: number;
234
+ /** When true, rooms from neighbouring areas reachable within {@link neighborSpillDistance}
235
+ * steps of the player are drawn (faded) across the boundary, projected into the current
236
+ * area's coordinate space. Requires a known player position. Default: false */
237
+ neighborSpill: boolean;
238
+ /** Max number of steps from the player's room to spill neighbouring-area rooms across a
239
+ * boundary (BFS depth over planar exits). Default: 20 */
240
+ neighborSpillDistance: number;
212
241
  };
213
242
  /** Creates a new Settings object with default values. */
214
243
  export declare function createSettings(): Settings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mudlet-map-renderer",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "url": "https://github.com/Delwing/mudlet-map-renderer"
@@ -46,7 +46,7 @@
46
46
  "node-dijkstra": "^2.5.1"
47
47
  },
48
48
  "peerDependencies": {
49
- "mudlet-map-binary-reader": ">=0.9.0"
49
+ "mudlet-map-binary-reader": ">=1.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "mudlet-map-binary-reader": {
@@ -57,8 +57,10 @@
57
57
  "@playwright/test": "^1.59.1",
58
58
  "@types/node": "^25.6.0",
59
59
  "@types/node-dijkstra": "^2.5.6",
60
+ "@types/three": "^0.184.1",
60
61
  "canvas": "^3.2.3",
61
- "mudlet-map-binary-reader": "^0.9.0",
62
+ "mudlet-map-binary-reader": "^1.0.1",
63
+ "three": "^0.184.0",
62
64
  "tslib": "^2.8.1",
63
65
  "tsx": "^4.21.0",
64
66
  "typescript": "^6.0.2",