@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7
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 +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
// this file renders is a read of one of those two — never a second, locally
|
|
12
12
|
// invented notion of where an agent stands.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// the vendor bundle (handles KHR_mesh_quantization and EXT_texture_webp
|
|
17
|
-
// natively; only EXT_meshopt_compression needs a decoder line), and there is
|
|
18
|
-
// no globalThis.tmctMudiii — pages publish one globalThis.tmct.
|
|
14
|
+
// GLTFLoader ships in the vendor bundle, and handles KHR_mesh_quantization and
|
|
15
|
+
// EXT_texture_webp natively; only EXT_meshopt_compression needs a decoder line.
|
|
19
16
|
//
|
|
20
17
|
// The page-shell <-> scene handshake, since the two scripts share no scope:
|
|
21
18
|
// - the page shell already defines `window.mudiiiHandleSceneClick(cellId)`
|
|
@@ -42,12 +39,10 @@
|
|
|
42
39
|
// agent or item, never the mid-lerp position. This is what an e2e
|
|
43
40
|
// assertion reads, and what a later screenshot ready-check reads too
|
|
44
41
|
// (see test-e2e/pages-mudiii.test.mjs's own header).
|
|
42
|
+
// .flashCell(cellId) / .showRoute(cells) / .clearRoute() — the clicked
|
|
43
|
+
// cell and the route the followed agent is walking to it, drawn along
|
|
44
|
+
// the cells the world's own exit search returned.
|
|
45
45
|
// .ready() — whether boot() has finished at least once.
|
|
46
|
-
// These three calls are not yet wired into mudiii-viz.mjs's own
|
|
47
|
-
// boot()/applyTickResult()/camera handlers — that file is owned by the viz
|
|
48
|
-
// track, not this one; the coordinator's own report names the exact call
|
|
49
|
-
// sites needed, mirroring how window.mudiiiHandleSceneClick was already
|
|
50
|
-
// added for the reverse direction.
|
|
51
46
|
//
|
|
52
47
|
// Reused from mudiii-viz.mjs rather than re-derived, off its own frozen
|
|
53
48
|
// exports: `roleOfAgentId`, `cellToWorld`, `cellFromGroundPoint`,
|
|
@@ -70,6 +65,12 @@ import { prefersReducedMotion } from "./viz-ticker.mjs";
|
|
|
70
65
|
* own `transition: left .25s ease` becomes in a requestAnimationFrame loop. */
|
|
71
66
|
export const TWEEN_DURATION_MS = 250;
|
|
72
67
|
|
|
68
|
+
/** The vertical field of view the page's one perspective camera is built with.
|
|
69
|
+
* Every rig this scene asks `cameraRigFor` for is measured against the live
|
|
70
|
+
* `camera3.fov`, so this number is only ever the camera's own starting value
|
|
71
|
+
* rather than a second copy a rig could drift from. */
|
|
72
|
+
export const CAMERA_FIELD_OF_VIEW_DEGREES = 55;
|
|
73
|
+
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
// Pure geometry/tween helpers — unit-tested directly in Node, then spliced
|
|
75
76
|
// via `.toString()` into the standalone browser IIFE below, the same
|
|
@@ -102,8 +103,11 @@ export function tweenStep(tween, now) {
|
|
|
102
103
|
return { ...point, t, done: false };
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
/** A fresh tween from `from` to `to`, starting at `now`. Pure
|
|
106
|
-
|
|
106
|
+
/** A fresh tween from `from` to `to`, starting at `now`. Pure — the default
|
|
107
|
+
* duration is written out rather than read from `TWEEN_DURATION_MS`, because
|
|
108
|
+
* the browser runs a `.toString()` copy of this function in a script that
|
|
109
|
+
* declares no such binding. mudiii-viz.test.mjs holds the two together. */
|
|
110
|
+
export function startTween(from, to, now, durationMs = 250) {
|
|
107
111
|
return { from, to, startedAt: now, durationMs };
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -113,7 +117,7 @@ export function startTween(from, to, now, durationMs = TWEEN_DURATION_MS) {
|
|
|
113
117
|
* tweening); `to` is the new destination point. Pure — `tweenStep` is
|
|
114
118
|
* called on the OLD tween at `now` to read the current position before
|
|
115
119
|
* building the new one. */
|
|
116
|
-
export function reseedTween(existingTween, to, now, durationMs =
|
|
120
|
+
export function reseedTween(existingTween, to, now, durationMs = 250) {
|
|
117
121
|
const current = existingTween ? tweenStep(existingTween, now) : null;
|
|
118
122
|
const from = current ? Object.fromEntries(Object.keys(to).map((k) => [k, current[k]])) : to;
|
|
119
123
|
return startTween(from, to, now, durationMs);
|
|
@@ -130,6 +134,69 @@ export function chebyshevDistanceBetweenCells(a, b) {
|
|
|
130
134
|
return Math.max(Math.abs(Number(ma[1]) - Number(mb[1])), Math.abs(Number(ma[2]) - Number(mb[2])));
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
/** Where the camera should aim once the visitor's own drag is applied on top
|
|
138
|
+
* of the rig's aim. `position` is where the rig put the camera, `lookAt` is
|
|
139
|
+
* where the rig wants it pointed, and `offset` is `{ yaw, pitch }` in radians
|
|
140
|
+
* — the drag's accumulated turn, held apart from the rig so a tick that
|
|
141
|
+
* recomputes the rig leaves the visitor's view alone.
|
|
142
|
+
*
|
|
143
|
+
* The offset turns the rig's own aim rather than replacing it, so the view
|
|
144
|
+
* swings with the agent when it turns: in POV that reads as the animal
|
|
145
|
+
* turning its head, which is the whole point of riding one.
|
|
146
|
+
*
|
|
147
|
+
* Pitch is clamped to 1.2 radians (about 69 degrees) either side of level, so
|
|
148
|
+
* a long upward drag can never carry the view past vertical and roll the
|
|
149
|
+
* board upside down. The returned `pitchOffset` is what survived that clamp,
|
|
150
|
+
* which the caller writes back over its own accumulator — otherwise a drag
|
|
151
|
+
* held past the clamp banks up an offset the view never shows, and the next
|
|
152
|
+
* drag back the other way does nothing until it has spent it.
|
|
153
|
+
*
|
|
154
|
+
* Returns `{ x, y, z, pitchOffset }`. Pure, self-contained. */
|
|
155
|
+
export function lookTargetWithOffset(position, lookAt, offset) {
|
|
156
|
+
const MAX_PITCH_RADIANS = 1.2;
|
|
157
|
+
const yaw = Number(offset && offset.yaw) || 0;
|
|
158
|
+
const pitch = Number(offset && offset.pitch) || 0;
|
|
159
|
+
if (!yaw && !pitch) return { x: lookAt.x, y: lookAt.y, z: lookAt.z, pitchOffset: 0 };
|
|
160
|
+
const dx = lookAt.x - position.x;
|
|
161
|
+
const dy = lookAt.y - position.y;
|
|
162
|
+
const dz = lookAt.z - position.z;
|
|
163
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
164
|
+
if (!radius) return { x: lookAt.x, y: lookAt.y, z: lookAt.z, pitchOffset: 0 };
|
|
165
|
+
const basePitch = Math.asin(Math.max(-1, Math.min(1, dy / radius)));
|
|
166
|
+
const aimedPitch = Math.max(-MAX_PITCH_RADIANS, Math.min(MAX_PITCH_RADIANS, basePitch + pitch));
|
|
167
|
+
const aimedYaw = Math.atan2(dx, dz) + yaw;
|
|
168
|
+
const flat = Math.cos(aimedPitch) * radius;
|
|
169
|
+
return {
|
|
170
|
+
x: position.x + flat * Math.sin(aimedYaw),
|
|
171
|
+
y: position.y + radius * Math.sin(aimedPitch),
|
|
172
|
+
z: position.z + flat * Math.cos(aimedYaw),
|
|
173
|
+
pitchOffset: aimedPitch - basePitch,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The town square's own sky, as gradient stops running from straight up
|
|
178
|
+
* (`at` 0) to straight down (`at` 1), each `{ at, color }`.
|
|
179
|
+
*
|
|
180
|
+
* The colours are the page's own `--square-sky`, `--square-horizon`,
|
|
181
|
+
* `--square-stone` and `--square-stone-dark`, in the order the page body's
|
|
182
|
+
* own background already reads them, so the canvas continues the page it
|
|
183
|
+
* sits in rather than cutting a dark hole in it. The board is twelve units
|
|
184
|
+
* across with nothing beyond it, so a camera aimed anywhere off it — the
|
|
185
|
+
* opening follow shot, an overhead orbit, a long look-drag in POV — is
|
|
186
|
+
* looking at this and nothing else.
|
|
187
|
+
*
|
|
188
|
+
* The band sits at 0.5 because that is the horizon in an equirectangular
|
|
189
|
+
* projection, where the caller maps these onto a sphere. Pure,
|
|
190
|
+
* self-contained. */
|
|
191
|
+
export function skyGradientStops() {
|
|
192
|
+
return [
|
|
193
|
+
{ at: 0, color: "#BFE3F0" },
|
|
194
|
+
{ at: 0.46, color: "#E9D9B6" },
|
|
195
|
+
{ at: 0.52, color: "#8C8172" },
|
|
196
|
+
{ at: 1, color: "#59503F" },
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
133
200
|
/** A facing word to a Y rotation in radians. Assumes a model's own local
|
|
134
201
|
* forward is +Z (true for fox and goblin, the two rigs in the current
|
|
135
202
|
* manifest, checked by walking each GLB's own bind-pose bone chain) so a
|
|
@@ -144,7 +211,7 @@ export function yawForFacing(facing) {
|
|
|
144
211
|
/** Whether `agentId` (predator or prey) currently believes an agent of the
|
|
145
212
|
* opposing role — the one belief-derived bit that separates a predator's
|
|
146
213
|
* "chase" rung from "wander", and a prey's "evade" rung from "forage"/
|
|
147
|
-
* "wander"
|
|
214
|
+
* "wander". Read entirely off
|
|
148
215
|
* the tick payload's own `role`/`belief` fields — no rung name travels on
|
|
149
216
|
* the wire, so this reconstructs which rung applied rather than trusting a
|
|
150
217
|
* label. Pure. */
|
|
@@ -319,6 +386,8 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
319
386
|
var startTween = ${startTween.toString()};
|
|
320
387
|
var reseedTween = ${reseedTween.toString()};
|
|
321
388
|
var chebyshevDistanceBetweenCells = ${chebyshevDistanceBetweenCells.toString()};
|
|
389
|
+
var lookTargetWithOffset = ${lookTargetWithOffset.toString()};
|
|
390
|
+
var skyGradientStops = ${skyGradientStops.toString()};
|
|
322
391
|
var yawForFacing = ${yawForFacing.toString()};
|
|
323
392
|
var threatEngagedFor = ${threatEngagedFor.toString()};
|
|
324
393
|
var movementRungFor = ${movementRungFor.toString()};
|
|
@@ -366,8 +435,38 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
366
435
|
// removal (source "ecology") apart from applyTick's own diff no-op
|
|
367
436
|
// (source "diff") without guessing from a vanished mesh alone.
|
|
368
437
|
var removalLog = [];
|
|
438
|
+
var routeLine = null, routeCells = [];
|
|
439
|
+
var flashMesh = null, flashUntil = 0;
|
|
440
|
+
var FLASH_MS = 600;
|
|
441
|
+
// The floor a one-shot flourish is held for when its own clip is shorter,
|
|
442
|
+
// long enough that a bite reads as a bite at the deck's fastest tick.
|
|
443
|
+
var ONE_SHOT_HOLD_MIN_MS = 450;
|
|
444
|
+
var tickRungs = {};
|
|
369
445
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
370
446
|
var cameraTween = null, lookAtTween = null;
|
|
447
|
+
// Where the rig last wanted the camera pointed. Held past the end of its own
|
|
448
|
+
// tween because the visitor's look is re-applied every frame, not only while
|
|
449
|
+
// the tween runs.
|
|
450
|
+
var lookTarget = null;
|
|
451
|
+
// Whether setCamera has rigged the camera at least once. Without it the very
|
|
452
|
+
// first call — boot's own overhead call, which changes nothing — would read
|
|
453
|
+
// as "no change" and leave the camera at its construction position.
|
|
454
|
+
var cameraRigged = false;
|
|
455
|
+
// Whether the visitor has turned or zoomed the overhead view themselves.
|
|
456
|
+
// OrbitControls writes straight to the camera, so a resize cannot tell an
|
|
457
|
+
// orbited view from a rigged one by reading the camera back.
|
|
458
|
+
var overheadOrbited = false;
|
|
459
|
+
// The visitor's own look, turned by dragging the canvas and applied on top
|
|
460
|
+
// of whatever rig the camera mode computes. It survives every tick: a tick
|
|
461
|
+
// re-issues setCamera with the same mode and the same agent, so clearing it
|
|
462
|
+
// there would snap the view back a fifth of a second after every drag.
|
|
463
|
+
var lookOffset = { yaw: 0, pitch: 0 };
|
|
464
|
+
var LOOK_RADIANS_PER_PIXEL = 0.005;
|
|
465
|
+
// How far a pointer may travel and still count as a click on a cell rather
|
|
466
|
+
// than a drag. Without it every look-around also walked the followed agent
|
|
467
|
+
// to whatever cell the drag started over.
|
|
468
|
+
var CLICK_SLOP_PX = 6;
|
|
469
|
+
var drag = null;
|
|
371
470
|
var lastFrameTs = null;
|
|
372
471
|
var booted = false;
|
|
373
472
|
|
|
@@ -404,10 +503,31 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
404
503
|
return true;
|
|
405
504
|
}
|
|
406
505
|
|
|
506
|
+
// Equirectangular, not a flat screen-space backdrop: three maps this onto a
|
|
507
|
+
// sphere around the camera, so the horizon stays put as the visitor turns
|
|
508
|
+
// instead of sliding with the view. Eight pixels wide is enough for a
|
|
509
|
+
// gradient with no horizontal variation, and wraps with no seam.
|
|
510
|
+
function buildSkyTexture() {
|
|
511
|
+
var swatch = document.createElement("canvas");
|
|
512
|
+
swatch.width = 8;
|
|
513
|
+
swatch.height = 256;
|
|
514
|
+
var ctx = swatch.getContext("2d");
|
|
515
|
+
var gradient = ctx.createLinearGradient(0, 0, 0, swatch.height);
|
|
516
|
+
var stops = skyGradientStops();
|
|
517
|
+
for (var i = 0; i < stops.length; i += 1) gradient.addColorStop(stops[i].at, stops[i].color);
|
|
518
|
+
ctx.fillStyle = gradient;
|
|
519
|
+
ctx.fillRect(0, 0, swatch.width, swatch.height);
|
|
520
|
+
var texture = new THREE.CanvasTexture(swatch);
|
|
521
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
522
|
+
if (THREE.SRGBColorSpace) texture.colorSpace = THREE.SRGBColorSpace;
|
|
523
|
+
texture.needsUpdate = true;
|
|
524
|
+
return texture;
|
|
525
|
+
}
|
|
526
|
+
|
|
407
527
|
function setUpScene() {
|
|
408
528
|
scene = new THREE.Scene();
|
|
409
|
-
scene.background =
|
|
410
|
-
camera3 = new THREE.PerspectiveCamera(
|
|
529
|
+
scene.background = buildSkyTexture();
|
|
530
|
+
camera3 = new THREE.PerspectiveCamera(${CAMERA_FIELD_OF_VIEW_DEGREES}, canvasAspect(), 0.1, 500);
|
|
411
531
|
camera3.position.set(0, 8, 8);
|
|
412
532
|
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
|
|
413
533
|
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.2));
|
|
@@ -423,6 +543,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
423
543
|
orbitControls.target.set(0, 0, 0);
|
|
424
544
|
|
|
425
545
|
canvas.addEventListener("pointerdown", onPointerDown);
|
|
546
|
+
canvas.addEventListener("wheel", function () {
|
|
547
|
+
if (cameraState.mode === "overhead") overheadOrbited = true;
|
|
548
|
+
}, { passive: true });
|
|
549
|
+
// On the window, not the canvas: a look-around that runs off the edge of
|
|
550
|
+
// the 3D stage must keep turning, and must still end when the button comes
|
|
551
|
+
// up over the deck.
|
|
552
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
553
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
554
|
+
window.addEventListener("pointercancel", onPointerCancel);
|
|
426
555
|
window.addEventListener("resize", onResize);
|
|
427
556
|
watchFoodPill();
|
|
428
557
|
onResize();
|
|
@@ -445,12 +574,35 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
445
574
|
scene.add(grid);
|
|
446
575
|
}
|
|
447
576
|
|
|
577
|
+
function canvasAspect() {
|
|
578
|
+
return (canvas.clientWidth || 640) / Math.max(1, canvas.clientHeight || 360);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// What the overhead rig has to fit the board into. Read off the LIVE camera
|
|
582
|
+
// so one field of view drives both the projection and the distance chosen to
|
|
583
|
+
// suit it.
|
|
584
|
+
function cameraView() {
|
|
585
|
+
return { aspect: canvasAspect(), fovDegrees: camera3 ? camera3.fov : ${CAMERA_FIELD_OF_VIEW_DEGREES} };
|
|
586
|
+
}
|
|
587
|
+
|
|
448
588
|
function onResize() {
|
|
449
589
|
if (!renderer || !camera3) return;
|
|
450
590
|
var w = canvas.clientWidth || 640, h = canvas.clientHeight || 360;
|
|
451
591
|
renderer.setSize(w, h, false);
|
|
452
|
-
camera3.aspect =
|
|
592
|
+
camera3.aspect = canvasAspect();
|
|
453
593
|
camera3.updateProjectionMatrix();
|
|
594
|
+
refitOverheadCamera();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// A new canvas shape fits a different amount of board, so the overhead rig is
|
|
598
|
+
// recomputed for it — a phone turned on its side otherwise keeps the height
|
|
599
|
+
// the old shape earned and crops the board's edges. A visitor who has already
|
|
600
|
+
// orbited keeps the view they made: pulling it back to the board's centre
|
|
601
|
+
// would undo their own gesture.
|
|
602
|
+
function refitOverheadCamera() {
|
|
603
|
+
if (!cameraRigged || cameraState.mode !== "overhead" || overheadOrbited) return;
|
|
604
|
+
var rig = cameraRigFor("overhead", null, GRID_SIZE, cameraView());
|
|
605
|
+
if (rig) tweenCameraToRig(rig, performance.now());
|
|
454
606
|
}
|
|
455
607
|
|
|
456
608
|
function pointFromEvent(evt) {
|
|
@@ -461,11 +613,46 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
461
613
|
};
|
|
462
614
|
}
|
|
463
615
|
|
|
616
|
+
// A press starts a drag rather than resolving a cell. Which of the two it
|
|
617
|
+
// turns out to be is only known when the button comes up, and until then
|
|
618
|
+
// pointermove turns the camera.
|
|
619
|
+
function onPointerDown(evt) {
|
|
620
|
+
if (!scene || !camera3 || !groundMesh) return;
|
|
621
|
+
drag = { pointerId: evt.pointerId, startX: evt.clientX, startY: evt.clientY, lastX: evt.clientX, lastY: evt.clientY, travelled: 0 };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function onPointerMove(evt) {
|
|
625
|
+
if (!drag || (drag.pointerId != null && evt.pointerId !== drag.pointerId)) return;
|
|
626
|
+
var dx = evt.clientX - drag.lastX;
|
|
627
|
+
var dy = evt.clientY - drag.lastY;
|
|
628
|
+
drag.lastX = evt.clientX;
|
|
629
|
+
drag.lastY = evt.clientY;
|
|
630
|
+
drag.travelled += Math.abs(dx) + Math.abs(dy);
|
|
631
|
+
// Overhead is OrbitControls' own drag, and two things turning one camera
|
|
632
|
+
// fight each other.
|
|
633
|
+
if (cameraState.mode === "overhead") { overheadOrbited = true; return; }
|
|
634
|
+
lookOffset.yaw -= dx * LOOK_RADIANS_PER_PIXEL;
|
|
635
|
+
lookOffset.pitch -= dy * LOOK_RADIANS_PER_PIXEL;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function onPointerUp(evt) {
|
|
639
|
+
if (!drag || (drag.pointerId != null && evt.pointerId !== drag.pointerId)) return;
|
|
640
|
+
var straightLine = Math.abs(evt.clientX - drag.startX) + Math.abs(evt.clientY - drag.startY);
|
|
641
|
+
var wasDrag = drag.travelled > CLICK_SLOP_PX || straightLine > CLICK_SLOP_PX;
|
|
642
|
+
drag = null;
|
|
643
|
+
if (wasDrag) return;
|
|
644
|
+
resolveCellClick(evt);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function onPointerCancel() { drag = null; }
|
|
648
|
+
|
|
464
649
|
// The raycast target is the ground mesh ALONE — never scene.children — so
|
|
465
650
|
// a click a hair off a prop or an agent still resolves to the cell under
|
|
466
651
|
// the cursor rather than hitting whatever mesh happens to sit in front.
|
|
467
|
-
function
|
|
652
|
+
function resolveCellClick(evt) {
|
|
468
653
|
if (!scene || !camera3 || !groundMesh) return;
|
|
654
|
+
var rect = canvas.getBoundingClientRect();
|
|
655
|
+
if (evt.clientX < rect.left || evt.clientX > rect.right || evt.clientY < rect.top || evt.clientY > rect.bottom) return;
|
|
469
656
|
var ndc = pointFromEvent(evt);
|
|
470
657
|
raycaster.setFromCamera(ndc, camera3);
|
|
471
658
|
var hits = raycaster.intersectObject(groundMesh, false);
|
|
@@ -557,10 +744,8 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
557
744
|
}
|
|
558
745
|
}
|
|
559
746
|
|
|
560
|
-
// ---- items: crumbs and morsels, the committed hay bale at
|
|
561
|
-
//
|
|
562
|
-
// matching the on-ground footprint the primitive spheres they replace were
|
|
563
|
-
// already tuned to. ---------------------------------------------------------
|
|
747
|
+
// ---- items: crumbs and morsels, both the committed hay bale, each at
|
|
748
|
+
// whatever targetHeight its own data/mudiii-assets.json row names. ----------
|
|
564
749
|
function itemAssetKeyFor(kind) { return kind === "morsel" ? "food-morsel" : "food-crumb"; }
|
|
565
750
|
|
|
566
751
|
function animateScaleTo(object3D, target, now) {
|
|
@@ -635,21 +820,56 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
635
820
|
// correctly clone a SkinnedMesh, and at this roster size — a handful of
|
|
636
821
|
// foxes and goblins — the extra memory of a separate load per agent is
|
|
637
822
|
// trivial next to that whole class of bug). ---------------------------------
|
|
823
|
+
// The id, drawn to a canvas and hung above the agent as a sprite. Parenting
|
|
824
|
+
// it to the agent's own group means it rides the movement tween with no
|
|
825
|
+
// per-frame bookkeeping, and a sprite always faces the camera, so there is
|
|
826
|
+
// no facing maths either. sizeAttenuation off holds it at a constant size on
|
|
827
|
+
// screen — an id that shrinks to nothing as the camera pulls back is worse
|
|
828
|
+
// than no id at all — which is why the scale below is in screen fractions
|
|
829
|
+
// rather than world units.
|
|
830
|
+
var LABEL_SCREEN_WIDTH = 0.13;
|
|
831
|
+
function makeAgentLabel(id, height) {
|
|
832
|
+
var canvas = document.createElement("canvas");
|
|
833
|
+
canvas.width = 256;
|
|
834
|
+
canvas.height = 64;
|
|
835
|
+
var ctx = canvas.getContext("2d");
|
|
836
|
+
ctx.font = "bold 38px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
837
|
+
ctx.textAlign = "center";
|
|
838
|
+
ctx.textBaseline = "middle";
|
|
839
|
+
ctx.lineWidth = 8;
|
|
840
|
+
ctx.lineJoin = "round";
|
|
841
|
+
ctx.strokeStyle = "rgba(20,16,10,.85)";
|
|
842
|
+
ctx.strokeText(id, 128, 34);
|
|
843
|
+
ctx.fillStyle = "#F3ECDD";
|
|
844
|
+
ctx.fillText(id, 128, 34);
|
|
845
|
+
var material = new THREE.SpriteMaterial({
|
|
846
|
+
map: new THREE.CanvasTexture(canvas), transparent: true, sizeAttenuation: false, depthWrite: false,
|
|
847
|
+
});
|
|
848
|
+
var sprite = new THREE.Sprite(material);
|
|
849
|
+
sprite.scale.set(LABEL_SCREEN_WIDTH, LABEL_SCREEN_WIDTH / 4, 1);
|
|
850
|
+
sprite.position.y = height + 0.3;
|
|
851
|
+
sprite.name = "label-" + id;
|
|
852
|
+
return sprite;
|
|
853
|
+
}
|
|
854
|
+
|
|
638
855
|
function ensureAgent(id, agent) {
|
|
639
856
|
if (agentGroups[id]) return agentGroups[id];
|
|
640
857
|
var kind = roleOfAgentId(id);
|
|
641
858
|
var entry = {
|
|
642
859
|
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
643
860
|
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
|
|
861
|
+
oneShotUntil: 0, model: null,
|
|
644
862
|
};
|
|
645
863
|
entry.group.visible = false;
|
|
646
864
|
scene.add(entry.group);
|
|
647
865
|
agentGroups[id] = entry;
|
|
648
866
|
var asset = manifestByKind[kind];
|
|
867
|
+
entry.group.add(makeAgentLabel(id, asset ? asset.targetHeight : 1));
|
|
649
868
|
if (asset) {
|
|
650
869
|
loadGlbRaw(modelUrlFor(asset.destPath)).then(function (gltf) {
|
|
651
870
|
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
652
871
|
entry.group.add(gltf.scene);
|
|
872
|
+
entry.model = gltf.scene;
|
|
653
873
|
entry.group.visible = true;
|
|
654
874
|
entry.clipMap = asset.clips || {};
|
|
655
875
|
entry.mixer = new THREE.AnimationMixer(gltf.scene);
|
|
@@ -671,6 +891,12 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
671
891
|
// keep blending into every clip after it, forever). Fade it out before
|
|
672
892
|
// any of the guards below can skip the rest of this call.
|
|
673
893
|
if (entry.oneShotAction) {
|
|
894
|
+
// A flourish holds for its own clip length, floored, before anything may
|
|
895
|
+
// fade it out. At the deck's 220ms default the next tick otherwise
|
|
896
|
+
// landed during the wind-up and a bite read as a twitch. This holds the
|
|
897
|
+
// ANIMATION only: the tick that arrived has already moved the agent, so
|
|
898
|
+
// the simulation never waits on a flourish.
|
|
899
|
+
if (performance.now() < entry.oneShotUntil) return;
|
|
674
900
|
entry.oneShotAction.fadeOut(0.15);
|
|
675
901
|
entry.oneShotAction = null;
|
|
676
902
|
}
|
|
@@ -702,6 +928,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
702
928
|
action.fadeIn(0.15).play();
|
|
703
929
|
if (prev) prev.fadeOut(0.15);
|
|
704
930
|
entry.oneShotAction = action;
|
|
931
|
+
var clip = typeof action.getClip === "function" ? action.getClip() : null;
|
|
932
|
+
entry.oneShotUntil = performance.now()
|
|
933
|
+
+ Math.max(ONE_SHOT_HOLD_MIN_MS, clip && clip.duration ? clip.duration * 1000 : 0);
|
|
705
934
|
entry.currentClip = null;
|
|
706
935
|
}
|
|
707
936
|
|
|
@@ -731,7 +960,10 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
731
960
|
}
|
|
732
961
|
if (entry.clipMap) {
|
|
733
962
|
var moving = singleHop;
|
|
734
|
-
|
|
963
|
+
// A hand-driven step is a walk, whatever the agent believes: it did not
|
|
964
|
+
// choose to chase or flee, the visitor chose for it.
|
|
965
|
+
var action = moving && tickRungs[id] === "driven" ? "driven" : currentActionFor(id, lastAgentsById, moving);
|
|
966
|
+
playClip(entry, clipForAction(agent.role, action, entry.clipMap));
|
|
735
967
|
}
|
|
736
968
|
}
|
|
737
969
|
|
|
@@ -766,21 +998,101 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
766
998
|
}
|
|
767
999
|
}
|
|
768
1000
|
|
|
1001
|
+
// ---- the clicked cell, and the route to it --------------------------------
|
|
1002
|
+
// The line follows the cells the world's own exit search returned, never a
|
|
1003
|
+
// straight segment from agent to target: a straight one cuts through
|
|
1004
|
+
// buildings and promises a walk the board would refuse.
|
|
1005
|
+
function clearRoute() {
|
|
1006
|
+
routeCells = [];
|
|
1007
|
+
if (!routeLine) return;
|
|
1008
|
+
if (routeLine.parent) routeLine.parent.remove(routeLine);
|
|
1009
|
+
routeLine.geometry.dispose();
|
|
1010
|
+
routeLine.material.dispose();
|
|
1011
|
+
routeLine = null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function showRoute(cells) {
|
|
1015
|
+
clearRoute();
|
|
1016
|
+
if (!scene || !cells || cells.length < 2) return;
|
|
1017
|
+
var points = [];
|
|
1018
|
+
for (var i = 0; i < cells.length; i += 1) {
|
|
1019
|
+
var world = cellToWorld(cells[i], GRID_SIZE, CELL_SIZE);
|
|
1020
|
+
if (!world) return;
|
|
1021
|
+
points.push(new THREE.Vector3(world.x, 0.09, world.z));
|
|
1022
|
+
}
|
|
1023
|
+
routeLine = new THREE.Line(
|
|
1024
|
+
new THREE.BufferGeometry().setFromPoints(points),
|
|
1025
|
+
new THREE.LineBasicMaterial({ color: 0xd98a2b }),
|
|
1026
|
+
);
|
|
1027
|
+
routeLine.name = "route";
|
|
1028
|
+
scene.add(routeLine);
|
|
1029
|
+
routeCells = cells.slice();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function clearFlash() {
|
|
1033
|
+
if (!flashMesh) return;
|
|
1034
|
+
if (flashMesh.parent) flashMesh.parent.remove(flashMesh);
|
|
1035
|
+
flashMesh.geometry.dispose();
|
|
1036
|
+
flashMesh.material.dispose();
|
|
1037
|
+
flashMesh = null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function flashCell(cell) {
|
|
1041
|
+
if (!scene) return;
|
|
1042
|
+
var world = cellToWorld(cell, GRID_SIZE, CELL_SIZE);
|
|
1043
|
+
if (!world) return;
|
|
1044
|
+
clearFlash();
|
|
1045
|
+
flashMesh = new THREE.Mesh(
|
|
1046
|
+
new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE),
|
|
1047
|
+
new THREE.MeshBasicMaterial({ color: 0xd98a2b, transparent: true, opacity: 0.75 }),
|
|
1048
|
+
);
|
|
1049
|
+
flashMesh.name = "cell-flash";
|
|
1050
|
+
flashMesh.rotation.x = -Math.PI / 2;
|
|
1051
|
+
flashMesh.position.set(world.x, 0.03, world.z);
|
|
1052
|
+
scene.add(flashMesh);
|
|
1053
|
+
flashUntil = performance.now() + FLASH_MS;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
769
1056
|
// ---- camera ---------------------------------------------------------------
|
|
770
1057
|
function agentSnapshotFor(id) {
|
|
771
1058
|
var entry = agentGroups[id];
|
|
772
1059
|
return entry ? { cell: entry.cell, facing: entry.facing } : null;
|
|
773
1060
|
}
|
|
774
1061
|
|
|
1062
|
+
// Both tweens start from where the camera ACTUALLY is, never from the old
|
|
1063
|
+
// tween's own endpoint: the visitor may have dragged or orbited it somewhere
|
|
1064
|
+
// else since, and a tween seeded off the rig alone teleports past that.
|
|
1065
|
+
function tweenCameraToRig(rig, now) {
|
|
1066
|
+
var from = camera3
|
|
1067
|
+
? { x: camera3.position.x, y: camera3.position.y, z: camera3.position.z }
|
|
1068
|
+
: rig.position;
|
|
1069
|
+
cameraTween = startTween(from, rig.position, now, tweenDurationMs);
|
|
1070
|
+
lookAtTween = startTween(lookTarget || rig.lookAt, rig.lookAt, now, tweenDurationMs);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Re-rigs the camera ONLY when the mode or the followed agent actually
|
|
1074
|
+
// changed. Every tick calls this with the state it already had, and re-rigging
|
|
1075
|
+
// on those calls is what snapped a dragged view back a fifth of a second
|
|
1076
|
+
// after every drag, and what undid an overhead orbit a frame after it was
|
|
1077
|
+
// made. Tracking the agent as it moves is refreshCameraTween's job below,
|
|
1078
|
+
// which leaves the visitor's own look alone.
|
|
775
1079
|
function setCamera(state) {
|
|
776
|
-
|
|
1080
|
+
var next = { mode: (state && state.mode) || "overhead", selectedId: (state && state.selectedId) || null };
|
|
1081
|
+
var changed = !cameraRigged || next.mode !== cameraState.mode || next.selectedId !== cameraState.selectedId;
|
|
1082
|
+
cameraState = next;
|
|
1083
|
+
if (orbitControls) orbitControls.enabled = cameraState.mode === "overhead";
|
|
1084
|
+
if (!changed) return;
|
|
1085
|
+
// A new mode or a new agent is a request for a fresh view, so the drag's
|
|
1086
|
+
// own turn goes with the old one.
|
|
1087
|
+
lookOffset.yaw = 0;
|
|
1088
|
+
lookOffset.pitch = 0;
|
|
1089
|
+
overheadOrbited = false;
|
|
777
1090
|
var agent = cameraState.selectedId ? agentSnapshotFor(cameraState.selectedId) : null;
|
|
778
|
-
var
|
|
1091
|
+
var view = cameraView();
|
|
1092
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE, view) || cameraRigFor("overhead", null, GRID_SIZE, view);
|
|
779
1093
|
if (!rig) return;
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
783
|
-
if (orbitControls) orbitControls.enabled = cameraState.mode === "overhead";
|
|
1094
|
+
cameraRigged = true;
|
|
1095
|
+
tweenCameraToRig(rig, performance.now());
|
|
784
1096
|
}
|
|
785
1097
|
|
|
786
1098
|
// Re-tween the camera toward its own rig after a tick lands, in case the
|
|
@@ -790,10 +1102,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
790
1102
|
if (cameraState.mode === "overhead") return;
|
|
791
1103
|
if (!cameraState.selectedId) return;
|
|
792
1104
|
var agent = agentSnapshotFor(cameraState.selectedId);
|
|
793
|
-
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE);
|
|
1105
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE, cameraView());
|
|
794
1106
|
if (!rig) return;
|
|
795
|
-
|
|
796
|
-
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
1107
|
+
tweenCameraToRig(rig, now);
|
|
797
1108
|
}
|
|
798
1109
|
|
|
799
1110
|
// ---- boot / tick / render loop --------------------------------------------
|
|
@@ -817,9 +1128,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
817
1128
|
itemMeshes = {};
|
|
818
1129
|
lastAgentsById = {};
|
|
819
1130
|
lastItemsById = {};
|
|
1131
|
+
tickRungs = {};
|
|
1132
|
+
clearRoute();
|
|
1133
|
+
clearFlash();
|
|
820
1134
|
removalLog = [];
|
|
821
1135
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
822
1136
|
await placeProps((input && input.propPlacements) || []);
|
|
1137
|
+
cameraRigged = false;
|
|
1138
|
+
lookTarget = null;
|
|
1139
|
+
drag = null;
|
|
823
1140
|
setCamera({ mode: "overhead", selectedId: null });
|
|
824
1141
|
booted = true;
|
|
825
1142
|
}
|
|
@@ -829,6 +1146,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
829
1146
|
var now = performance.now();
|
|
830
1147
|
var agents = (tick && tick.agents) || {};
|
|
831
1148
|
var items = (tick && tick.items) || {};
|
|
1149
|
+
tickRungs = (tick && tick.rungs) || {};
|
|
832
1150
|
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
833
1151
|
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
834
1152
|
// Ecology first: an eaten agent/item is already gone from this tick's own
|
|
@@ -859,8 +1177,34 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
859
1177
|
}
|
|
860
1178
|
if (entry.mixer) entry.mixer.update(deltaSec);
|
|
861
1179
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
1180
|
+
if (flashMesh) {
|
|
1181
|
+
var flashLeft = flashUntil - performance.now();
|
|
1182
|
+
if (flashLeft <= 0) clearFlash();
|
|
1183
|
+
else flashMesh.material.opacity = 0.75 * (flashLeft / FLASH_MS);
|
|
1184
|
+
}
|
|
1185
|
+
// A finished tween is dropped rather than re-applied every frame. Left in
|
|
1186
|
+
// place it pinned the camera to its rig point on every single frame, which
|
|
1187
|
+
// is what stopped an overhead orbit from surviving the frame it was made in.
|
|
1188
|
+
if (cameraTween) {
|
|
1189
|
+
var cp = tweenStep(cameraTween, ts);
|
|
1190
|
+
camera3.position.set(cp.x, cp.y, cp.z);
|
|
1191
|
+
if (cp.done) cameraTween = null;
|
|
1192
|
+
}
|
|
1193
|
+
if (lookAtTween) {
|
|
1194
|
+
var lp = tweenStep(lookAtTween, ts);
|
|
1195
|
+
lookTarget = { x: lp.x, y: lp.y, z: lp.z };
|
|
1196
|
+
if (lp.done) lookAtTween = null;
|
|
1197
|
+
}
|
|
1198
|
+
// Follow and pov aim every frame, tween or no tween, because the visitor's
|
|
1199
|
+
// own drag has to keep showing between ticks. Overhead hands the camera to
|
|
1200
|
+
// OrbitControls once its own tween is spent.
|
|
1201
|
+
if (lookTarget && cameraState.mode !== "overhead") {
|
|
1202
|
+
var aimed = lookTargetWithOffset(camera3.position, lookTarget, lookOffset);
|
|
1203
|
+
lookOffset.pitch = aimed.pitchOffset;
|
|
1204
|
+
camera3.lookAt(aimed.x, aimed.y, aimed.z);
|
|
1205
|
+
} else if (lookTarget && lookAtTween) {
|
|
1206
|
+
camera3.lookAt(lookTarget.x, lookTarget.y, lookTarget.z);
|
|
1207
|
+
}
|
|
864
1208
|
if (orbitControls && orbitControls.enabled) orbitControls.update();
|
|
865
1209
|
renderer.render(scene, camera3);
|
|
866
1210
|
}
|
|
@@ -869,6 +1213,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
869
1213
|
boot: boot,
|
|
870
1214
|
applyTick: applyTick,
|
|
871
1215
|
setCamera: setCamera,
|
|
1216
|
+
flashCell: flashCell,
|
|
1217
|
+
showRoute: showRoute,
|
|
1218
|
+
clearRoute: clearRoute,
|
|
1219
|
+
// The route currently drawn, cell by cell — an e2e assertion's read, so
|
|
1220
|
+
// it can check the line follows the board's own exits rather than
|
|
1221
|
+
// counting pixels on a software renderer.
|
|
1222
|
+
routeCellsDrawn: function () { return routeCells.slice(); },
|
|
872
1223
|
cellOf: function (id) {
|
|
873
1224
|
if (agentGroups[id]) return agentGroups[id].cell;
|
|
874
1225
|
if (itemMeshes[id]) return itemMeshes[id].cell;
|
|
@@ -886,10 +1237,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
886
1237
|
// manifest's own targetHeight to compare it against — an e2e assertion's
|
|
887
1238
|
// read, so it goes through the group actually in the scene rather than a
|
|
888
1239
|
// second, locally invented measurement.
|
|
1240
|
+
// Measured through the loaded MODEL, never the whole group: the group
|
|
1241
|
+
// also carries the id label, whose sprite geometry would widen the box
|
|
1242
|
+
// and report a height nobody rendered.
|
|
889
1243
|
meshHeightOf: function (id) {
|
|
890
1244
|
var entry = agentGroups[id];
|
|
891
|
-
if (!entry || !entry.
|
|
892
|
-
var box = new THREE.Box3().setFromObject(entry.
|
|
1245
|
+
if (!entry || !entry.model) return null;
|
|
1246
|
+
var box = new THREE.Box3().setFromObject(entry.model);
|
|
893
1247
|
var size = new THREE.Vector3();
|
|
894
1248
|
box.getSize(size);
|
|
895
1249
|
var asset = manifestByKind[entry.kind];
|
|
@@ -904,6 +1258,23 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
904
1258
|
for (var i = 0; i < removalLog.length; i += 1) if (removalLog[i].id === id) out.push(removalLog[i]);
|
|
905
1259
|
return out;
|
|
906
1260
|
},
|
|
1261
|
+
// How much of the canvas the ground plane actually covers, width and
|
|
1262
|
+
// height, each 0..1 — an e2e assertion's read, so a framing check measures
|
|
1263
|
+
// the projection the visitor sees rather than re-deriving the rig from the
|
|
1264
|
+
// numbers that built it. The four corners are projected through the live
|
|
1265
|
+
// camera, so a cropped board reads over 1 on the axis it runs off.
|
|
1266
|
+
boardFrameFraction: function () {
|
|
1267
|
+
if (!camera3 || !groundMesh) return null;
|
|
1268
|
+
var half = (GRID_SIZE * CELL_SIZE) / 2;
|
|
1269
|
+
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1270
|
+
var corners = [[-half, -half], [-half, half], [half, -half], [half, half]];
|
|
1271
|
+
for (var i = 0; i < corners.length; i += 1) {
|
|
1272
|
+
var ndc = new THREE.Vector3(corners[i][0], 0, corners[i][1]).project(camera3);
|
|
1273
|
+
minX = Math.min(minX, ndc.x); maxX = Math.max(maxX, ndc.x);
|
|
1274
|
+
minY = Math.min(minY, ndc.y); maxY = Math.max(maxY, ndc.y);
|
|
1275
|
+
}
|
|
1276
|
+
return { width: (maxX - minX) / 2, height: (maxY - minY) / 2 };
|
|
1277
|
+
},
|
|
907
1278
|
ready: function () { return booted; },
|
|
908
1279
|
};
|
|
909
1280
|
})();`;
|