PageSourceSearch

https://retroapplejs.github.io/res/EMU_DEVICE_video_THREE.js

js retroapplejs.github.io collected 2026-10-03 09:50:42 UTC 70,586 bytes, 1,993 lines download raw bytes

1//
2// Copyright (c) 2026 Freddy Vandriessche.
3// notice: https://raw.githubusercontent.com/RetroAppleJS/RetroAppleJS.github.io/main/LICENSE.md
4//
5// EMU_DEVICE_video_THREE.js
6//
7// First-phase Apple II 3D monitor renderer for RetroAppleJS.
8//
9// Principle:
10// - keep an existing compatible Apple2Video renderer as the 2D producer;
11// - render that producer into a hidden 560x384 source canvas;
12// - copy/fit that source canvas into a second hidden 560x384 texture canvas;
13// - expose the texture canvas as a THREE.CanvasTexture;
14// - assign the texture to the 3D monitor screen material's map + emissiveMap;
15// - update the texture at a throttled rate, initially 10fps.
16//
17// v5 changes:
18// - keeps the v4 OrbitControls camera binding fix;
19// - keeps the separate THREE_scene include model;
20// - splits the Apple II source canvas from the Three texture canvas;
21// - adds configurable texture padding/fit compensation for screen-mesh UV overscan.
22//
23//
24// Optional configuration before loading this file:
25//
26//   window.Apple2VideoTHREE_CONFIG = { ... };
27// Runtime defaults live only in APPLE2_THREE_CFG_DEFAULT below.
28// The UI and the initialized Three.js scene must read through the cfg helpers,
29// never through separate literal fallbacks.
30
31"use strict";
32
33// Explicit dependency: THREE uses the GPU renderer as its hidden 2D producer.
34// index.html defines Apple2VideoGPU before loading this file.
35var Apple2VideoTHREE_2D = typeof(Apple2VideoGPU) == "function" ? Apple2VideoGPU : null;
36
37var APPLE2_THREE_CFG_DEFAULT = 
38{
39        textureFPS: 10,
40        renderFPS: 50,
41        perspective: 30,
42        orbitControls: false,
43        lightMove: false,
44        lightMoveHandles: true,
45        lightIntensity: 0.45,
46
47        baseMap: true,
48        roughMetalMap: false,        
49        fixedCameraLookAt: true,
50        fixedCameraTargetX: 0,
51        fixedCameraTargetY: 0,
52        fixedCameraTargetZ: 0,
53
54        emissiveColor: 0xFFFFFF,
55        emissiveIntensity: 1,
56        environmentLuminosity: 0,
57        environmentLightIntensity: 0.45,
58
59        // Final orientation from v3/v4.
60        textureFlipY: false,
61        textureMirrorX: false,
62        textureMirrorY: false,
63
64        // v5 overscan/UV compensation.  These are canvas pixels, not Apple II pixels.
65        // The defaults correspond to roughly 7 Apple II pixels horizontally and
66        // 8 Apple II pixels vertically on the doubled 560x384 canvas.
67        texturePadLeft: 14,
68        texturePadTop: 64,
69        texturePadRight: 14,
70        texturePadBottom: 64,
71        texturePadColor: "#000000",
72        textureSmoothing: true,
73
74        eventShield: false,
75        eventShieldPreventDefault: false,
76
77        showGrid: false,
78        showAxes: false,
79        background: 0x000000, // frame background
80        cameraAspectMul: 1
81    };
82
83    function Apple2VideoTHREE_copyConfig()
84    {
85        var cfg = {}, user = window.Apple2VideoTHREE_CONFIG || {};
86        for (var k in APPLE2_THREE_CFG_DEFAULT) cfg[k] = APPLE2_THREE_CFG_DEFAULT[k];
87        for (var u in user) cfg[u] = user[u];
88        return cfg;
89    }
90
91    function Apple2VideoTHREE_clampInt(v, lo, hi)
92    {
93        v = Number(v);
94        if (!isFinite(v)) v = 0;
95        v = Math.round(v);
96        if (v < lo) return lo;
97        if (v > hi) return hi;
98        return v;
99    }
100
101    function Apple2VideoTHREE_getEmbeddedProjectScene()
102    {
103        // Important: a top-level `const THREE_scene = ...` in a non-module
104        // browser script is not necessarily a property of window.  Therefore
105        // check the lexical global first, then fall back to window.THREE_scene.
106        try
107        {
108            if (typeof(THREE_scene) !== "undefined")
109                return THREE_scene;
110        }
111        catch(e) {}
112
113        if (window.THREE_scene !== undefined)
114            return window.THREE_scene;
115
116        return null;
117    }
118
119    function Apple2VideoTHREE_getEmbeddedDefaultImageMaps()
120    {
121        try
122        {
123            if (typeof(default_image_maps) !== "undefined")
124                return default_image_maps;
125        }
126        catch(e) {}
127
128        if (window.default_image_maps !== undefined)
129            return window.default_image_maps;
130
131        return null;
132    }
133
134    function Apple2VideoTHREE_isUsableImageDataUrl(url)
135    {
136        return typeof(url) == "string" &&
137               url.indexOf("data:image/") == 0 &&
138               url.indexOf("...") < 0 &&
139               url.length > 32;
140    }
141
142    function Apple2VideoTHREE_getControlInstance()
143     {
144        if (typeof(oApple2Video) === "undefined" || oApple2Video === null ||
145            typeof(oApple2Video.getRendererInstance) !== "function")
146            return null;
147
148        return oApple2Video.getRendererInstance("threejs", true);
149    }
150
151    function Apple2VideoTHREE_setPerspectiveControl(input)
152    {
153        var renderer3D = Apple2VideoTHREE_getControlInstance();
154        if (!renderer3D || typeof(renderer3D.setPerspective) !== "function") return;
155
156        var value = renderer3D.setPerspective(input.value);
157        input.value = value;
158
159        var output = document.getElementById("threePerspectiveValue");
160        if (output) output.textContent = value + "°";
161    }
162
163    function Apple2VideoTHREE_setLuminosityControl(input)
164    {
165        var renderer3D = Apple2VideoTHREE_getControlInstance();
166        if (!renderer3D || typeof(renderer3D.setEmissiveIntensity) !== "function") return;
167
168        var value = renderer3D.setEmissiveIntensity(Number(input.value) / 100);
169        input.value = Math.round(value * 100);
170
171        var output = document.getElementById("threeLuminosityValue");
172        if (output) output.textContent = value.toFixed(2);
173    }
174
175    function Apple2VideoTHREE_setOrbitControl(input)
176    {
177        var renderer3D = Apple2VideoTHREE_getControlInstance();
178        if (!renderer3D || typeof(renderer3D.setOrbitEnabled) !== "function") return;
179
180        input.checked = renderer3D.setOrbitEnabled(input.checked);
181    }
182
183    function Apple2VideoTHREE_setTextureSmoothingControl(input)
184    {
185        var renderer3D = Apple2VideoTHREE_getControlInstance();
186        if (!renderer3D || typeof(renderer3D.setTextureSmoothing) !== "function") return;
187
188        input.checked = renderer3D.setTextureSmoothing(input.checked);
189    }
190
191    function Apple2VideoTHREE_setLightMoveControl(input)
192    {
193        var renderer3D = Apple2VideoTHREE_getControlInstance();
194        if (!renderer3D || typeof(renderer3D.setLightMoveEnabled) !== "function") return;
195
196        input.checked = renderer3D.setLightMoveEnabled(input.checked);
197    }
198
199    function Apple2VideoTHREE_syncLightIntensityControl(value)
200    {
201        var input = document.getElementById("threeLightIntensity");
202        var output = document.getElementById("threeLightIntensityValue");
203
204        value = Number(value);
205        if (!isFinite(value)) value = 0;
206
207        if (input) input.value = value;
208        if (output) output.textContent = value.toFixed(2);
209    }
210
211    function Apple2VideoTHREE_setLightIntensityControl(input)
212    {
213        var renderer3D = Apple2VideoTHREE_getControlInstance();
214        if (!renderer3D || typeof(renderer3D.setLightIntensity) !== "function") return;
215
216        var value = renderer3D.setLightIntensity(Number(input.value));
217        Apple2VideoTHREE_syncLightIntensityControl(value);
218    }
219
220    function Apple2VideoTHREE_setEnvironmentControl(input)
221    {
222        var renderer3D = Apple2VideoTHREE_getControlInstance();
223        if (!renderer3D || typeof(renderer3D.setEnvironmentLuminosity) !== "function") return;
224
225        var value = renderer3D.setEnvironmentLuminosity(Number(input.value) / 100);
226        input.value = Math.round(value * 100);
227
228        var output = document.getElementById("threeEnvironmentValue");
229        if (output) output.textContent = Math.round(value * 100) + "%";
230    }
231
232    function Apple2VideoTHREE_resetCameraControl()
233    {
234        var renderer3D = Apple2VideoTHREE_getControlInstance();
235        if (!renderer3D || typeof(renderer3D.resetCameraPosition) !== "function") return;
236
237        renderer3D.resetCameraPosition();
238    }
239
240    function Apple2VideoTHREE_resetLightControl()
241    {
242        var renderer3D = Apple2VideoTHREE_getControlInstance();
243        if (!renderer3D || typeof(renderer3D.resetLightPosition) !== "function") return;
244
245        renderer3D.resetLightPosition();
246        if (typeof(renderer3D.getLightIntensity) === "function")
247            Apple2VideoTHREE_syncLightIntensityControl(renderer3D.getLightIntensity());
248
249    }
250
251    function Apple2VideoTHREE_setBaseMapControl(input)
252    {
253        var renderer3D = Apple2VideoTHREE_getControlInstance();
254        if (!renderer3D || typeof(renderer3D.setBaseMapEnabled) !== "function") return;
255
256        input.checked = renderer3D.setBaseMapEnabled(input.checked);
257    }
258
259    function Apple2VideoTHREE_setRoughMetalControl(input)
260    {
261        var renderer3D = Apple2VideoTHREE_getControlInstance();
262        if (!renderer3D || typeof(renderer3D.setRoughMetalEnabled) !== "function") return;
263
264        input.checked = renderer3D.setRoughMetalEnabled(input.checked);
265    }
266
267    function Apple2VideoTHREE_controls(renderer3D)
268    {
269        var fov = renderer3D && typeof(renderer3D.getPerspective) === "function"
270            ? renderer3D.getPerspective()
271            : APPLE2_THREE_CFG_DEFAULT.perspective;
272        var luminosity = renderer3D && typeof(renderer3D.getEmissiveIntensity) === "function"
273            ? renderer3D.getEmissiveIntensity()
274            : 1;
275        var orbit = renderer3D && typeof(renderer3D.getOrbitEnabled) === "function"
276            ? renderer3D.getOrbitEnabled()
277            : !!APPLE2_THREE_CFG_DEFAULT.orbitControls;
278        var lightMove = renderer3D && typeof(renderer3D.getLightMoveEnabled) === "function"
279            ? renderer3D.getLightMoveEnabled()
280            : !!APPLE2_THREE_CFG_DEFAULT.lightMove;
281        var lightIntensity = renderer3D && typeof(renderer3D.getLightIntensity) === "function"
282            ? renderer3D.getLightIntensity()
283            : APPLE2_THREE_CFG_DEFAULT.lightIntensity;
284        var baseMap = renderer3D && typeof(renderer3D.getBaseMapEnabled) === "function"
285            ? renderer3D.getBaseMapEnabled()
286            : !!APPLE2_THREE_CFG_DEFAULT.baseMap;
287        var roughMetal = renderer3D && typeof(renderer3D.getRoughMetalEnabled) === "function"
288            ? renderer3D.getRoughMetalEnabled()
289            : !!APPLE2_THREE_CFG_DEFAULT.roughMetalMap;
290        var smoothing = renderer3D && typeof(renderer3D.getTextureSmoothing) === "function"
291            ? renderer3D.getTextureSmoothing()
292            : !!APPLE2_THREE_CFG_DEFAULT.textureSmoothing;
293
294        return "<div class=\"appbut mini\">"
295            +"<input id=\"threePerspective\" type=\"range\" min=\"5\" max=\"90\" step=\"1\" value=\""+Math.round(fov)+"\" class=\"slider\""
296            +" oninput=\"Apple2VideoTHREE_setPerspectiveControl(this)\" style=\"width:65px;float:left\"></input>"
297            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">perspective "
298            +"<span id=\"threePerspectiveValue\">"+Math.round(fov)+"°</span></div>"
299            +"</div><br>"
300            
301            +"<div class=\"appbut mini\">"
302            +"<input id=\"threeLuminosity\" type=\"range\" min=\"0\" max=\"300\" step=\"5\" value=\""+Math.round(luminosity*100)+"\" class=\"slider\""
303            +" oninput=\"Apple2VideoTHREE_setLuminosityControl(this)\" style=\"width:65px;float:left\"></input>"
304            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">screen luminosity "
305            +"<span id=\"threeLuminosityValue\">"+Number(luminosity).toFixed(2)+"</span></div>"
306            +"</div><br>"
307
308            +"<div class=\"appbut mini\">"
309            +"<input id=\"threeBaseMap\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setBaseMapControl(this)\""
310            +(baseMap ? " checked" : "")
311            +" style=\"width:65px;float:left\"></input>"
312            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">base material</div>"
313            +"</div><br>"
314
315            +"<div class=\"appbut mini\">"
316            +"<input id=\"threeRoughMetal\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setRoughMetalControl(this)\""
317            +(roughMetal ? " checked" : "")
318            +" style=\"width:65px;float:left\"></input>"
319            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">rough/metal</div>"
320            +"</div><br>"
321
322            +"<div class=\"appbut mini\">"
323            +"<input id=\"threeOrbit\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setOrbitControl(this)\""
324            +(orbit ? " checked" : "")
325            +" style=\"width:65px;float:left\"></input>"
326             +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">orbit camera"
327             +"&nbsp;|&nbsp;<button type=\"button\" class=\"appbut mini\" onclick=\"Apple2VideoTHREE_resetCameraControl()\">reset</button>"
328             +"</div>"
329            +"</div><br>"
330
331            +"<div class=\"appbut mini\">"
332            +"<input id=\"threeMoveLight\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setLightMoveControl(this)\""
333            +(lightMove ? " checked" : "")
334            +" style=\"width:65px;float:left\"></input>"
335            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">move light"
336            +"&nbsp;|&nbsp;<button type=\"button\" class=\"appbut mini\" onclick=\"Apple2VideoTHREE_resetLightControl()\">reset</button>"
337            +"</div>"
338            +"</div><br>"
339
340            +"<div class=\"appbut mini\">"
341            +"<input id=\"threeLightIntensity\" type=\"range\" min=\"0\" max=\"5\" step=\"0.05\" value=\""+Number(lightIntensity).toFixed(2)+"\" class=\"slider\""
342            +" oninput=\"Apple2VideoTHREE_setLightIntensityControl(this)\" style=\"width:65px;float:left\"></input>"
343            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">light intensity "
344            +"<span id=\"threeLightIntensityValue\">"+Number(lightIntensity).toFixed(2)+"</span></div>"
345            +"</div><br>"
346
347            +"<div class=\"appbut mini\">"
348            +"<input id=\"threeTextureSmoothing\" type=\"checkbox\" onchange=\"Apple2VideoTHREE_setTextureSmoothingControl(this)\""
349            +(smoothing ? " checked" : "")
350            +" style=\"width:65px;float:left\"></input>"
351            +"<div style=\"float:left;border:0px solid;padding:2px 0px 0px 10px;\">smooth texture</div>"
352            +"</div><br>"          
353    }
354
355    function Apple2VideoTHREE(canvas)
356    {
357        this.id = {
358             "DCODE":"A2THR"
359            ,"coID":"Apple2Video"
360            ,"hostPCODE":"A2BO"
361            ,"deviceIdx":2
362            ,"icon":"fa fa-eye"
363            ,"description":"Apple II Three.js video"
364            ,"deviceEnable":true
365        };
366
367
368        const DISPLAY_W = 560;
369        const DISPLAY_H = 384;
370
371        var video3D = this;
372        var cfg = Apple2VideoTHREE_copyConfig();
373
374        var renderLoopStarted = false;
375        var resizeListenerInstalled = false;
376        var lastTextureUpdate = 0;
377        var lastRenderUpdate = 0;
378        var sceneLoadStarted = false;
379
380        this.ctx = canvas || null;            // For Apple2Plus this is the visible canvas.
381        this.canvas = canvas || null;
382        this.vidram = null;                  // Apple2Hw assigns this after construction.
383        this.hw = null;
384
385        this.video2D = null;                 // Previous renderer, usually EMU_DEVICE_video_GPU.js.
386        this.serial8 = new Uint8Array();
387
388        this.renderer = null;
389        this.scene = null;
390        this.camera = null;
391        this.controls = null;
392        this.controlsCamera = null;
393        this.screenMesh = null;
394        this.screenTexture = null;
395
396        this.defaultCameraState = null;
397        this.defaultLightState = null;
398        this.sceneBounds = null;
399        this.environmentLight = null;
400        this.defaultMaterialTextures = {};
401
402        this.lightMoveEnabled = !!cfg.lightMove;
403        this.activeSceneLight = null;
404        this.lightTransformControls = null;
405        this.lightTransformHelper = null;
406        this.lightMovePlane = null;
407        this.lightMoveRaycaster = null;
408        this.lightMoveNdc = null;
409        this.lightMovePoint = null;
410        this._lightMovePointerHandler = function(event)
411        {
412            video3D.onLightPointerMove(event);
413        };
414        this._lightMovePreviousCursor = "";
415
416        this.textureDirty = true;
417        this.ready = false;
418        this._eventShieldInstalled = false;
419
420        // v5: source canvas receives the Apple II GPU.js output.
421        this.videoCanvas = document.createElement("canvas");
422        this.videoCanvas.width = DISPLAY_W;
423        this.videoCanvas.height = DISPLAY_H;
424        this.videoCanvas.style.width = DISPLAY_W + "px";
425        this.videoCanvas.style.height = DISPLAY_H + "px";
426        this.videoCanvas.style.display = "none";
427
428        // v5: texture canvas is what Three.js uploads.  We copy the source canvas
429        // into this canvas with optional padding/fit compensation.
430        this.textureCanvas = document.createElement("canvas");
431        this.textureCanvas.width = DISPLAY_W;
432        this.textureCanvas.height = DISPLAY_H;
433        this.textureCanvas.style.width = DISPLAY_W + "px";
434        this.textureCanvas.style.height = DISPLAY_H + "px";
435        this.textureCanvas.style.display = "none";
436        this.textureCtx = this.textureCanvas.getContext("2d", {alpha:false});
437
438        // Apple2Plus uses this test:
439        //   new Apple2Video().initGPU === undefined ? pass 2D context : pass canvas
440        // So this driver intentionally exposes initGPU, even though Three.js is not GPU.js.
441        this.initGPU = function()
442        {
443            return true;
444        };
445
446        this.getSceneNumber = function(key,lo,hi)
447        {
448            var value = Number(cfg[key]);
449            if (!isFinite(value))
450                value = Number(APPLE2_THREE_CFG_DEFAULT[key]);
451            if (!isFinite(value))
452                value = 0;
453
454            if (lo !== undefined && value < lo) value = lo;
455            if (hi !== undefined && value > hi) value = hi;
456            return value;
457        };
458
459        this.getSceneBoolean = function(key)
460        {
461            var value = cfg[key];
462            if (value === undefined)
463                value = APPLE2_THREE_CFG_DEFAULT[key];
464            return !!value;
465        };
466
467        this.ensure2DRenderer = function()
468        {
469            if (this.video2D) return true;
470            if (typeof(Apple2VideoTHREE_2D) !== "function" || Apple2VideoTHREE_2D === Apple2VideoTHREE)
471            {
472                console.warn("Apple2VideoTHREE: Apple2VideoGPU is unavailable. Load EMU_DEVICE_video_GPU.js before EMU_DEVICE_video_THREE.js.");
473                return false;
474            }
475
476            this.video2D = new Apple2VideoTHREE_2D(this.videoCanvas);
477            this.video2D.vidram = this.vidram;
478            this.video2D.hw = this.hw;
479
480            return true;
481        };
482
483        this.sync2DLinks = function()
484        {
485            if (!this.video2D) return;
486
487            this.video2D.vidram = this.vidram;
488            this.video2D.hw = this.hw;
489
490            // Expose the 2D renderer's
491            // serial buffer and delegate kernel calls back to it.
492            if (this.video2D.serial8)
493                this.serial8 = this.video2D.serial8;
494
495            this.kernel = function(mem, rom, serial8)
496            {
497                if (!video3D.video2D || typeof(video3D.video2D.kernel) !== "function") return;
498
499                var r = video3D.video2D.kernel(mem, rom, serial8);
500                video3D.textureDirty = true;
501                return r;
502            };
503        };
504
505        this.kernel = function()
506        {
507            console.warn("Apple2VideoTHREE: 2D kernel is not initialised yet.");
508        };
509
510        this.reset = function()
511        {
512            this.textureDirty = true;
513            if (this.ensure2DRenderer())
514            {
515                this.video2D.reset();
516                this.sync2DLinks();
517            }
518            this.ensureTHREE();
519        };
520
521        this.cycle = function(ticks)
522        {
523            if (this.video2D && typeof(this.video2D.cycle) === "function")
524            {
525                this.video2D.cycle(ticks);
526
527                // Since EMU_DEVICE_video_GPU.js now calls its own kernel directly,
528                // Apple2VideoTHREE no longer sees the kernel call.
529                // Keep the Three texture upload loop alive while this renderer is active.
530                this.textureDirty = true;
531            }
532        };
533
534        this.redraw = function()
535        {
536            if (this.video2D && typeof(this.video2D.redraw) === "function")
537                this.video2D.redraw();
538            this.textureDirty = true;
539        };
540
541        this.write = function(addr, d8)
542        {
543            if (this.video2D && typeof(this.video2D.write) === "function")
544            {
545                this.video2D.vidram = this.vidram;
546                this.video2D.hw = this.hw;
547                this.video2D.write(addr, d8);
548                this.textureDirty = true;
549            }
550        };
551
552        this.setGfx = function(flag)
553        {
554            if (this.video2D && typeof(this.video2D.setGfx) === "function")
555                this.video2D.setGfx(flag);
556            this.sync2DLinks();
557        };
558
559        this.setMix = function(flag)
560        {
561            if (this.video2D && typeof(this.video2D.setMix) === "function")
562                this.video2D.setMix(flag);
563            this.sync2DLinks();
564        };
565
566        this.setPage2 = function(flag)
567        {
568            if (this.video2D && typeof(this.video2D.setPage2) === "function")
569                this.video2D.setPage2(flag);
570            this.sync2DLinks();
571        };
572
573        this.setHires = function(flag)
574        {
575            if (this.video2D && typeof(this.video2D.setHires) === "function")
576                this.video2D.setHires(flag);
577            this.sync2DLinks();
578        };
579
580        this.setMonitor = function(mode)
581        {
582            var ret = {"color":"#000000", "name":"THREE"};
583
584            if (this.video2D && typeof(this.video2D.setMonitor) === "function")
585                ret = this.video2D.setMonitor(mode);
586
587            this.sync2DLinks();
588            this.textureDirty = true;
589            return ret;
590        };
591
592        this.getChromeMode = function()
593        {
594            if (this.video2D && typeof(this.video2D.getChromeMode) === "function")
595                return this.video2D.getChromeMode();
596            return 0;
597        };
598
599        this.getloresCols = function()
600        {
601            if (this.video2D && typeof(this.video2D.getloresCols) === "function")
602                return this.video2D.getloresCols();
603            return [];
604        };
605
606        this.gethiresCols = function()
607        {
608            if (this.video2D && typeof(this.video2D.gethiresCols) === "function")
609                return this.video2D.gethiresCols();
610            return [];
611        };
612
613        this.setCol = function(idx, column, val)
614        {
615            if (this.video2D && typeof(this.video2D.setCol) === "function")
616            {
617                var ret = this.video2D.setCol(idx, column, val);
618                this.textureDirty = true;
619                return ret;
620            }
621            return val;
622        };
623
624        this.addrVisible = function(addr)
625        {
626            if (this.video2D && typeof(this.video2D.addrVisible) === "function")
627                return this.video2D.addrVisible(addr);
628            return true;
629        };
630
631        this.ensureTHREE = function()
632        {
633            if (this.renderer)
634                return true;
635
636            if (typeof(THREE) === "undefined")
637            {
638                console.warn("Apple2VideoTHREE: THREE is not loaded. Add three.min.js before EMU_DEVICE_video_THREE.js.");
639                return false;
640            }
641
642            if (!this.canvas)
643            {
644                console.warn("Apple2VideoTHREE: no visible canvas was supplied.");
645                return false;
646            }
647
648            this.prepareVisibleCanvas();
649
650            this.renderer = new THREE.WebGLRenderer({
651                canvas: this.canvas,
652                antialias: true,
653                alpha: false
654            });
655
656            this.renderer.setPixelRatio(window.devicePixelRatio || 1);
657            this.resizeRenderer();
658            this.resizeTextureCanvas();
659
660            this.screenTexture = new THREE.CanvasTexture(this.textureCanvas);
661            this.applyInitialTextureSmoothing();
662
663            this.buildFallbackScene();
664            this.loadProjectScene();
665            this.installResizeListener();
666            this.startRenderLoop();
667
668            return true;
669        };
670
671        this.prepareVisibleCanvas = function()
672        {
673            if (!this.canvas) return;
674
675            if (!this.canvas.width)  this.canvas.width = DISPLAY_W;
676            if (!this.canvas.height) this.canvas.height = DISPLAY_H;
677
678            if (this.canvas.style)
679            {
680                if (!this.canvas.style.width)  this.canvas.style.width = DISPLAY_W + "px";
681                if (!this.canvas.style.height) this.canvas.style.height = DISPLAY_H + "px";
682                this.canvas.style.display = "block";
683                this.canvas.style.background = "#000";
684                this.canvas.style.pointerEvents = "auto";
685                this.canvas.style.touchAction = "none";
686                this.canvas.style.userSelect = "none";
687            }
688        };
689
690        this.resizeRenderer = function()
691        {
692            if (!this.renderer || !this.canvas) return;
693
694            var w = this.canvas.clientWidth  || this.canvas.width  || DISPLAY_W;
695            var h = this.canvas.clientHeight || this.canvas.height || DISPLAY_H;
696
697            this.renderer.setSize(w, h, false);
698
699            if (this.camera && this.camera.isPerspectiveCamera)
700            {
701                this.camera.aspect = (w * cfg.cameraAspectMul) / h;
702                this.camera.updateProjectionMatrix();
703            }
704        };
705
706        this.configureRendererFromProject = function(project)
707        {
708            if (!this.renderer || !project) return;
709
710            this.renderer.shadowMap.enabled = Boolean(project.shadows);
711            this.renderer.shadowMap.type = (project.shadowType === 1) ?
712                THREE.PCFShadowMap : THREE.PCFSoftShadowMap;
713
714            this.renderer.toneMapping = (project.toneMapping === 1) ? THREE.ReinhardToneMapping :
715                                        (project.toneMapping === 2) ? THREE.CineonToneMapping :
716                                        (project.toneMapping === 3) ? THREE.ACESFilmicToneMapping :
717                                                                     THREE.LinearToneMapping;
718
719            this.renderer.toneMappingExposure = project.toneMappingExposure || 1;
720        };
721
722        this.getPerspective = function()
723        {
724            return this.camera && this.camera.isPerspectiveCamera
725                ? this.camera.fov
726                : this.getSceneNumber("perspective",5,90);
727        };
728
729        this.updateSceneBounds = function()
730        {
731            if (!this.scene) return null;
732
733            this.scene.updateMatrixWorld(true);
734            var box = new THREE.Box3().setFromObject(this.scene);
735            if (box.isEmpty())
736            {
737                this.sceneBounds = null;
738                return null;
739            }
740
741            var size = box.getSize(new THREE.Vector3());
742            if (!isFinite(size.length()) || size.lengthSq()===0)
743            {
744                this.sceneBounds = null;
745                return null;
746            }
747
748            this.sceneBounds = box;
749            return box;
750        };
751
752        this.getSceneCenter = function()
753        {
754            var box = this.sceneBounds || this.updateSceneBounds();
755            if (box)
756                return box.getCenter(new THREE.Vector3());
757
758            return new THREE.Vector3(
759                cfg.fixedCameraTargetX || 0,
760                cfg.fixedCameraTargetY || 0,
761                cfg.fixedCameraTargetZ || 0
762            );
763        };
764
765        this.getCameraTarget = function()
766        {
767            if (this.controls && this.controls.target)
768                return this.controls.target.clone();
769
770            return this.getSceneCenter();
771        };
772
773        this.captureDefaultCameraState = function(camera,target)
774        {
775            if (!camera) return;
776            target = target ? target.clone() : this.getSceneCenter();
777
778            this.defaultCameraState = {
779                 "position":camera.position.clone()
780                ,"quaternion":camera.quaternion.clone()
781                ,"up":camera.up.clone()
782                ,"zoom":camera.zoom
783                ,"fov":camera.isPerspectiveCamera ? camera.fov : 30
784                ,"target":target
785            };
786        };
787
788        this.resetCameraPosition = function()
789        {
790            if (!this.camera)
791                this.ensureTHREE();
792
793            var state = this.defaultCameraState;
794            if (!this.camera || !state) return false;
795
796            var direction = state.position.clone().sub(state.target);
797            var distance = direction.length();
798
799            if (this.camera.isPerspectiveCamera && distance > 0)
800            {
801                var currentFov = Math.max(1,this.camera.fov);
802                distance *= Math.tan(state.fov*Math.PI/360) /
803                            Math.tan(currentFov*Math.PI/360);
804            }
805
806            if (direction.lengthSq() > 0)
807                direction.setLength(distance);
808
809            this.camera.position.copy(state.target).add(direction);
810            this.camera.quaternion.copy(state.quaternion);
811            this.camera.up.copy(state.up);
812            this.camera.zoom = state.zoom;
813            this.camera.updateProjectionMatrix();
814            this.camera.updateMatrix();
815            this.camera.updateMatrixWorld(true);
816
817            if (this.controls)
818            {
819                this.controls.target.copy(state.target);
820                this.controls.update();
821            }
822
823            return true;
824        };
825
826        this.setPerspective = function(fov)
827        {
828            fov = Number(fov);
829            if (!isFinite(fov)) fov = this.getPerspective();
830            fov = Math.max(5,Math.min(90,fov));
831            cfg.perspective = fov;
832
833            if (!this.camera)
834                this.ensureTHREE();
835
836            if (this.camera && this.camera.isPerspectiveCamera)
837            {
838                var oldFov = this.camera.fov || 30;
839                var target = this.getCameraTarget();
840
841                if (Math.abs(fov-oldFov)>1e-6)
842                {
843                    var viewDir = new THREE.Vector3().subVectors(this.camera.position,target);
844                    var oldDistance = viewDir.length();
845
846                    if (oldDistance>1e-6)
847                    {
848                        var oldFovRad = THREE.MathUtils.degToRad(oldFov);
849                        var newFovRad = THREE.MathUtils.degToRad(fov);
850                        var visibleHeightAtTarget = 2*oldDistance*Math.tan(oldFovRad*0.5);
851                        var newDistance = visibleHeightAtTarget/(2*Math.tan(newFovRad*0.5));
852
853                        viewDir.normalize();
854                        this.camera.position.copy(target).add(viewDir.multiplyScalar(newDistance));
855                        this.camera.near = Math.max(newDistance/1000,0.001);
856                        this.camera.far = Math.max(newDistance*1000,1000);
857                    }
858                }
859
860                var w = this.canvas ? (this.canvas.clientWidth || this.canvas.width || DISPLAY_W) : DISPLAY_W;
861                var h = this.canvas ? (this.canvas.clientHeight || this.canvas.height || DISPLAY_H) : DISPLAY_H;
862
863                this.camera.fov = fov;
864                this.camera.aspect = (w*cfg.cameraAspectMul)/Math.max(1,h);
865                this.camera.updateProjectionMatrix();
866                this.camera.lookAt(target);
867                this.camera.updateMatrixWorld(true);
868
869                if (this.controls)
870                {
871                    this.controls.target.copy(target);
872                    this.controls.update();
873                }
874            }
875
876            return fov;
877        };
878
879        this.getEmissiveIntensity = function()
880        {
881            return Number(cfg.emissiveIntensity) || 0;
882        };
883
884        this.setEmissiveIntensity = function(intensity)
885        {
886            intensity = Number(intensity);
887            if (!isFinite(intensity)) intensity = this.getEmissiveIntensity();
888            intensity = Math.max(0, Math.min(5, intensity));
889            cfg.emissiveIntensity = intensity;
890
891            if (!this.scene)
892                this.ensureTHREE();
893
894            var screen = this.screenMesh || this.findScreenMesh();
895            if (screen)
896            {
897                var materials = Array.isArray(screen.material) ? screen.material : [screen.material];
898                for (var i=0;i<materials.length;i++)
899                {
900                    var material = materials[i];
901                    if (!material || material.emissive === undefined) continue;
902                    material.emissiveIntensity = intensity;
903                    material.needsUpdate = true;
904                }
905            }
906
907            return intensity;
908        };
909
910        this.getEnvironmentLuminosity = function()
911        {
912            var value = Number(cfg.environmentLuminosity);
913            if (!isFinite(value)) value = 0;
914            return Math.max(0,Math.min(1,value));
915        };
916
917        this.applyEnvironmentLuminosity = function()
918        {
919            if (!this.scene) return this.getEnvironmentLuminosity();
920
921            var value = this.getEnvironmentLuminosity();
922            this.scene.background = new THREE.Color(value,value,value);
923
924            if (!this.environmentLight || this.environmentLight.parent!==this.scene)
925            {
926                this.environmentLight = this.scene.getObjectByName("__Apple2VideoTHREE_environment");
927                if (!this.environmentLight)
928                {
929                    this.environmentLight = new THREE.AmbientLight(0xffffff,0);
930                    this.environmentLight.name = "__Apple2VideoTHREE_environment";
931                    this.scene.add(this.environmentLight);
932                }
933            }
934
935            var gain = Number(cfg.environmentLightIntensity);
936            if (!isFinite(gain)) gain = 0.45;
937            this.environmentLight.intensity = value*Math.max(0,gain);
938
939            return value;
940        };
941
942        this.setEnvironmentLuminosity = function(value)
943        {
944            value = Number(value);
945            if (!isFinite(value)) value = this.getEnvironmentLuminosity();
946            cfg.environmentLuminosity = Math.max(0,Math.min(1,value));
947
948            if (!this.scene)
949                this.ensureTHREE();
950
951            return this.applyEnvironmentLuminosity();
952        };
953
954        this.getBaseMapEnabled = function()
955        {
956            return !!cfg.baseMap;
957        };
958
959        this.setBaseMapEnabled = function(flag)
960        {
961            cfg.baseMap = !!flag;
962            this.applyScreenTextureMaterialSlots();
963            return cfg.baseMap;
964        };
965
966        this.getRoughMetalEnabled = function()
967        {
968            return !!cfg.roughMetalMap;
969        };
970
971        this.setRoughMetalEnabled = function(flag)
972        {
973            cfg.roughMetalMap = !!flag;
974            this.applyScreenTextureMaterialSlots();
975            return cfg.roughMetalMap;
976        };
977
978        this.applyScreenMaterialProfile = function(mat)
979        {
980            if (!mat) return false;
981
982            // Mirrors the scalar part of tools/GUI_DEV/THREEJS_loader.html
983            // after APPLY BASE. Texture slots are managed separately below.
984            if (mat.color) mat.color.set(0xffffff);
985            if (mat.roughness !== undefined) mat.roughness = 1;
986            if (mat.metalness !== undefined) mat.metalness = 0;
987            if (mat.envMapIntensity !== undefined) mat.envMapIntensity = 0.45;
988
989            mat.side = THREE.DoubleSide;
990            mat.needsUpdate = true;
991            return true;
992        };
993
994        this.applyDefaultTextureProfile = function(texture,slot)
995        {
996            if (!texture) return texture;
997
998            var srgb = (slot == "map" || slot == "emissiveMap");
999            texture.mapping = THREE.UVMapping;
1000            texture.wrapS = THREE.RepeatWrapping;
1001            texture.wrapT = THREE.RepeatWrapping;
1002            texture.repeat.set(1,1);
1003            texture.offset.set(0,0);
1004            texture.center.set(0,0);
1005            texture.rotation = 0;
1006            texture.format = THREE.RGBAFormat;
1007            texture.type = THREE.UnsignedByteType;
1008            texture.minFilter = THREE.LinearMipmapLinearFilter;
1009            texture.magFilter = THREE.LinearFilter;
1010            texture.anisotropy = 1;
1011            texture.flipY = false;
1012            texture.generateMipmaps = true;
1013            texture.premultiplyAlpha = false;
1014            texture.unpackAlignment = 4;
1015
1016            if (srgb)
1017            {
1018                if (THREE.SRGBColorSpace !== undefined)
1019                    texture.colorSpace = THREE.SRGBColorSpace;
1020                else if (THREE.sRGBEncoding !== undefined)
1021                    texture.encoding = THREE.sRGBEncoding;
1022            }
1023            else
1024            {
1025                if (THREE.NoColorSpace !== undefined)
1026                    texture.colorSpace = THREE.NoColorSpace;
1027                else if (THREE.LinearEncoding !== undefined)
1028                    texture.encoding = THREE.LinearEncoding;
1029            }
1030
1031            texture.needsUpdate = true;
1032            return texture;
1033        };
1034
1035        this.getDefaultMaterialTexture = function(slot)
1036        {
1037            var maps = Apple2VideoTHREE_getEmbeddedDefaultImageMaps();
1038            if (!maps) return null;
1039
1040            var url = maps[slot];
1041            if (slot == "roughMetal")
1042                url = maps.roughnessMap || maps.metalnessMap;
1043
1044            if (!Apple2VideoTHREE_isUsableImageDataUrl(url))
1045                return null;
1046
1047            if (this.defaultMaterialTextures[slot])
1048                return this.defaultMaterialTextures[slot];
1049
1050            var texture = new THREE.TextureLoader().load(url,function(tex)
1051            {
1052                video3D.applyDefaultTextureProfile(tex,slot);
1053                video3D.applyScreenTextureMaterialSlots();
1054                video3D.textureDirty = true;
1055            });
1056
1057            texture.name = "__Apple2VideoTHREE_default_" + slot;
1058            this.applyDefaultTextureProfile(texture,slot);
1059            this.defaultMaterialTextures[slot] = texture;
1060
1061            return texture;
1062        };
1063
1064        this.applyScreenTextureMaterialSlots = function()
1065        {
1066            var screen = this.screenMesh || this.findScreenMesh();
1067            if (!screen)
1068                return false;
1069
1070            this.screenMesh = screen;
1071
1072            var baseTexture = cfg.baseMap
1073                ? (this.getDefaultMaterialTexture("map") || this.screenTexture)
1074                : null;
1075
1076            var roughTexture = cfg.roughMetalMap
1077                ? (this.getDefaultMaterialTexture("roughnessMap") ||
1078                   this.getDefaultMaterialTexture("roughMetal"))
1079                : null;
1080
1081            var metalTexture = cfg.roughMetalMap
1082                ? (this.getDefaultMaterialTexture("metalnessMap") || roughTexture)
1083                : null;
1084
1085            var materials = Array.isArray(screen.material) ? screen.material : [screen.material];
1086            for (var i=0;i<materials.length;i++)
1087            {
1088                var mat = materials[i];
1089                if (!mat) continue;
1090
1091                this.applyScreenMaterialProfile(mat);
1092
1093                mat.map = baseTexture;
1094
1095                if (mat.emissive !== undefined)
1096                {
1097                    mat.emissive = new THREE.Color(cfg.emissiveColor);
1098                    mat.emissiveMap = this.screenTexture;
1099                    mat.emissiveIntensity = cfg.emissiveIntensity;
1100                }
1101
1102                mat.roughnessMap = roughTexture;
1103                mat.metalnessMap = metalTexture;
1104                mat.needsUpdate = true;
1105            }
1106
1107            this.textureDirty = true;
1108            return true;
1109        };
1110
1111        this.getTextureSmoothing = function()
1112        {
1113            return !!cfg.textureSmoothing;
1114        };
1115
1116        this.applyInitialTextureSmoothing = function()
1117        {
1118            return this.setTextureSmoothing(this.getSceneBoolean("textureSmoothing"));
1119        };
1120
1121        this.resizeTextureCanvas = function()
1122        {
1123            var scale = cfg.textureSmoothing ? 2 : 1;
1124            var width = DISPLAY_W*scale;
1125            var height = DISPLAY_H*scale;
1126
1127            if (this.textureCanvas.width !== width)
1128                this.textureCanvas.width = width;
1129            if (this.textureCanvas.height !== height)
1130                this.textureCanvas.height = height;
1131
1132            this.textureCtx = this.textureCanvas.getContext("2d",{alpha:false});
1133        };
1134
1135        this.setTextureSmoothing = function(flag)
1136        {
1137            cfg.textureSmoothing = !!flag;
1138            this.resizeTextureCanvas();
1139            this.configureScreenTexture();
1140            this.updateTextureCanvas();
1141
1142            if (this.screenTexture)
1143                this.screenTexture.needsUpdate = true;
1144
1145            this.textureDirty = true;
1146            return cfg.textureSmoothing;            
1147        };
1148
1149        this.renderTHREEScene = function()
1150        {
1151            if (!this.renderer || !this.scene || !this.camera) return;
1152
1153            this.renderer.setRenderTarget(null);
1154            this.renderer.render(this.scene,this.camera);           
1155        };
1156
1157
1158
1159        this.configureScreenTexture = function()
1160        {
1161            if (!this.screenTexture) return;
1162
1163            this.screenTexture.flipY = !!cfg.textureFlipY;
1164            this.screenTexture.wrapS = THREE.ClampToEdgeWrapping;
1165            this.screenTexture.wrapT = THREE.ClampToEdgeWrapping;
1166            this.screenTexture.minFilter = THREE.LinearFilter;
1167            this.screenTexture.magFilter = cfg.textureSmoothing
1168                ? THREE.LinearFilter
1169                : THREE.NearestFilter;            
1170            this.screenTexture.generateMipmaps = false;
1171            var maxAnisotropy = this.renderer && this.renderer.capabilities &&
1172                typeof(this.renderer.capabilities.getMaxAnisotropy) === "function"
1173                ? this.renderer.capabilities.getMaxAnisotropy()
1174                : 1;
1175            this.screenTexture.anisotropy = cfg.textureSmoothing
1176                ? Math.max(1,maxAnisotropy)
1177                : 1;
1178            this.screenTexture.needsUpdate = true;
1179
1180            if (THREE.SRGBColorSpace !== undefined)
1181                this.screenTexture.colorSpace = THREE.SRGBColorSpace;
1182            else if (THREE.sRGBEncoding !== undefined)
1183                this.screenTexture.encoding = THREE.sRGBEncoding;
1184
1185            this.applyTextureMirror();
1186            this.screenTexture.needsUpdate = true;
1187        };
1188
1189        this.applyTextureMirror = function()
1190        {
1191            if (!this.screenTexture) return;
1192
1193            this.screenTexture.repeat.set(
1194                cfg.textureMirrorX ? -1 : 1,
1195                cfg.textureMirrorY ? -1 : 1
1196            );
1197
1198            this.screenTexture.offset.set(
1199                cfg.textureMirrorX ? 1 : 0,
1200                cfg.textureMirrorY ? 1 : 0
1201            );
1202        };
1203
1204        this.setTextureOrientation = function(flipY, mirrorX, mirrorY)
1205        {
1206            cfg.textureFlipY = !!flipY;
1207            cfg.textureMirrorX = !!mirrorX;
1208            cfg.textureMirrorY = !!mirrorY;
1209            this.configureScreenTexture();
1210            this.textureDirty = true;
1211            return this.getTextureOrientation();
1212        };
1213
1214        this.getTextureOrientation = function()
1215        {
1216            return {
1217                textureFlipY: cfg.textureFlipY,
1218                textureMirrorX: cfg.textureMirrorX,
1219                textureMirrorY: cfg.textureMirrorY
1220            };
1221        };
1222
1223        this.setTexturePadding = function(left, top, right, bottom)
1224        {
1225            if (typeof(left) === "object" && left !== null)
1226            {
1227                var p = left;
1228                left = p.left;
1229                top = p.top;
1230                right = p.right;
1231                bottom = p.bottom;
1232            }
1233            cfg.texturePadLeft   = Apple2VideoTHREE_clampInt(left,   0, DISPLAY_W - 1);
1234            cfg.texturePadTop    = Apple2VideoTHREE_clampInt(top,    0, DISPLAY_H - 1);
1235            cfg.texturePadRight  = Apple2VideoTHREE_clampInt(right,  0, DISPLAY_W - 1);
1236            cfg.texturePadBottom = Apple2VideoTHREE_clampInt(bottom, 0, DISPLAY_H - 1);
1237
1238            this.textureDirty = true;
1239            return this.getTexturePadding();
1240        };
1241
1242        this.getTexturePadding = function()
1243        {
1244            return {
1245                left: cfg.texturePadLeft | 0,
1246                top: cfg.texturePadTop | 0,
1247                right: cfg.texturePadRight | 0,
1248                bottom: cfg.texturePadBottom | 0,
1249                color: cfg.texturePadColor,
1250                smoothing: !!cfg.textureSmoothing
1251            };
1252        };
1253
1254        this.updateTextureCanvas = function()
1255        {
1256            if (!this.textureCtx || !this.videoCanvas) return;
1257
1258            var scaleX = this.textureCanvas.width/DISPLAY_W;
1259            var scaleY = this.textureCanvas.height/DISPLAY_H;
1260
1261            var l = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadLeft,   0, DISPLAY_W - 1)*scaleX);
1262            var t = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadTop,    0, DISPLAY_H - 1)*scaleY);
1263            var r = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadRight,  0, DISPLAY_W - 1)*scaleX);
1264            var b = Math.round(Apple2VideoTHREE_clampInt(cfg.texturePadBottom, 0, DISPLAY_H - 1)*scaleY);
1265
1266            var dw = this.textureCanvas.width - l - r;
1267            var dh = this.textureCanvas.height - t - b;
1268
1269            if (dw < 1) dw = 1;
1270            if (dh < 1) dh = 1;
1271
1272            var ctx = this.textureCtx;
1273            ctx.save();
1274            ctx.imageSmoothingEnabled = !!cfg.textureSmoothing;
1275            if ("imageSmoothingQuality" in ctx)
1276                ctx.imageSmoothingQuality = cfg.textureSmoothing ? "high" : "low";
1277            ctx.fillStyle = cfg.texturePadColor || "#000000";
1278            ctx.fillRect(0,0,this.textureCanvas.width,this.textureCanvas.height);
1279            ctx.drawImage(this.videoCanvas,0,0,DISPLAY_W,DISPLAY_H,l,t,dw,dh);
1280            
1281            ctx.restore();
1282        };
1283
1284        this.getConfiguredInitialPerspective = function()
1285        {
1286            return this.getSceneNumber("perspective",5,90);
1287        };
1288
1289        this.applyInitialPerspective = function()
1290        {
1291            if (!this.camera || !this.camera.isPerspectiveCamera)
1292                return false;
1293
1294            this.setPerspective(this.getConfiguredInitialPerspective());
1295            this.captureDefaultCameraState(this.camera,this.getCameraTarget());
1296            return true;
1297        };
1298
1299        this.getConfiguredInitialLightIntensity = function()
1300        {
1301            return this.getSceneNumber("lightIntensity",0,5);
1302        };
1303
1304        this.applyInitialLightIntensity = function()
1305        {
1306            var light = this.findMovableLight();
1307            if (!light || light.intensity === undefined)
1308                return false;
1309
1310            light.intensity = this.getConfiguredInitialLightIntensity();
1311            light.updateMatrixWorld(true);
1312
1313            if (light.userData)
1314                delete light.userData.__Apple2VideoTHREE_defaultLight;
1315
1316            this.captureDefaultLightState(light);
1317
1318            if (typeof(Apple2VideoTHREE_syncLightIntensityControl) === "function")
1319                Apple2VideoTHREE_syncLightIntensityControl(light.intensity);
1320
1321            return true;
1322        };
1323
1324        this.loadProjectScene = function()
1325        {
1326            if (sceneLoadStarted) return;
1327            sceneLoadStarted = true;
1328
1329            var json_proj = Apple2VideoTHREE_getEmbeddedProjectScene();
1330            if (!json_proj)
1331            {
1332                console.warn("Apple2VideoTHREE: THREE_scene is not defined; using fallback scene.");
1333                return;
1334            }
1335
1336            this.installProjectScene(json_proj);
1337        };
1338
1339        this.installProjectScene = function(json_proj)
1340        {
1341            if (!json_proj || !json_proj.scene)
1342                return;
1343
1344            this.configureRendererFromProject(json_proj.project);
1345
1346            var loader = new THREE.ObjectLoader();
1347            this.scene = loader.parse(json_proj.scene);
1348            this.updateSceneBounds();
1349
1350            this.camera = this.cameraFromProject(json_proj.camera);
1351            this.applyInitialPerspective();
1352            this.attachTextureToScreen();
1353            this.applyEnvironmentLuminosity();
1354            this.applyInitialLightIntensity();
1355
1356            if (cfg.showGrid)
1357                this.scene.add(new THREE.GridHelper(200, 20));
1358
1359            if (cfg.showAxes)
1360                this.scene.add(new THREE.AxesHelper(60));
1361
1362            this.installOrbitControls();
1363            if (this.lightMoveEnabled)
1364                this.setLightMoveEnabled(true);
1365            this.resizeRenderer();
1366            this.ready = true;
1367        };
1368
1369        this.cameraFromProject = function(cameraJson)
1370        {
1371            if (!cameraJson || !cameraJson.object)
1372                return this.makeFallbackCamera();
1373
1374            var c = cameraJson.object;
1375            var cam = new THREE.PerspectiveCamera(c.fov, c.aspect, c.near, c.far);
1376
1377            cam.name = c.name || "Camera";
1378            cam.zoom = c.zoom || 1;
1379            cam.focus = c.focus || 10;
1380            cam.filmGauge = c.filmGauge || 35;
1381            cam.filmOffset = c.filmOffset || 0;
1382
1383            if (c.matrix)
1384            {
1385                var m4 = new THREE.Matrix4().fromArray(c.matrix);
1386                cam.matrix.copy(m4);
1387                cam.matrix.decompose(cam.position, cam.quaternion, cam.scale);
1388            }
1389
1390            if (c.up) cam.up.fromArray(c.up);
1391            if (c.layers !== undefined) cam.layers.mask = c.layers;
1392
1393            var target = this.getSceneCenter();
1394            if (cfg.fixedCameraLookAt)
1395                cam.lookAt(target);
1396
1397            cam.updateProjectionMatrix();
1398            cam.updateMatrix();
1399            cam.updateMatrixWorld(true);
1400            this.captureDefaultCameraState(cam,target);
1401
1402            return cam;
1403        };
1404
1405        this.makeFallbackCamera = function()
1406        {
1407            var cam = new THREE.PerspectiveCamera(this.getSceneNumber("perspective",5,90), DISPLAY_W / DISPLAY_H, 0.01, 1000);
1408            cam.position.set(0, 0, 3.2);
1409            cam.lookAt(0, 0, 0);
1410            cam.updateMatrix();
1411            cam.updateMatrixWorld(true);
1412            return cam;
1413        };
1414
1415        this.buildFallbackScene = function()
1416        {
1417            this.scene = new THREE.Scene();
1418            this.scene.background = new THREE.Color(cfg.background);
1419            this.camera = this.makeFallbackCamera();
1420
1421            var group = new THREE.Group();
1422            group.name = "Apple II fallback monitor";
1423
1424            var frameGeo = new THREE.BoxGeometry(3.2, 2.35, 0.18);
1425            var frameMat = new THREE.MeshStandardMaterial({
1426                color: 0x303030,
1427                roughness: 0.85,
1428                metalness: 0.0
1429            });
1430            var frame = new THREE.Mesh(frameGeo, frameMat);
1431            frame.position.z = -0.08;
1432            group.add(frame);
1433
1434            var screenGeo = new THREE.PlaneGeometry(2.8, 1.92);
1435            var screenMat = new THREE.MeshStandardMaterial({
1436                color: 0xffffff,
1437                map: this.screenTexture,
1438                emissive: new THREE.Color(cfg.emissiveColor),
1439                emissiveMap: this.screenTexture,
1440                emissiveIntensity: cfg.emissiveIntensity,
1441                roughness: 0.55,
1442                metalness: 0.0
1443            });
1444            var screen = new THREE.Mesh(screenGeo, screenMat);
1445            screen.name = "screen";
1446            screen.position.z = 0.02;
1447            group.add(screen);
1448
1449            this.screenMesh = screen;
1450            this.scene.add(group);
1451
1452            var ambient = new THREE.AmbientLight(0xffffff, 0.55);
1453            this.scene.add(ambient);
1454
1455            var light = new THREE.DirectionalLight(0xffffff, this.get
1455SceneNumber("lightIntensity",0,5));
1456            light.position.set(2, 4, 3);
1457            this.scene.add(light);
1458
1459            this.updateSceneBounds();
1460            this.applyInitialPerspective();
1461            this.captureDefaultCameraState(this.camera,this.getSceneCenter());
1462            this.applyEnvironmentLuminosity();
1463            this.applyInitialLightIntensity();
1464            this.installOrbitControls();
1465            if (this.lightMoveEnabled)
1466                this.setLightMoveEnabled(true);
1467            this.ready = true;
1468        };
1469
1470        this.findScreenMesh = function()
1471        {
1472            if (!this.scene) return null;
1473
1474            var found = this.scene.getObjectByName("screen");
1475            if (found && found.isMesh) return found;
1476
1477            this.scene.traverse(function(obj)
1478            {
1479                if (found || !obj.isMesh) return;
1480
1481                var mat = obj.material;
1482                if (Array.isArray(mat)) mat = mat[0];
1483
1484                if (obj.name && obj.name.toLowerCase().indexOf("screen") >= 0)
1485                    found = obj;
1486                else if (mat && mat.name && mat.name.toLowerCase().indexOf("screen") >= 0)
1487                    found = obj;
1488            });
1489
1490            return found;
1491        };
1492
1493        this.attachTextureToScreen = function()
1494        {
1495            var screen = this.findScreenMesh();
1496            if (!screen)
1497            {
1498                console.warn("Apple2VideoTHREE: no mesh named 'screen' found in the scene.");
1499                return false;
1500            }
1501
1502            this.screenMesh = screen;
1503            return this.applyScreenTextureMaterialSlots();
1504        };
1505
1506        this.findMovableLight = function()
1507        {
1508            if (!this.scene) return null;
1509
1510            var pointLight = null;
1511            var positionedLight = null;
1512
1513            this.scene.traverse(function(obj)
1514            {
1515                if (!pointLight && obj.isPointLight)
1516                    pointLight = obj;
1517
1518                if (!positionedLight && obj.isLight &&
1519                    !obj.isAmbientLight && !obj.isHemisphereLight && obj.position)
1520                    positionedLight = obj;
1521            });
1522
1523            return pointLight || positionedLight;
1524        };
1525
1526        this.captureDefaultLightState = function(light)
1527        {
1528            if (!light || !light.position) return null;
1529
1530            if (!light.userData)
1531                light.userData = {};
1532
1533            if (!light.userData.__Apple2VideoTHREE_defaultLight)
1534            {
1535                light.userData.__Apple2VideoTHREE_defaultLight = {
1536                     "position": light.position.clone()
1537                    ,"intensity": light.intensity
1538                    ,"distance": light.distance
1539                    ,"decay": light.decay
1540                };
1541            }
1542
1543            this.defaultLightState = light.userData.__Apple2VideoTHREE_defaultLight;
1544            return this.defaultLightState;
1545        };
1546
1547        this.resetLightPosition = function()
1548        {
1549            if (!this.scene)
1550                this.ensureTHREE();
1551
1552            var light = this.activeSceneLight || this.findMovableLight();
1553            if (!light) return false;
1554
1555            var state = this.captureDefaultLightState(light);
1556            if (!state) return false;
1557
1558            light.position.copy(state.position);
1559
1560            if (state.intensity !== undefined) light.intensity = state.intensity;
1561            if (state.distance  !== undefined) light.distance  = state.distance;
1562            if (state.decay     !== undefined) light.decay     = state.decay;
1563
1564            light.updateMatrixWorld(true);
1565
1566            if (this.lightTransformControls &&
1567                typeof(this.lightTransformControls.updateMatrixWorld) == "function")
1568                this.lightTransformControls.updateMatrixWorld(true);
1569
1570            if (this.lightMoveEnabled && this.lightMovePlane && this.camera)
1571            {
1572                var normal = new THREE.Vector3();
1573                this.camera.getWorldDirection(normal);
1574                this.lightMovePlane.setFromNormalAndCoplanarPoint(normal,light.position);
1575            }
1576
1577            return true;
1578        };
1579
1580        this.installLightTransformControls = function(light)
1581        {
1582            if (!cfg.lightMoveHandles || !light || !this.scene || !this.camera ||
1583                !this.renderer || typeof(THREE.TransformControls) != "function")
1584                return false;
1585
1586            if (!this.lightTransformControls)
1587            {
1588                var controls3D = new THREE.TransformControls(this.camera,this.renderer.domElement);
1589                controls3D.setMode("translate");
1590                if (typeof(controls3D.setSize) == "function")
1591                    controls3D.setSize(0.8);
1592                controls3D.name = "__Apple2VideoTHREE_light_transform";
1593
1594                controls3D.addEventListener("dragging-changed",function(event)
1595                {
1596                    if (video3D.controls)
1597                        video3D.controls.enabled = !video3D.lightMoveEnabled &&
1598                                                   !event.value &&
1599                                                   !!cfg.orbitControls;
1600                });
1601
1602                controls3D.addEventListener("objectChange",function()
1603                {
1604                    if (video3D.activeSceneLight)
1605                    {
1606                        video3D.activeSceneLight.updateMatrixWorld(true);
1607
1608                        if (video3D.lightMovePlane && video3D.camera)
1609                        {
1610                            var normal = new THREE.Vector3();
1611                            video3D.camera.getWorldDirection(normal);
1612                            video3D.lightMovePlane.setFromNormalAndCoplanarPoint(
1613                                normal,
1614                                video3D.activeSceneLight.position
1615                            );
1616                        }
1617                    }
1618                });
1619
1620                controls3D.addEventListener("change",function()
1621                {
1622                    video3D.renderTHREEScene();
1623                });
1624
1625                this.lightTransformControls = controls3D;
1626            }
1627            else
1628            {
1629                this.lightTransformControls.camera = this.camera;
1630            }
1631
1632            this.lightTransformHelper =
1633                typeof(this.lightTransformControls.getHelper) == "function"
1634                    ? this.lightTransformControls.getHelper()
1635                    : this.lightTransformControls;
1636
1637            if (this.lightTransformHelper && this.lightTransformHelper.parent !== this.scene)
1638                this.scene.add(this.lightTransformHelper);
1639
1640            this.lightTransformControls.attach(light);
1641            return true;
1642        };
1643
1644        this.detachLightTransformControls = function()
1645        {
1646            if (this.lightTransformControls &&
1647                typeof(this.lightTransformControls.detach) == "function")
1648                this.lightTransformControls.detach();
1649
1650            if (this.lightTransformHelper && this.lightTransformHelper.parent)
1651                this.lightTransformHelper.parent.remove(this.lightTransformHelper);
1652        };
1653
1654        this.getLightIntensity = function()
1655        {
1656            var light = this.activeSceneLight || this.findMovableLight();
1657            if (!light || light.intensity === undefined)
1658                return this.getSceneNumber("lightIntensity",0,5);
1659
1660            var value = Number(light.intensity);
1661            return isFinite(value) ? value : this.getSceneNumber("lightIntensity",0,5);
1662        };
1663
1664        this.setLightIntensity = function(intensity)
1665        {
1666            intensity = Number(intensity);
1667            if (!isFinite(intensity)) intensity = this.getLightIntensity();
1668            intensity = Math.max(0,Math.min(5,intensity));
1669            cfg.lightIntensity = intensity;
1670
1671            if (!this.scene)
1672                this.ensureTHREE();
1673
1674            var light = this.activeSceneLight || this.findMovableLight();
1675            if (!light || light.intensity === undefined)
1676                return intensity;
1677
1678            this.captureDefaultLightState(light);
1679            light.intensity = intensity;
1680            light.updateMatrixWorld(true);
1681
1682            return intensity;
1683        };
1684
1685        this.getLightMoveEnabled = function()
1686        {
1687            return !!this.lightMoveEnabled;
1688        };
1689
1690        this.setLightMoveEnabled = function(flag)
1691        {
1692            flag = !!flag;
1693            cfg.lightMove = flag;
1694
1695            if (flag && !this.renderer)
1696                this.ensureTHREE();
1697
1698            var el = this.renderer && this.renderer.domElement;
1699            if (!flag)
1700            {
1701                if (el)
1702                {
1703                    el.removeEventListener("pointermove",this._lightMovePointerHandler,true);
1704                    el.style.cursor = this._lightMovePreviousCursor || "";
1705                }
1706
1707                this.detachLightTransformControls();
1708
1709                cfg.lightMove = false;
1710                this.lightMoveEnabled = false;
1711                this.activeSceneLight = null;
1712
1713                if (this.controls)
1714                    this.controls.enabled = !!cfg.orbitControls;
1715
1716                return false;
1717            }
1718
1719            var light = this.findMovableLight();
1720            if (!el || !this.camera || !light)
1721            {
1722                cfg.lightMove = false;
1723                this.lightMoveEnabled = false;
1724                return false;
1725            }
1726
1727            if (!this.lightMovePlane)
1728            {
1729                this.lightMovePlane = new THREE.Plane();
1730                this.lightMoveRaycaster = new THREE.Raycaster();
1731                this.lightMoveNdc = new THREE.Vector2();
1732                this.lightMovePoint = new THREE.Vector3();
1733            }
1734
1735            this.captureDefaultLightState(light);
1736            this.activeSceneLight = light;
1737            this.lightMoveEnabled = true;
1738
1739            var normal = new THREE.Vector3();
1740            this.camera.getWorldDirection(normal);
1741            this.lightMovePlane.setFromNormalAndCoplanarPoint(normal,light.position);
1742
1743            if (this.controls)
1744                this.controls.enabled = false;
1745
1746            var hasTransformHandles = this.installLightTransformControls(light);
1747            el.removeEventListener("pointermove",this._lightMovePointerHandler,true);
1748
1749            if (hasTransformHandles)
1750            {
1751                el.style.cursor = this._lightMovePreviousCursor || "";
1752            }
1753            else
1754            {
1755                this._lightMovePreviousCursor = el.style.cursor || "";
1756                el.addEventListener("pointermove",this._lightMovePointerHandler,true);
1757                el.style.cursor = "crosshair";
1758            }
1759
1760
1761            return true;
1762        };
1763
1764        this.onLightPointerMove = function(event)
1765        {
1766            if (!this.lightMoveEnabled || !this.activeSceneLight ||
1767                !this.lightMoveRaycaster || !this.lightMovePlane ||
1768                !this.renderer || !this.camera)
1769                return;
1770
1771            var rect = this.renderer.domElement.getBoundingClientRect();
1772            this.lightMoveNdc.x =
1773                ((event.clientX-rect.left)/Math.max(1,rect.width))*2-1;
1774            this.lightMoveNdc.y =
1775                -(((event.clientY-rect.top)/Math.max(1,rect.height))*2-1);
1776
1777            this.lightMoveRaycaster.setFromCamera(this.lightMoveNdc,this.camera);
1778            var hit = this.lightMoveRaycaster.ray.intersectPlane(
1779                this.lightMovePlane,
1780                this.lightMovePoint
1781            );
1782
1783            if (!hit) return;
1784
1785            this.activeSceneLight.position.copy(this.lightMovePoint);
1786            this.activeSceneLight.updateMatrixWorld(true);
1787
1788            if (event.cancelable)
1789                event.preventDefault();
1790            event.stopPropagation();
1791        };
1792
1793        this.getOrbitEnabled = function()
1794        {
1795            return !!cfg.orbitControls;
1796        };
1797
1798        this.setOrbitEnabled = function(flag)
1799        {
1800            cfg.orbitControls = !!flag;
1801
1802            if (!cfg.orbitControls)
1803            {
1804                if (this.controls && typeof(this.controls.dispose) === "function")
1805                    this.controls.dispose();
1806
1807                this.controls = null;
1808                this.controlsCamera = null;
1809                return false;
1810            }
1811
1812            if (!this.renderer)
1813                this.ensureTHREE();
1814
1815            this.installOrbitControls();
1816            return !!this.controls;
1817        };
1818
1819        this.installOrbitControls = function()
1820        {
1821            if (!cfg.orbitControls || !this.camera || !this.renderer)
1822                return;
1823
1824            if (typeof(THREE.OrbitControls) !== "function")
1825                return;
1826
1827            // v4 fix retained:
1828            // ensureTHREE() first builds a fallback scene/camera, then replaces it
1829            // with the editor/project scene/camera.  Recreate controls if the
1830            // active rendered camera changed.
1831            if (this.controls)
1832            {
1833                var sameCamera = (this.controlsCamera === this.camera) ||
1834                                 (this.controls.object === this.camera);
1835                if (sameCamera)
1836                    return;
1837
1838                if (typeof(this.controls.dispose) === "function")
1839                    this.controls.dispose();
1840
1841                this.controls = null;
1842                this.controlsCamera = null;
1843            }
1844
1845            this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
1846            this.controlsCamera = this.camera;
1847            this.controls.enableDamping = true;
1848            this.controls.dampingFactor = 0.08;
1849
1850            this.controls.target.copy(this.getSceneCenter());
1851            this.controls.enabled = !this.lightMoveEnabled;
1852            this.controls.update();
1853            if (typeof(this.controls.saveState)==="function")
1854                this.controls.saveState();
1855
1856            this.installCanvasEventShield();
1857        };
1858
1859        this.getOrbitControlsInfo = function()
1860        {
1861            return {
1862                hasControls: !!this.controls,
1863                controlsCameraIsRenderedCamera: !!this.controls &&
1864                    ((this.controlsCamera === this.camera) || (this.controls.object === this.camera)),
1865                eventShield: !!cfg.eventShield,
1866                eventShieldInstalled: !!this._eventShieldInstalled
1867            };
1868        };
1869
1870        this.installCanvasEventShield = function()
1871        {
1872            if (!cfg.eventShield || this._eventShieldInstalled || !this.renderer || !this.renderer.domElement)
1873                return;
1874
1875            this._eventShieldInstalled = true;
1876
1877            var el = this.renderer.domElement;
1878            var opt = {capture:true, passive:false};
1879            var events = [
1880                "pointerdown", "pointermove", "pointerup", "pointercancel",
1881                "mousedown", "mousemove", "mouseup", "click", "dblclick",
1882                "wheel", "touchstart", "touchmove", "touchend", "contextmenu"
1883            ];
1884
1885            var stop = function(ev)
1886            {
1887                // Do not use stopImmediatePropagation(), because OrbitControls also
1888                // listens on this same element and must still receive the event.
1889                ev.stopPropagation();
1890
1891                if (ev.type === "contextmenu")
1892                    ev.preventDefault();
1893                else if (cfg.eventShieldPreventDefault && ev.cancelable)
1894                    ev.preventDefault();
1895            };
1896
1897            for (var i = 0; i < events.length; i++)
1898            {
1899                try { el.addEventListener(events[i], stop, opt); }
1900                catch(e) { el.addEventListener(events[i], stop, true); }
1901            }
1902        };
1903
1904        this.installResizeListener = function()
1905        {
1906            if (resizeListenerInstalled) return;
1907            resizeListenerInstalled = true;
1908
1909            window.addEventListener("resize", function()
1910            {
1911                video3D.resizeRenderer();
1912            });
1913        };
1914
1915        this.startRenderLoop = function()
1916        {
1917            if (renderLoopStarted) return;
1918            renderLoopStarted = true;
1919
1920            function animate(now)
1921            {
1922                window.requestAnimationFrame(animate);
1923
1924                var renderInterval = 1000 / Math.max(1, cfg.renderFPS | 0);
1925                if (now - lastRenderUpdate < renderInterval)
1926                    return;
1927                lastRenderUpdate = now;
1928
1929                video3D.pumpTexture(now);
1930
1931                if (video3D.controls)
1932                    video3D.controls.update();
1933
1934                video3D.renderTHREEScene();
1935            }
1936
1937            window.requestAnimationFrame(animate);
1938        };
1939
1940        this.pumpTexture = function(now)
1941        {
1942            if (!this.screenTexture || !this.textureDirty)
1943                return;
1944
1945            var textureInterval = 1000 / Math.max(1, cfg.textureFPS | 0);
1946            if (now - lastTextureUpdate < textureInterval)
1947                return;
1948
1949            this.updateTextureCanvas();
1950            this.screenTexture.needsUpdate = true;
1951            this.textureDirty = false;
1952            lastTextureUpdate = now;
1953        };
1954
1955        this.getTextureCanvas = function()
1956        {
1957            return this.textureCanvas;
1958        };
1959
1960        this.getSourceCanvas = function()
1961        {
1962            return this.videoCanvas;
1963        };
1964
1965        this.get2DRenderer = function()
1966        {
1967            return this.video2D;
1968        };
1969
1970        this.getScene = function()
1971        {
1972            return this.scene;
1973        };
1974
1975        this.getRenderer = function()
1976        {
1977            return this.renderer;
1978        };
1979
1980        this.ctrl_dlg = function()
1981        {
1982            return Apple2VideoTHREE_controls(this);
1983        };
1984
1985    }
1986
1987Apple2VideoTHREE.ctrl_dlg = function()
1988{
1989    return Apple2VideoTHREE_controls(null);
1990};
1991
1992if(typeof(oEMU)!="undefined" && oEMU.component && oEMU.component.Video)
1993    oEMU.component.Video.Apple2Video = new Apple2VideoTHREE();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.