@pascal-app/viewer 1.0.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -19
- package/dist/components/viewer/drone-controller.d.ts.map +1 -1
- package/dist/components/viewer/drone-controller.js +1 -5
- package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
- package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
- package/dist/components/viewer/first-person/build-collider-world.js +426 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
- package/dist/components/viewer/first-person/ground-holes.js +116 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
- package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
- package/dist/components/viewer/first-person/terrain-collider.js +175 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +2 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +8 -4
- package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
- package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +22 -1
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
- package/dist/components/viewer/use-daylight-rooms.js +75 -33
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/room-context.d.ts.map +1 -1
- package/dist/lib/daylight/room-context.js +3 -2
- package/dist/lib/daylight/room-map.d.ts +149 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +813 -0
- package/dist/lib/daylight/room-scene.d.ts +4 -2
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +81 -40
- package/dist/lib/daylight/rooms.d.ts +71 -32
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +371 -286
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +7 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +14 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +4 -3
- package/dist/lib/typing-target.d.ts +2 -0
- package/dist/lib/typing-target.d.ts.map +1 -0
- package/dist/lib/typing-target.js +7 -0
- package/dist/lib/viewer-config.d.ts +27 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +23 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/sfx/audio-settings.d.ts +81 -0
- package/dist/sfx/audio-settings.d.ts.map +1 -0
- package/dist/sfx/audio-settings.js +166 -0
- package/dist/sfx/index.d.ts +6 -0
- package/dist/sfx/index.d.ts.map +1 -0
- package/dist/sfx/index.js +5 -0
- package/dist/sfx/interaction-sounds.d.ts +7 -0
- package/dist/sfx/interaction-sounds.d.ts.map +1 -0
- package/dist/sfx/interaction-sounds.js +39 -0
- package/dist/sfx/player.d.ts +31 -0
- package/dist/sfx/player.d.ts.map +1 -0
- package/dist/sfx/player.js +255 -0
- package/dist/sfx/sounds.d.ts +60 -0
- package/dist/sfx/sounds.d.ts.map +1 -0
- package/dist/sfx/sounds.js +25 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
- package/dist/sfx/use-viewer-ui-sounds.js +42 -0
- package/dist/store/use-first-person-hud.d.ts +13 -0
- package/dist/store/use-first-person-hud.d.ts.map +1 -0
- package/dist/store/use-first-person-hud.js +21 -0
- package/dist/store/use-viewer.d.ts +2 -56
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +3 -1
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +134 -78
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +56 -10
- package/dist/ui/components/action-button.d.ts +14 -0
- package/dist/ui/components/action-button.d.ts.map +1 -0
- package/dist/ui/components/action-button.js +21 -0
- package/dist/ui/components/action-menu-placement.d.ts +7 -0
- package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
- package/dist/ui/components/action-menu-placement.js +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
- package/dist/ui/components/floorplan-compass-button.js +22 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
- package/dist/ui/components/follow-pascal-toggle.js +21 -0
- package/dist/ui/components/localized-html.d.ts +9 -0
- package/dist/ui/components/localized-html.d.ts.map +1 -0
- package/dist/ui/components/localized-html.js +16 -0
- package/dist/ui/components/render-settings-menu.d.ts +9 -0
- package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
- package/dist/ui/components/render-settings-menu.js +145 -0
- package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
- package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-highlight.js +56 -0
- package/dist/ui/components/rooms/room-labels.d.ts +278 -0
- package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-labels.js +1295 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
- package/dist/ui/components/rooms/viewer-rooms.js +123 -0
- package/dist/ui/components/scene-loader.d.ts +18 -0
- package/dist/ui/components/scene-loader.d.ts.map +1 -0
- package/dist/ui/components/scene-loader.js +29 -0
- package/dist/ui/components/sky-control.d.ts +37 -0
- package/dist/ui/components/sky-control.d.ts.map +1 -0
- package/dist/ui/components/sky-control.js +382 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
- package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
- package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
- package/dist/ui/components/viewer-controls-bar.js +130 -0
- package/dist/ui/components/viewer-hover-label.d.ts +14 -0
- package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
- package/dist/ui/components/viewer-hover-label.js +28 -0
- package/dist/ui/components/viewer-scene-header.d.ts +34 -0
- package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
- package/dist/ui/components/viewer-scene-header.js +92 -0
- package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
- package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
- package/dist/ui/components/viewer-selection-manager.js +116 -0
- package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
- package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-modes.js +42 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-switcher.js +16 -0
- package/dist/ui/components/viewer-stage.d.ts +25 -0
- package/dist/ui/components/viewer-stage.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage.js +107 -0
- package/dist/ui/components/walkthrough-hud.d.ts +29 -0
- package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
- package/dist/ui/components/walkthrough-hud.js +55 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview.js +700 -0
- package/dist/ui/hooks/use-mobile.d.ts +2 -0
- package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
- package/dist/ui/hooks/use-mobile.js +14 -0
- package/dist/ui/index.d.ts +49 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/lib/below-grade.d.ts +38 -0
- package/dist/ui/lib/below-grade.d.ts.map +1 -0
- package/dist/ui/lib/below-grade.js +102 -0
- package/dist/ui/lib/camera-pose.d.ts +19 -0
- package/dist/ui/lib/camera-pose.d.ts.map +1 -0
- package/dist/ui/lib/camera-pose.js +113 -0
- package/dist/ui/lib/cn.d.ts +3 -0
- package/dist/ui/lib/cn.d.ts.map +1 -0
- package/dist/ui/lib/cn.js +5 -0
- package/dist/ui/lib/level-name.d.ts +10 -0
- package/dist/ui/lib/level-name.d.ts.map +1 -0
- package/dist/ui/lib/level-name.js +33 -0
- package/dist/ui/lib/node-display-label.d.ts +8 -0
- package/dist/ui/lib/node-display-label.d.ts.map +1 -0
- package/dist/ui/lib/node-display-label.js +36 -0
- package/dist/ui/lib/room-area.d.ts +3 -0
- package/dist/ui/lib/room-area.d.ts.map +1 -0
- package/dist/ui/lib/room-area.js +8 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
- package/dist/ui/lib/room-assembly-overlay.js +222 -0
- package/dist/ui/lib/room-name-messages.d.ts +28 -0
- package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
- package/dist/ui/lib/room-name-messages.js +112 -0
- package/dist/ui/lib/room-selection.d.ts +58 -0
- package/dist/ui/lib/room-selection.d.ts.map +1 -0
- package/dist/ui/lib/room-selection.js +297 -0
- package/dist/ui/lib/scene-theme-name.d.ts +4 -0
- package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
- package/dist/ui/lib/scene-theme-name.js +22 -0
- package/dist/ui/lib/selection-modifiers.d.ts +20 -0
- package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
- package/dist/ui/lib/selection-modifiers.js +15 -0
- package/dist/ui/lib/viewer-selection.d.ts +132 -0
- package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
- package/dist/ui/lib/viewer-selection.js +462 -0
- package/dist/ui/lib/zone-content.d.ts +6 -0
- package/dist/ui/lib/zone-content.d.ts.map +1 -0
- package/dist/ui/lib/zone-content.js +96 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.d.ts.map +1 -0
- package/dist/ui/primitives/button.js +35 -0
- package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
- package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/ui/primitives/dropdown-menu.js +53 -0
- package/dist/ui/primitives/popover.d.ts +8 -0
- package/dist/ui/primitives/popover.d.ts.map +1 -0
- package/dist/ui/primitives/popover.js +17 -0
- package/dist/ui/primitives/switch.d.ts +5 -0
- package/dist/ui/primitives/switch.d.ts.map +1 -0
- package/dist/ui/primitives/switch.js +8 -0
- package/dist/ui/primitives/tooltip.d.ts +8 -0
- package/dist/ui/primitives/tooltip.d.ts.map +1 -0
- package/dist/ui/primitives/tooltip.js +17 -0
- package/dist/ui/store/camera-pose-store.d.ts +17 -0
- package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
- package/dist/ui/store/camera-pose-store.js +15 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
- package/dist/ui/store/navigation-sync-pose-store.js +16 -0
- package/dist/ui/store/use-follow-pascal.d.ts +30 -0
- package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
- package/dist/ui/store/use-follow-pascal.js +28 -0
- package/dist/ui/theme.css +339 -0
- package/package.json +34 -7
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import { DataTexture, FloatType, NearestFilter, RGBAFormat, RGFormat, UnsignedByteType, Vector2, } from 'three';
|
|
2
|
+
import { Fn, floor, If, int, ivec2, Loop, round, texture, uniform, vec2 } from 'three/tsl';
|
|
3
|
+
export const CELL = 0.1;
|
|
4
|
+
/** Cells per tile side. */
|
|
5
|
+
export const TILE = 16;
|
|
6
|
+
const TILE_M = CELL * TILE;
|
|
7
|
+
/**
|
|
8
|
+
* A cell is wall when a wall's footprint reaches this close to its centre (on each axis): thin
|
|
9
|
+
* walls still mark an unbroken line of cells, and a wall cell ends at most ~10 cm past the face.
|
|
10
|
+
*/
|
|
11
|
+
const WALL_REACH = CELL / 4;
|
|
12
|
+
/** Margin around a room: the 2-cell dilation plus polygon slop. */
|
|
13
|
+
const APRON = 0.35;
|
|
14
|
+
/** Cells built around a tile so its 2-pass dilation and its edges' neighbours are exact. */
|
|
15
|
+
const MARGIN = 3;
|
|
16
|
+
/** Rooms of a level closer than this (in tiles) share a cluster. */
|
|
17
|
+
const CLUSTER_GAP = 2;
|
|
18
|
+
const LEVEL_W = 256;
|
|
19
|
+
/** Directory cells, at most (they grow coarser on vast sites). */
|
|
20
|
+
const MAX_DIR_CELLS = 65536;
|
|
21
|
+
/** Room ids are stored as two bytes (id + 1). */
|
|
22
|
+
export const MAX_ROOMS = 65535;
|
|
23
|
+
/** WebGPU guarantees 8192 texels a side; the renderer asks for no more. */
|
|
24
|
+
export const DEFAULT_TEXTURE_LIMIT = 8192;
|
|
25
|
+
export class RoomMap {
|
|
26
|
+
/** The device's largest 2D texture side; set before `build`. */
|
|
27
|
+
textureLimit = DEFAULT_TEXTURE_LIMIT;
|
|
28
|
+
levels = [];
|
|
29
|
+
/** Level indices sorted by building, then height: the order the GPU searches in. */
|
|
30
|
+
order = [];
|
|
31
|
+
position = [];
|
|
32
|
+
/** Per building: its range in `order`. */
|
|
33
|
+
buildingRange = [];
|
|
34
|
+
/** Per building: the turn [cos, sin] that lays its walls along the cells. */
|
|
35
|
+
turns = [];
|
|
36
|
+
originX = 0;
|
|
37
|
+
originZ = 0;
|
|
38
|
+
/** The directory's corner: it stays on the world axes, whatever the buildings' turns. */
|
|
39
|
+
dirX = 0;
|
|
40
|
+
dirZ = 0;
|
|
41
|
+
dirTiles = 8;
|
|
42
|
+
dirW = 0;
|
|
43
|
+
dirH = 0;
|
|
44
|
+
/** Per directory cell: the buildings whose rooms reach it. */
|
|
45
|
+
dirLists = [];
|
|
46
|
+
atlas = new Uint8Array(2);
|
|
47
|
+
atlasTilesW = 1;
|
|
48
|
+
atlasTiles = 0;
|
|
49
|
+
indexBytes = 0;
|
|
50
|
+
warnedOverflow = false;
|
|
51
|
+
u = {
|
|
52
|
+
origin: uniform(new Vector2()),
|
|
53
|
+
dirOrigin: uniform(new Vector2()),
|
|
54
|
+
/** The only building's turn (with `single`). */
|
|
55
|
+
turn: uniform(new Vector2(1, 0)),
|
|
56
|
+
dirTiles: uniform(8, 'int'),
|
|
57
|
+
dirW: uniform(0, 'int'),
|
|
58
|
+
dirH: uniform(0, 'int'),
|
|
59
|
+
/** One building: its level range is the only candidate (no directory reads). */
|
|
60
|
+
single: uniform(1, 'int'),
|
|
61
|
+
singleCount: uniform(0, 'int'),
|
|
62
|
+
/** Binary-search steps for the tallest building. */
|
|
63
|
+
searchSteps: uniform(0, 'int'),
|
|
64
|
+
atlasTilesW: uniform(1, 'int'),
|
|
65
|
+
indexW: uniform(1, 'int'),
|
|
66
|
+
index: texture(floatTexture(new Float32Array(4), 1, 1)),
|
|
67
|
+
levels: texture(floatTexture(new Float32Array(8), 2, 1)),
|
|
68
|
+
atlas: texture(byteTexture(new Uint8Array(2), 1, 1)),
|
|
69
|
+
};
|
|
70
|
+
levelData = new Float32Array(8);
|
|
71
|
+
/**
|
|
72
|
+
* Rebuild from rooms. `onTile` sees every built tile (the doorway search walks room edges).
|
|
73
|
+
* `walls` keep a room from growing out through them: a wall's far side stays outside.
|
|
74
|
+
*/
|
|
75
|
+
build(levels, rooms, onTile, walls = []) {
|
|
76
|
+
this.levels = levels.map((l) => ({ y: l.y, building: l.building, clusters: [] }));
|
|
77
|
+
this.order = levels
|
|
78
|
+
.map((_, i) => i)
|
|
79
|
+
.sort((a, b) => levels[a].building - levels[b].building || levels[a].y - levels[b].y);
|
|
80
|
+
this.position = new Array(levels.length);
|
|
81
|
+
this.order.forEach((level, pos) => {
|
|
82
|
+
this.position[level] = pos;
|
|
83
|
+
});
|
|
84
|
+
const buildings = levels.reduce((n, l) => Math.max(n, l.building + 1), 0);
|
|
85
|
+
this.buildingRange = Array.from({ length: buildings }, () => [0, 0]);
|
|
86
|
+
this.order.forEach((level, pos) => {
|
|
87
|
+
const range = this.buildingRange[levels[level].building];
|
|
88
|
+
if (range[1] === 0)
|
|
89
|
+
range[0] = pos;
|
|
90
|
+
range[1]++;
|
|
91
|
+
});
|
|
92
|
+
if (rooms.length > MAX_ROOMS)
|
|
93
|
+
console.warn(`[daylight] ${rooms.length} rooms: only the first ${MAX_ROOMS} get room light`);
|
|
94
|
+
// Each building's turn: the direction its room edges run along, folded to a quarter turn
|
|
95
|
+
// (walls at right angles agree) and weighted by length.
|
|
96
|
+
const sums = Array.from({ length: buildings }, () => [0, 0]);
|
|
97
|
+
for (const room of rooms.slice(0, MAX_ROOMS)) {
|
|
98
|
+
const sum = sums[levels[room.level]?.building ?? -1];
|
|
99
|
+
if (!sum)
|
|
100
|
+
continue;
|
|
101
|
+
for (const ring of [room.polygon, ...(room.holes ?? [])])
|
|
102
|
+
for (let a = 0, b = ring.length - 1; a < ring.length; b = a++) {
|
|
103
|
+
const dx = ring[a][0] - ring[b][0];
|
|
104
|
+
const dz = ring[a][1] - ring[b][1];
|
|
105
|
+
const length = Math.hypot(dx, dz);
|
|
106
|
+
const angle = 4 * Math.atan2(dz, dx);
|
|
107
|
+
sum[0] += length * Math.cos(angle);
|
|
108
|
+
sum[1] += length * Math.sin(angle);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// A building already square to the world keeps the plain frame, to the last bit.
|
|
112
|
+
this.turns = sums.map(([c, s]) => {
|
|
113
|
+
const angle = Math.hypot(c, s) < 1e-9 ? 0 : Math.atan2(s, c) / 4;
|
|
114
|
+
return Math.abs(angle) < 1e-9 ? IDENTITY : [Math.cos(angle), Math.sin(angle)];
|
|
115
|
+
});
|
|
116
|
+
const turnOf = (level) => this.turns[levels[level]?.building ?? -1] ?? IDENTITY;
|
|
117
|
+
const turned = (ring, [cos, sin]) => ring.map(([x, z]) => [x * cos + z * sin, z * cos - x * sin]);
|
|
118
|
+
// From here on rooms and walls are in their building's turned frame.
|
|
119
|
+
const used = rooms.slice(0, MAX_ROOMS).map((room) => {
|
|
120
|
+
const turn = turnOf(room.level);
|
|
121
|
+
return turn === IDENTITY
|
|
122
|
+
? room
|
|
123
|
+
: {
|
|
124
|
+
level: room.level,
|
|
125
|
+
polygon: turned(room.polygon, turn),
|
|
126
|
+
holes: room.holes?.map((hole) => turned(hole, turn)),
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
const footprints = walls.map((wall) => {
|
|
130
|
+
const turn = turnOf(wall.level);
|
|
131
|
+
const t = wall.triangles;
|
|
132
|
+
if (turn === IDENTITY)
|
|
133
|
+
return t;
|
|
134
|
+
const [cos, sin] = turn;
|
|
135
|
+
const out = new Float64Array(t.length);
|
|
136
|
+
for (let k = 0; k + 1 < t.length; k += 2) {
|
|
137
|
+
out[k] = t[k] * cos + t[k + 1] * sin;
|
|
138
|
+
out[k + 1] = t[k + 1] * cos - t[k] * sin;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
});
|
|
142
|
+
// Site origin on the tile grid; every rect below is in whole tiles from it.
|
|
143
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
144
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
145
|
+
for (const room of used)
|
|
146
|
+
for (const [x, z] of room.polygon) {
|
|
147
|
+
minX = Math.min(minX, x);
|
|
148
|
+
minZ = Math.min(minZ, z);
|
|
149
|
+
}
|
|
150
|
+
if (!Number.isFinite(minX))
|
|
151
|
+
minX = minZ = 0;
|
|
152
|
+
const originTx = Math.floor((minX - APRON) / TILE_M);
|
|
153
|
+
const originTz = Math.floor((minZ - APRON) / TILE_M);
|
|
154
|
+
this.originX = originTx * TILE_M;
|
|
155
|
+
this.originZ = originTz * TILE_M;
|
|
156
|
+
// Each room's tile rect (padded by the apron).
|
|
157
|
+
const rects = used.map((room) => {
|
|
158
|
+
let x0 = Number.POSITIVE_INFINITY;
|
|
159
|
+
let z0 = Number.POSITIVE_INFINITY;
|
|
160
|
+
let x1 = Number.NEGATIVE_INFINITY;
|
|
161
|
+
let z1 = Number.NEGATIVE_INFINITY;
|
|
162
|
+
for (const [x, z] of room.polygon) {
|
|
163
|
+
x0 = Math.min(x0, x);
|
|
164
|
+
z0 = Math.min(z0, z);
|
|
165
|
+
x1 = Math.max(x1, x);
|
|
166
|
+
z1 = Math.max(z1, z);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
tx0: Math.floor((x0 - APRON) / TILE_M) - originTx,
|
|
170
|
+
tz0: Math.floor((z0 - APRON) / TILE_M) - originTz,
|
|
171
|
+
tx1: Math.floor((x1 + APRON) / TILE_M) - originTx,
|
|
172
|
+
tz1: Math.floor((z1 + APRON) / TILE_M) - originTz,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
const atlasTilesW = Math.max(1, Math.floor(Math.min(this.textureLimit, 4096) / TILE));
|
|
176
|
+
const atlasCapacity = atlasTilesW * Math.max(1, Math.floor(this.textureLimit / TILE));
|
|
177
|
+
const detailTiles = [];
|
|
178
|
+
let buffer = new Uint16Array(0);
|
|
179
|
+
let scratch = new Uint16Array(0);
|
|
180
|
+
let wallBuffer = new Uint8Array(0);
|
|
181
|
+
// Wall triangles per level and tile row (a row's strip reaches a margin into the next rows).
|
|
182
|
+
const wallRows = levels.map(() => new Map());
|
|
183
|
+
const reach = MARGIN * CELL + WALL_REACH;
|
|
184
|
+
walls.forEach((wall, wi) => {
|
|
185
|
+
const rows = wallRows[wall.level];
|
|
186
|
+
if (!rows)
|
|
187
|
+
return;
|
|
188
|
+
const t = footprints[wi];
|
|
189
|
+
for (let k = 0; k + 5 < t.length; k += 6) {
|
|
190
|
+
const zMin = Math.min(t[k + 1], t[k + 3], t[k + 5]);
|
|
191
|
+
const zMax = Math.max(t[k + 1], t[k + 3], t[k + 5]);
|
|
192
|
+
const r0 = Math.floor((zMin - reach - this.originZ) / TILE_M);
|
|
193
|
+
const r1 = Math.floor((zMax + reach - this.originZ) / TILE_M);
|
|
194
|
+
for (let r = r0; r <= r1; r++) {
|
|
195
|
+
const list = rows.get(r);
|
|
196
|
+
if (list)
|
|
197
|
+
list.push(wi, k);
|
|
198
|
+
else
|
|
199
|
+
rows.set(r, [wi, k]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// Where rooms overlap (an automatic room under drawn ones, a unit around its rooms), the
|
|
204
|
+
// smallest wins: rasterized last, so each level's rooms go largest first.
|
|
205
|
+
const area = used.map((room) => Math.abs(ringArea(room.polygon)));
|
|
206
|
+
const byLevel = levels.map(() => []);
|
|
207
|
+
used.forEach((room, ri) => {
|
|
208
|
+
byLevel[room.level]?.push(ri);
|
|
209
|
+
});
|
|
210
|
+
for (const list of byLevel)
|
|
211
|
+
list.sort((a, b) => area[b] - area[a] || a - b);
|
|
212
|
+
for (let li = 0; li < levels.length; li++) {
|
|
213
|
+
const level = this.levels[li];
|
|
214
|
+
for (const members of clusterRooms(byLevel[li], rects)) {
|
|
215
|
+
let tx0 = Number.POSITIVE_INFINITY;
|
|
216
|
+
let tz0 = Number.POSITIVE_INFINITY;
|
|
217
|
+
let tx1 = Number.NEGATIVE_INFINITY;
|
|
218
|
+
let tz1 = Number.NEGATIVE_INFINITY;
|
|
219
|
+
for (const ri of members) {
|
|
220
|
+
const r = rects[ri];
|
|
221
|
+
tx0 = Math.min(tx0, r.tx0);
|
|
222
|
+
tz0 = Math.min(tz0, r.tz0);
|
|
223
|
+
tx1 = Math.max(tx1, r.tx1);
|
|
224
|
+
tz1 = Math.max(tz1, r.tz1);
|
|
225
|
+
}
|
|
226
|
+
const cluster = {
|
|
227
|
+
tx0,
|
|
228
|
+
tz0,
|
|
229
|
+
tw: tx1 - tx0 + 1,
|
|
230
|
+
th: tz1 - tz0 + 1,
|
|
231
|
+
entries: new Float32Array((tx1 - tx0 + 1) * (tz1 - tz0 + 1)),
|
|
232
|
+
};
|
|
233
|
+
level.clusters.push(cluster);
|
|
234
|
+
// Rooms per tile, from each room's own rect.
|
|
235
|
+
const buckets = Array.from({ length: cluster.entries.length }, () => []);
|
|
236
|
+
for (const ri of members) {
|
|
237
|
+
const r = rects[ri];
|
|
238
|
+
for (let tz = r.tz0; tz <= r.tz1; tz++)
|
|
239
|
+
for (let tx = r.tx0; tx <= r.tx1; tx++)
|
|
240
|
+
buckets[(tz - tz0) * cluster.tw + (tx - tx0)].push(ri);
|
|
241
|
+
}
|
|
242
|
+
// One row of tiles at a time (plus a margin all round): each room rasterizes once per row.
|
|
243
|
+
const stride = cluster.tw * TILE + 2 * MARGIN;
|
|
244
|
+
const rowH = TILE + 2 * MARGIN;
|
|
245
|
+
if (buffer.length < stride * rowH) {
|
|
246
|
+
buffer = new Uint16Array(stride * rowH);
|
|
247
|
+
scratch = new Uint16Array(stride * rowH);
|
|
248
|
+
wallBuffer = new Uint8Array(stride * rowH);
|
|
249
|
+
}
|
|
250
|
+
const strip = buffer.subarray(0, stride * rowH);
|
|
251
|
+
const wallStrip = wallBuffer.subarray(0, stride * rowH);
|
|
252
|
+
const inRow = new Set();
|
|
253
|
+
for (let tz = tz0; tz <= tz1; tz++) {
|
|
254
|
+
inRow.clear();
|
|
255
|
+
for (let tx = tx0; tx <= tx1; tx++)
|
|
256
|
+
for (const ri of buckets[(tz - tz0) * cluster.tw + (tx - tx0)])
|
|
257
|
+
inRow.add(ri);
|
|
258
|
+
if (!inRow.size)
|
|
259
|
+
continue;
|
|
260
|
+
const ci0 = tx0 * TILE - MARGIN;
|
|
261
|
+
const cj0 = tz * TILE - MARGIN;
|
|
262
|
+
const x0 = this.originX + ci0 * CELL;
|
|
263
|
+
const z0 = this.originZ + cj0 * CELL;
|
|
264
|
+
strip.fill(0);
|
|
265
|
+
for (const ri of [...inRow].sort((a, b) => area[b] - area[a] || a - b))
|
|
266
|
+
rasterize(used[ri], ri + 1, strip, stride, rowH, x0, z0);
|
|
267
|
+
// Growth only fills empty cells: inside a big room there are none.
|
|
268
|
+
if (strip.includes(0)) {
|
|
269
|
+
const rowWalls = wallRows[li].get(tz);
|
|
270
|
+
if (rowWalls) {
|
|
271
|
+
wallStrip.fill(0);
|
|
272
|
+
for (let w = 0; w < rowWalls.length; w += 2)
|
|
273
|
+
markWall(footprints[rowWalls[w]], rowWalls[w + 1], wallStrip, stride, rowH, x0, z0);
|
|
274
|
+
}
|
|
275
|
+
dilate(strip, scratch, stride, rowH, rowWalls ? wallStrip : undefined);
|
|
276
|
+
}
|
|
277
|
+
for (let tx = tx0; tx <= tx1; tx++) {
|
|
278
|
+
const t = (tz - tz0) * cluster.tw + (tx - tx0);
|
|
279
|
+
if (!buckets[t].length)
|
|
280
|
+
continue;
|
|
281
|
+
const i0 = MARGIN + (tx - tx0) * TILE;
|
|
282
|
+
const first = strip[MARGIN * stride + i0];
|
|
283
|
+
let same = true;
|
|
284
|
+
for (let j = MARGIN; j < MARGIN + TILE && same; j++)
|
|
285
|
+
for (let i = i0; i < i0 + TILE; i++)
|
|
286
|
+
if (strip[j * stride + i] !== first) {
|
|
287
|
+
same = false;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (same)
|
|
291
|
+
cluster.entries[t] = first;
|
|
292
|
+
else if (detailTiles.length >= atlasCapacity) {
|
|
293
|
+
// Out of atlas: the tile's most common room stands for all of it.
|
|
294
|
+
if (!this.warnedOverflow) {
|
|
295
|
+
console.warn('[daylight] room map atlas full: some room edges are coarse');
|
|
296
|
+
this.warnedOverflow = true;
|
|
297
|
+
}
|
|
298
|
+
cluster.entries[t] = majority(strip, stride, i0);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const tile = new Uint16Array(TILE * TILE);
|
|
302
|
+
for (let j = 0; j < TILE; j++)
|
|
303
|
+
tile.set(strip.subarray((MARGIN + j) * stride + i0, (MARGIN + j) * stride + i0 + TILE), j * TILE);
|
|
304
|
+
detailTiles.push(tile);
|
|
305
|
+
cluster.entries[t] = -detailTiles.length;
|
|
306
|
+
}
|
|
307
|
+
onTile?.({
|
|
308
|
+
level: li,
|
|
309
|
+
ci0,
|
|
310
|
+
cj0,
|
|
311
|
+
x0,
|
|
312
|
+
z0,
|
|
313
|
+
turn: turnOf(li),
|
|
314
|
+
stride,
|
|
315
|
+
i0,
|
|
316
|
+
j0: MARGIN,
|
|
317
|
+
cells: strip,
|
|
318
|
+
uniform: same,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Atlas: detailed tiles in rows, two bytes (room + 1) per cell.
|
|
325
|
+
this.atlasTilesW = atlasTilesW;
|
|
326
|
+
this.atlasTiles = detailTiles.length;
|
|
327
|
+
const atlasRows = Math.max(1, Math.ceil(detailTiles.length / atlasTilesW));
|
|
328
|
+
const aw = atlasTilesW * TILE;
|
|
329
|
+
this.atlas = new Uint8Array(aw * atlasRows * TILE * 2);
|
|
330
|
+
detailTiles.forEach((tile, a) => {
|
|
331
|
+
const ox = (a % atlasTilesW) * TILE;
|
|
332
|
+
const oz = Math.floor(a / atlasTilesW) * TILE;
|
|
333
|
+
for (let j = 0; j < TILE; j++)
|
|
334
|
+
for (let i = 0; i < TILE; i++) {
|
|
335
|
+
const v = tile[j * TILE + i];
|
|
336
|
+
const k = ((oz + j) * aw + ox + i) * 2;
|
|
337
|
+
this.atlas[k] = v & 255;
|
|
338
|
+
this.atlas[k + 1] = v >> 8;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// Directory: per cell, the buildings whose clusters reach it (coarser cells on vast sites).
|
|
342
|
+
// On the world axes: each cluster's rect, turned back, as a world box.
|
|
343
|
+
const boxes = [];
|
|
344
|
+
for (const l of this.levels) {
|
|
345
|
+
const [cos, sin] = this.turns[l.building] ?? IDENTITY;
|
|
346
|
+
for (const c of l.clusters) {
|
|
347
|
+
const box = {
|
|
348
|
+
building: l.building,
|
|
349
|
+
x0: Number.POSITIVE_INFINITY,
|
|
350
|
+
z0: Number.POSITIVE_INFINITY,
|
|
351
|
+
x1: Number.NEGATIVE_INFINITY,
|
|
352
|
+
z1: Number.NEGATIVE_INFINITY,
|
|
353
|
+
};
|
|
354
|
+
for (const tx of [c.tx0, c.tx0 + c.tw])
|
|
355
|
+
for (const tz of [c.tz0, c.tz0 + c.th]) {
|
|
356
|
+
const mx = this.originX + tx * TILE_M;
|
|
357
|
+
const mz = this.originZ + tz * TILE_M;
|
|
358
|
+
const x = mx * cos - mz * sin;
|
|
359
|
+
const z = mx * sin + mz * cos;
|
|
360
|
+
box.x0 = Math.min(box.x0, x);
|
|
361
|
+
box.z0 = Math.min(box.z0, z);
|
|
362
|
+
box.x1 = Math.max(box.x1, x);
|
|
363
|
+
box.z1 = Math.max(box.z1, z);
|
|
364
|
+
}
|
|
365
|
+
boxes.push(box);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this.dirX = boxes.reduce((min, box) => Math.min(min, box.x0), Number.POSITIVE_INFINITY);
|
|
369
|
+
this.dirZ = boxes.reduce((min, box) => Math.min(min, box.z0), Number.POSITIVE_INFINITY);
|
|
370
|
+
if (!boxes.length)
|
|
371
|
+
this.dirX = this.dirZ = 0;
|
|
372
|
+
// Whole tiles; a hair under, so a box ending on a tile edge doesn't claim the next tile.
|
|
373
|
+
const tiles = (metres) => Math.max(0, Math.ceil(metres / TILE_M - 1e-6));
|
|
374
|
+
const maxTx = boxes.reduce((max, box) => Math.max(max, tiles(box.x1 - this.dirX)), 0);
|
|
375
|
+
const maxTz = boxes.reduce((max, box) => Math.max(max, tiles(box.z1 - this.dirZ)), 0);
|
|
376
|
+
this.dirTiles = Math.max(8, Math.ceil(Math.sqrt((maxTx * maxTz) / MAX_DIR_CELLS)));
|
|
377
|
+
this.dirW = Math.max(1, Math.ceil(maxTx / this.dirTiles));
|
|
378
|
+
this.dirH = Math.max(1, Math.ceil(maxTz / this.dirTiles));
|
|
379
|
+
this.dirLists = Array.from({ length: this.dirW * this.dirH }, () => []);
|
|
380
|
+
const dirCell = (metres) => Math.floor(metres / TILE_M / this.dirTiles);
|
|
381
|
+
for (const box of boxes)
|
|
382
|
+
for (let dz = dirCell(box.z0 - this.dirZ + 1e-6); dz <= Math.min(this.dirH - 1, dirCell(box.z1 - this.dirZ - 1e-6)); dz++)
|
|
383
|
+
for (let dx = dirCell(box.x0 - this.dirX + 1e-6); dx <= Math.min(this.dirW - 1, dirCell(box.x1 - this.dirX - 1e-6)); dx++) {
|
|
384
|
+
const list = this.dirLists[dz * this.dirW + dx];
|
|
385
|
+
if (!list.includes(box.building))
|
|
386
|
+
list.push(box.building);
|
|
387
|
+
}
|
|
388
|
+
this.upload();
|
|
389
|
+
}
|
|
390
|
+
/** CPU lookup in assembled space (same rule as the shader). */
|
|
391
|
+
roomAt(x, y, z) {
|
|
392
|
+
const dx = Math.floor((x - this.dirX) / TILE_M / this.dirTiles);
|
|
393
|
+
const dz = Math.floor((z - this.dirZ) / TILE_M / this.dirTiles);
|
|
394
|
+
if (dx < 0 || dz < 0 || dx >= this.dirW || dz >= this.dirH)
|
|
395
|
+
return -1;
|
|
396
|
+
for (const b of this.dirLists[dz * this.dirW + dx]) {
|
|
397
|
+
const [start, count] = this.buildingRange[b];
|
|
398
|
+
let found = start;
|
|
399
|
+
for (let pos = start; pos < start + count; pos++)
|
|
400
|
+
if (this.levels[this.order[pos]].y <= y - 0.12)
|
|
401
|
+
found = pos;
|
|
402
|
+
// The point in the building's turned frame.
|
|
403
|
+
const [cos, sin] = this.turns[b];
|
|
404
|
+
const mx = x * cos + z * sin;
|
|
405
|
+
const mz = z * cos - x * sin;
|
|
406
|
+
const tx = Math.floor((mx - this.originX) / TILE_M);
|
|
407
|
+
const tz = Math.floor((mz - this.originZ) / TILE_M);
|
|
408
|
+
const room = this.levelRoomAt(this.order[found], mx, mz, tx, tz);
|
|
409
|
+
if (room >= 0)
|
|
410
|
+
return room;
|
|
411
|
+
}
|
|
412
|
+
return -1;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A level's room at a point of its building's frame; clusters' rects can overlap (a wing around
|
|
416
|
+
* a courtyard pavilion).
|
|
417
|
+
*/
|
|
418
|
+
levelRoomAt(li, x, z, tx, tz) {
|
|
419
|
+
for (const c of this.levels[li].clusters) {
|
|
420
|
+
const lx = tx - c.tx0;
|
|
421
|
+
const lz = tz - c.tz0;
|
|
422
|
+
if (lx < 0 || lz < 0 || lx >= c.tw || lz >= c.th)
|
|
423
|
+
continue;
|
|
424
|
+
const e = c.entries[lz * c.tw + lx];
|
|
425
|
+
if (e > 0)
|
|
426
|
+
return e - 1;
|
|
427
|
+
if (e === 0)
|
|
428
|
+
continue;
|
|
429
|
+
const a = -e - 1;
|
|
430
|
+
const ci = Math.floor((x - this.originX) / CELL) - tx * TILE;
|
|
431
|
+
const cj = Math.floor((z - this.originZ) / CELL) - tz * TILE;
|
|
432
|
+
const aw = this.atlasTilesW * TILE;
|
|
433
|
+
const k = ((Math.floor(a / this.atlasTilesW) * TILE + cj) * aw + (a % this.atlasTilesW) * TILE + ci) *
|
|
434
|
+
2;
|
|
435
|
+
const room = this.atlas[k] + this.atlas[k + 1] * 256 - 1;
|
|
436
|
+
if (room >= 0)
|
|
437
|
+
return room;
|
|
438
|
+
}
|
|
439
|
+
return -1;
|
|
440
|
+
}
|
|
441
|
+
/** The level stacked right above `level` in its building, or −1. */
|
|
442
|
+
levelAbove(level) {
|
|
443
|
+
const pos = this.position[level];
|
|
444
|
+
if (pos === undefined)
|
|
445
|
+
return -1;
|
|
446
|
+
const next = this.order[pos + 1];
|
|
447
|
+
return next !== undefined && this.levels[next].building === this.levels[level].building
|
|
448
|
+
? next
|
|
449
|
+
: -1;
|
|
450
|
+
}
|
|
451
|
+
/** Index the GPU uses for `level` (rooms store it). */
|
|
452
|
+
gpuLevel(level) {
|
|
453
|
+
return this.position[level] ?? 0;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Live presentation per level (input order): explode offset, whether it is shown, and whether
|
|
457
|
+
* the levels over it are hidden (its rooms open to the sky from above).
|
|
458
|
+
*/
|
|
459
|
+
setPresentation(offsets, shown, focused) {
|
|
460
|
+
let changed = false;
|
|
461
|
+
this.order.forEach((level, pos) => {
|
|
462
|
+
const off = offsets[level] ?? 0;
|
|
463
|
+
const flags = (focused[level] ? 1 : 0) + (shown[level] !== false ? 2 : 0);
|
|
464
|
+
const t = pos * 8;
|
|
465
|
+
const liveY = this.levels[level].y + off;
|
|
466
|
+
if (this.levelData[t] !== liveY) {
|
|
467
|
+
this.levelData[t] = liveY;
|
|
468
|
+
changed = true;
|
|
469
|
+
}
|
|
470
|
+
if (this.levelData[t + 6] !== off || this.levelData[t + 7] !== flags) {
|
|
471
|
+
this.levelData[t + 6] = off;
|
|
472
|
+
this.levelData[t + 7] = flags;
|
|
473
|
+
changed = true;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
if (changed)
|
|
477
|
+
this.u.levels.value.needsUpdate = true;
|
|
478
|
+
}
|
|
479
|
+
memoryBytes() {
|
|
480
|
+
return this.atlas.byteLength + this.indexBytes + this.levelData.byteLength;
|
|
481
|
+
}
|
|
482
|
+
get stats() {
|
|
483
|
+
return { atlasTiles: this.atlasTiles, dirW: this.dirW, dirH: this.dirH };
|
|
484
|
+
}
|
|
485
|
+
upload() {
|
|
486
|
+
// index: [directory cells] [directory lists] [clusters, 2 texels] [entries]
|
|
487
|
+
const dirCells = this.dirW * this.dirH;
|
|
488
|
+
const listTexels = this.dirLists.reduce((s, l) => s + l.length, 0);
|
|
489
|
+
const clusterCount = this.levels.reduce((s, l) => s + l.clusters.length, 0);
|
|
490
|
+
const entryTexels = this.levels.reduce((s, l) => s + l.clusters.reduce((t, c) => t + c.entries.length, 0), 0);
|
|
491
|
+
const texels = dirCells + listTexels + clusterCount * 2 + entryTexels + 1;
|
|
492
|
+
// As wide as the device allows, so a vast site's directory still fits.
|
|
493
|
+
const indexW = Math.min(this.textureLimit, 4096);
|
|
494
|
+
const rows = Math.max(1, Math.ceil(texels / indexW));
|
|
495
|
+
if (rows > this.textureLimit)
|
|
496
|
+
console.warn('[daylight] room map index exceeds the device texture limit');
|
|
497
|
+
const index = new Float32Array(indexW * rows * 4);
|
|
498
|
+
let list = dirCells;
|
|
499
|
+
this.dirLists.forEach((buildings, c) => {
|
|
500
|
+
index[c * 4] = list;
|
|
501
|
+
index[c * 4 + 1] = buildings.length;
|
|
502
|
+
for (const b of buildings) {
|
|
503
|
+
const [start, count] = this.buildingRange[b];
|
|
504
|
+
index.set([start, count, ...this.turns[b]], list * 4);
|
|
505
|
+
list++;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
const clusterStart = new Array(this.levels.length).fill(0);
|
|
509
|
+
let cluster = list;
|
|
510
|
+
let entry = list + clusterCount * 2;
|
|
511
|
+
this.levels.forEach((l, li) => {
|
|
512
|
+
clusterStart[li] = cluster;
|
|
513
|
+
for (const c of l.clusters) {
|
|
514
|
+
index.set([c.tx0, c.tz0, c.tw, c.th, entry, 0, 0, 0], cluster * 4);
|
|
515
|
+
for (let k = 0; k < c.entries.length; k++)
|
|
516
|
+
index[(entry + k) * 4] = c.entries[k];
|
|
517
|
+
entry += c.entries.length;
|
|
518
|
+
cluster += 2;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
this.indexBytes = index.byteLength;
|
|
522
|
+
// levels (GPU order): [liveY, clusterStart, clusterCount, 0] [0, 0, offset, flags]
|
|
523
|
+
const levelRows = Math.max(1, Math.ceil((this.order.length * 2) / LEVEL_W));
|
|
524
|
+
this.levelData = new Float32Array(LEVEL_W * levelRows * 4);
|
|
525
|
+
this.order.forEach((level, pos) => {
|
|
526
|
+
const l = this.levels[level];
|
|
527
|
+
this.levelData.set([l.y, clusterStart[level], l.clusters.length, 0, 0, 0, 0, 2], pos * 8);
|
|
528
|
+
});
|
|
529
|
+
const aw = this.atlasTilesW * TILE;
|
|
530
|
+
replace(this.u.index, floatTexture(index, indexW, rows));
|
|
531
|
+
replace(this.u.levels, floatTexture(this.levelData, LEVEL_W, levelRows));
|
|
532
|
+
replace(this.u.atlas, byteTexture(this.atlas, aw, this.atlas.length / 2 / aw));
|
|
533
|
+
this.u.origin.value.set(this.originX, this.originZ);
|
|
534
|
+
this.u.dirOrigin.value.set(this.dirX, this.dirZ);
|
|
535
|
+
this.u.turn.value.set(...(this.turns[0] ?? IDENTITY));
|
|
536
|
+
this.u.indexW.value = indexW;
|
|
537
|
+
this.u.dirTiles.value = this.dirTiles;
|
|
538
|
+
this.u.dirW.value = this.dirW;
|
|
539
|
+
this.u.dirH.value = this.dirH;
|
|
540
|
+
this.u.atlasTilesW.value = this.atlasTilesW;
|
|
541
|
+
this.u.single.value = this.buildingRange.length <= 1 ? 1 : 0;
|
|
542
|
+
this.u.singleCount.value = this.buildingRange[0]?.[1] ?? 0;
|
|
543
|
+
this.u.searchSteps.value = this.buildingRange.reduce((steps, [, count]) => Math.max(steps, Math.ceil(Math.log2(count + 1))), 0);
|
|
544
|
+
}
|
|
545
|
+
// ---- TSL ---------------------------------------------------------------------------------
|
|
546
|
+
indexTexel(i) {
|
|
547
|
+
const k = int(i);
|
|
548
|
+
return this.u.index.load(ivec2(k.mod(this.u.indexW), k.div(this.u.indexW)));
|
|
549
|
+
}
|
|
550
|
+
/** [liveY, clusterStart, clusterCount, 0] of the level at GPU position `pos`. */
|
|
551
|
+
levelTexel0(pos) {
|
|
552
|
+
const k = int(pos).mul(2);
|
|
553
|
+
return this.u.levels.load(ivec2(k.mod(LEVEL_W), k.div(LEVEL_W)));
|
|
554
|
+
}
|
|
555
|
+
/** [0, 0, offset, flags] of the level at GPU position `pos`. */
|
|
556
|
+
levelTexel1(pos) {
|
|
557
|
+
const k = int(pos).mul(2).add(1);
|
|
558
|
+
return this.u.levels.load(ivec2(k.mod(LEVEL_W), k.div(LEVEL_W)));
|
|
559
|
+
}
|
|
560
|
+
/** Room index (−1 outside) at a world-space position (live heights). */
|
|
561
|
+
roomAtNode = Fn(([p]) => {
|
|
562
|
+
const u = this.u;
|
|
563
|
+
const room = int(-1).toVar();
|
|
564
|
+
// Candidate buildings: the only one, or the directory cell's list.
|
|
565
|
+
const count = int(0).toVar();
|
|
566
|
+
const listStart = int(0).toVar();
|
|
567
|
+
If(u.single.equal(1), () => {
|
|
568
|
+
count.assign(1);
|
|
569
|
+
}).Else(() => {
|
|
570
|
+
const dir = ivec2(floor(p.xz.sub(u.dirOrigin).div(TILE_M).div(u.dirTiles)));
|
|
571
|
+
If(dir.x
|
|
572
|
+
.greaterThanEqual(0)
|
|
573
|
+
.and(dir.y.greaterThanEqual(0))
|
|
574
|
+
.and(dir.x.lessThan(u.dirW))
|
|
575
|
+
.and(dir.y.lessThan(u.dirH)), () => {
|
|
576
|
+
const d = this.indexTexel(dir.y.mul(u.dirW).add(dir.x));
|
|
577
|
+
listStart.assign(int(d.x));
|
|
578
|
+
count.assign(int(d.y));
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
const y = p.y.sub(0.12);
|
|
582
|
+
Loop({ start: int(0), end: count, type: 'int', condition: '<' }, ({ i }) => {
|
|
583
|
+
If(room.lessThan(0), () => {
|
|
584
|
+
const start = int(0).toVar();
|
|
585
|
+
const levels = u.singleCount.toVar();
|
|
586
|
+
const turn = u.turn.toVar();
|
|
587
|
+
If(u.single.equal(0), () => {
|
|
588
|
+
const range = this.indexTexel(listStart.add(i));
|
|
589
|
+
start.assign(int(range.x));
|
|
590
|
+
levels.assign(int(range.y));
|
|
591
|
+
turn.assign(range.zw);
|
|
592
|
+
});
|
|
593
|
+
// The point in the building's turned frame, from the site origin.
|
|
594
|
+
const m = vec2(p.x.mul(turn.x).add(p.z.mul(turn.y)), p.z.mul(turn.x).sub(p.x.mul(turn.y)))
|
|
595
|
+
.sub(u.origin)
|
|
596
|
+
.toVar();
|
|
597
|
+
const tile = ivec2(floor(m.div(TILE_M))).toVar();
|
|
598
|
+
// Last level of the building whose live floor is at or below the point (else its lowest).
|
|
599
|
+
const lo = start.toVar();
|
|
600
|
+
const hi = start.add(levels).sub(1).toVar();
|
|
601
|
+
const found = start.toVar();
|
|
602
|
+
Loop({ start: int(0), end: u.searchSteps, type: 'int', condition: '<' }, () => {
|
|
603
|
+
If(lo.lessThanEqual(hi), () => {
|
|
604
|
+
const mid = lo.add(hi).div(2);
|
|
605
|
+
If(this.levelTexel0(mid).x.lessThanEqual(y), () => {
|
|
606
|
+
found.assign(mid);
|
|
607
|
+
lo.assign(mid.add(1));
|
|
608
|
+
}).Else(() => {
|
|
609
|
+
hi.assign(mid.sub(1));
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
const level = this.levelTexel0(found);
|
|
614
|
+
// Clusters' rects can overlap: the first one holding a room at the point wins.
|
|
615
|
+
Loop({ start: int(0), end: int(level.z), type: 'int', condition: '<' }, ({ i: c }) => {
|
|
616
|
+
If(room.lessThan(0), () => {
|
|
617
|
+
const base = int(level.y).add(c.mul(2));
|
|
618
|
+
const rect = this.indexTexel(base);
|
|
619
|
+
const local = tile.sub(ivec2(int(rect.x), int(rect.y)));
|
|
620
|
+
If(local.x
|
|
621
|
+
.greaterThanEqual(0)
|
|
622
|
+
.and(local.y.greaterThanEqual(0))
|
|
623
|
+
.and(local.x.lessThan(int(rect.z)))
|
|
624
|
+
.and(local.y.lessThan(int(rect.w))), () => {
|
|
625
|
+
const entryStart = int(this.indexTexel(base.add(1)).x);
|
|
626
|
+
const e = this.indexTexel(entryStart.add(local.y.mul(int(rect.z))).add(local.x)).x;
|
|
627
|
+
If(e.greaterThan(0.5), () => {
|
|
628
|
+
room.assign(int(round(e)).sub(1));
|
|
629
|
+
}).ElseIf(e.lessThan(-0.5), () => {
|
|
630
|
+
const a = int(round(e.negate())).sub(1);
|
|
631
|
+
const cell = ivec2(floor(m.div(CELL))).sub(tile.mul(TILE));
|
|
632
|
+
const at = ivec2(a.mod(u.atlasTilesW).mul(TILE).add(cell.x), a.div(u.atlasTilesW).mul(TILE).add(cell.y));
|
|
633
|
+
const texel = u.atlas.load(at);
|
|
634
|
+
const id = int(round(texel.x.mul(255))).add(int(round(texel.y.mul(255))).mul(256));
|
|
635
|
+
room.assign(id.sub(1));
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
return room;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// ---- build helpers --------------------------------------------------------------------------
|
|
646
|
+
const IDENTITY = [1, 0];
|
|
647
|
+
/** Groups a level's rooms whose padded tile rects lie within `CLUSTER_GAP` tiles of each other. */
|
|
648
|
+
function clusterRooms(members, rects) {
|
|
649
|
+
const parent = members.map((_, i) => i);
|
|
650
|
+
const find = (i) => {
|
|
651
|
+
while (parent[i] !== i) {
|
|
652
|
+
parent[i] = parent[parent[i]];
|
|
653
|
+
i = parent[i];
|
|
654
|
+
}
|
|
655
|
+
return i;
|
|
656
|
+
};
|
|
657
|
+
const order = members
|
|
658
|
+
.map((_, i) => i)
|
|
659
|
+
.sort((a, b) => rects[members[a]].tx0 - rects[members[b]].tx0);
|
|
660
|
+
for (let a = 0; a < order.length; a++) {
|
|
661
|
+
const ra = rects[members[order[a]]];
|
|
662
|
+
for (let b = a + 1; b < order.length; b++) {
|
|
663
|
+
const rb = rects[members[order[b]]];
|
|
664
|
+
if (rb.tx0 > ra.tx1 + CLUSTER_GAP)
|
|
665
|
+
break;
|
|
666
|
+
if (rb.tz0 <= ra.tz1 + CLUSTER_GAP && ra.tz0 <= rb.tz1 + CLUSTER_GAP)
|
|
667
|
+
parent[find(order[a])] = find(order[b]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const groups = new Map();
|
|
671
|
+
members.forEach((ri, i) => {
|
|
672
|
+
const root = find(i);
|
|
673
|
+
const group = groups.get(root);
|
|
674
|
+
if (group)
|
|
675
|
+
group.push(ri);
|
|
676
|
+
else
|
|
677
|
+
groups.set(root, [ri]);
|
|
678
|
+
});
|
|
679
|
+
return [...groups.values()];
|
|
680
|
+
}
|
|
681
|
+
function ringArea(ring) {
|
|
682
|
+
let a = 0;
|
|
683
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++)
|
|
684
|
+
a += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];
|
|
685
|
+
return a / 2;
|
|
686
|
+
}
|
|
687
|
+
/** Scanline fill of a room (holes subtracted, even-odd) at cell centres. */
|
|
688
|
+
function rasterize(room, value, cells, width, height, x0, z0) {
|
|
689
|
+
const rings = [room.polygon, ...(room.holes ?? [])];
|
|
690
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
691
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
692
|
+
for (const [, z] of room.polygon) {
|
|
693
|
+
minZ = Math.min(minZ, z);
|
|
694
|
+
maxZ = Math.max(maxZ, z);
|
|
695
|
+
}
|
|
696
|
+
const j0 = Math.max(0, Math.floor((minZ - z0) / CELL));
|
|
697
|
+
const j1 = Math.min(height - 1, Math.ceil((maxZ - z0) / CELL));
|
|
698
|
+
const xs = [];
|
|
699
|
+
for (let j = j0; j <= j1; j++) {
|
|
700
|
+
const z = z0 + (j + 0.5) * CELL;
|
|
701
|
+
xs.length = 0;
|
|
702
|
+
for (const ring of rings)
|
|
703
|
+
for (let a = 0, b = ring.length - 1; a < ring.length; b = a++) {
|
|
704
|
+
const [xa, za] = ring[a];
|
|
705
|
+
const [xb, zb] = ring[b];
|
|
706
|
+
if (za > z !== zb > z)
|
|
707
|
+
xs.push(((xb - xa) * (z - za)) / (zb - za) + xa);
|
|
708
|
+
}
|
|
709
|
+
xs.sort((p, q) => p - q);
|
|
710
|
+
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
711
|
+
// Cells whose centre lies in (xs[k], xs[k+1]].
|
|
712
|
+
const i0 = Math.max(0, Math.floor((xs[k] - x0) / CELL - 0.5) + 1);
|
|
713
|
+
const i1 = Math.min(width - 1, Math.floor((xs[k + 1] - x0) / CELL - 0.5));
|
|
714
|
+
if (i1 >= i0)
|
|
715
|
+
cells.fill(value, j * width + i0, j * width + i1 + 1);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/** Marks the cells one footprint triangle (`t[at…at+5]`, world XZ) reaches. */
|
|
720
|
+
function markWall(t, at, wall, width, height, x0, z0) {
|
|
721
|
+
const zMin = Math.min(t[at + 1], t[at + 3], t[at + 5]);
|
|
722
|
+
const zMax = Math.max(t[at + 1], t[at + 3], t[at + 5]);
|
|
723
|
+
const j0 = Math.max(0, Math.ceil((zMin - WALL_REACH - z0) / CELL - 0.5));
|
|
724
|
+
const j1 = Math.min(height - 1, Math.floor((zMax + WALL_REACH - z0) / CELL - 0.5));
|
|
725
|
+
for (let j = j0; j <= j1; j++) {
|
|
726
|
+
// The triangle's x extent inside the band of z this row's cell centres reach.
|
|
727
|
+
const lo = z0 + (j + 0.5) * CELL - WALL_REACH;
|
|
728
|
+
const hi = lo + 2 * WALL_REACH;
|
|
729
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
730
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
731
|
+
for (let a = 0; a < 3; a++) {
|
|
732
|
+
const xa = t[at + a * 2];
|
|
733
|
+
const za = t[at + a * 2 + 1];
|
|
734
|
+
const xb = t[at + ((a + 1) % 3) * 2];
|
|
735
|
+
const zb = t[at + ((a + 1) % 3) * 2 + 1];
|
|
736
|
+
if (za >= lo && za <= hi) {
|
|
737
|
+
xMin = Math.min(xMin, xa);
|
|
738
|
+
xMax = Math.max(xMax, xa);
|
|
739
|
+
}
|
|
740
|
+
for (const z of [lo, hi])
|
|
741
|
+
if (za < z !== zb < z) {
|
|
742
|
+
const x = xa + ((xb - xa) * (z - za)) / (zb - za);
|
|
743
|
+
xMin = Math.min(xMin, x);
|
|
744
|
+
xMax = Math.max(xMax, x);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (xMin > xMax)
|
|
748
|
+
continue;
|
|
749
|
+
const i0 = Math.max(0, Math.ceil((xMin - WALL_REACH - x0) / CELL - 0.5));
|
|
750
|
+
const i1 = Math.min(width - 1, Math.floor((xMax + WALL_REACH - x0) / CELL - 0.5));
|
|
751
|
+
if (i1 >= i0)
|
|
752
|
+
wall.fill(1, j * width + i0, j * width + i1 + 1);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Two passes of 4-neighbour growth into empty cells: wall thickness and polygon slop resolve.
|
|
757
|
+
* A room grows into a wall but never out of it: past a wall's far face is outside, wherever the
|
|
758
|
+
* room's outline runs inside the wall (its centre line, or either face).
|
|
759
|
+
*/
|
|
760
|
+
function dilate(cells, scratch, width, height, wall) {
|
|
761
|
+
const steps = [-1, 1, -width, width];
|
|
762
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
763
|
+
scratch.set(cells);
|
|
764
|
+
for (let j = 1; j < height - 1; j++)
|
|
765
|
+
for (let i = 1; i < width - 1; i++) {
|
|
766
|
+
const k = j * width + i;
|
|
767
|
+
if (scratch[k])
|
|
768
|
+
continue;
|
|
769
|
+
const open = !wall || wall[k] === 1;
|
|
770
|
+
for (const step of steps) {
|
|
771
|
+
const n = scratch[k + step];
|
|
772
|
+
if (n && (open || !wall?.[k + step])) {
|
|
773
|
+
cells[k] = n;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/** The most common value among a tile's own cells. */
|
|
781
|
+
function majority(cells, stride, i0) {
|
|
782
|
+
const counts = new Map();
|
|
783
|
+
for (let j = MARGIN; j < MARGIN + TILE; j++)
|
|
784
|
+
for (let i = i0; i < i0 + TILE; i++) {
|
|
785
|
+
const v = cells[j * stride + i];
|
|
786
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
787
|
+
}
|
|
788
|
+
let best = 0;
|
|
789
|
+
let bestCount = -1;
|
|
790
|
+
for (const [v, c] of counts)
|
|
791
|
+
if (c > bestCount) {
|
|
792
|
+
best = v;
|
|
793
|
+
bestCount = c;
|
|
794
|
+
}
|
|
795
|
+
return best;
|
|
796
|
+
}
|
|
797
|
+
function floatTexture(data, width, height) {
|
|
798
|
+
const tex = new DataTexture(data, width, height, RGBAFormat, FloatType);
|
|
799
|
+
tex.magFilter = tex.minFilter = NearestFilter;
|
|
800
|
+
tex.needsUpdate = true;
|
|
801
|
+
return tex;
|
|
802
|
+
}
|
|
803
|
+
function byteTexture(data, width, height) {
|
|
804
|
+
const tex = new DataTexture(data, width, height, RGFormat, UnsignedByteType);
|
|
805
|
+
tex.magFilter = tex.minFilter = NearestFilter;
|
|
806
|
+
tex.needsUpdate = true;
|
|
807
|
+
return tex;
|
|
808
|
+
}
|
|
809
|
+
function replace(node, tex) {
|
|
810
|
+
const previous = node.value;
|
|
811
|
+
node.value = tex;
|
|
812
|
+
previous.dispose();
|
|
813
|
+
}
|