jailedthreejs 0.9.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cell.js ADDED
@@ -0,0 +1,508 @@
1
+ // cell.js
2
+ //
3
+ // The Cell class drives a single <cell> element:
4
+ // - DOM → Three.js object conversion
5
+ // - Event wiring / raycasting integration
6
+ // - CSS → object painting
7
+ // - Mutation observers (DOM + <style> changes)
8
+ // - Per-frame update callbacks
9
+
10
+ import * as THREE from 'three';
11
+ import { fastRemove_arry, getClassMap } from './utils.js';
12
+ import {
13
+ paintCell,
14
+ paintConvict,
15
+ deep_searchParms,
16
+ paintSpecificMuse,
17
+ paintConstantMuse,
18
+ getCSSRule
19
+ } from './artist.js';
20
+ import {
21
+ default_onCellClick_method,
22
+ default_onCellPointerMove_method,
23
+ default_onCellMouseDown_method,
24
+ default_onCellMouseUp_method,
25
+ default_onCellDoubleClick_method,
26
+ default_onCellContextMenu_method
27
+ } from './NoScope.js';
28
+
29
+ class Cell {
30
+ static allCells = new WeakMap();
31
+
32
+ /**
33
+ * Retrieve an existing Cell for a <cell> element.
34
+ *
35
+ * @param {HTMLElement} element
36
+ * @returns {Cell|null}
37
+ */
38
+ static getCell(element) {
39
+ if (Cell.allCells.has(element)) {
40
+ return Cell.allCells.get(element);
41
+ }
42
+ console.error('No Cell found with the element:', element);
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * @param {HTMLElement} cellElm
48
+ * @param {THREE.WebGLRenderer} renderer
49
+ * @param {THREE.Scene} scene
50
+ * @param {THREE.Camera|null} [camera=null]
51
+ * @param {Function|null} [_MainAnimMethod=null]
52
+ */
53
+ constructor(cellElm, renderer, scene, camera = null, _MainAnimMethod = null) {
54
+ this.cellElm = cellElm;
55
+ Object.defineProperty(cellElm, 'cell', {
56
+ value: this,
57
+ enumerable: false
58
+ });
59
+
60
+ this.threeRenderer = renderer;
61
+ this.loadedScene = scene;
62
+ this.focusedCamera = camera;
63
+
64
+ this.constantConvicts = [];
65
+ this.classyConvicts = [];
66
+ this.namedConvicts = [];
67
+ this._allConvictsByDom = new WeakMap();
68
+
69
+ this.updateFunds = [];
70
+ this._observedStyleElements = new WeakSet();
71
+ this._pendingStyleRepaint = false;
72
+
73
+ // paint constant :active rules each frame
74
+ this.updateFunds.push(() => {
75
+ this.constantConvicts.forEach(cC => {
76
+ paintConstantMuse(cC);
77
+ });
78
+ });
79
+
80
+ this._last_cast_caught = null;
81
+ this._lastHitPosition = null;
82
+ Cell.allCells.set(cellElm, this);
83
+
84
+ // initial scan
85
+ this._ScanCell();
86
+
87
+ // bind DOM event handlers
88
+ this._boundPointerMove = evt => {
89
+ default_onCellPointerMove_method(evt, this);
90
+ };
91
+ this._boundClick = evt => {
92
+ default_onCellClick_method(evt, this);
93
+ };
94
+ this._boundMouseDown = evt => {
95
+ default_onCellMouseDown_method(evt, this);
96
+ };
97
+ this._boundMouseUp = evt => {
98
+ default_onCellMouseUp_method(evt, this);
99
+ };
100
+ this._boundDoubleClick = evt => {
101
+ default_onCellDoubleClick_method(evt, this);
102
+ };
103
+ this._boundContextMenu = evt => {
104
+ evt.preventDefault();
105
+ default_onCellContextMenu_method(evt, this);
106
+ };
107
+
108
+ cellElm.addEventListener('mousemove', this._boundPointerMove);
109
+ cellElm.addEventListener('click', this._boundClick);
110
+ cellElm.addEventListener('mousedown', this._boundMouseDown);
111
+ cellElm.addEventListener('mouseup', this._boundMouseUp);
112
+ cellElm.addEventListener('dblclick', this._boundDoubleClick);
113
+ cellElm.addEventListener('contextmenu', this._boundContextMenu);
114
+
115
+ // initial paint
116
+ paintCell(this);
117
+
118
+ // Observe <style> content so keyframes / rules updates repaint
119
+ this._styleElemObserver = new MutationObserver(() => {
120
+ if (this._pendingStyleRepaint) return;
121
+ this._pendingStyleRepaint = true;
122
+ requestAnimationFrame(() => {
123
+ this._pendingStyleRepaint = false;
124
+ paintCell(this);
125
+ this.classyConvicts.concat(this.namedConvicts).forEach(paintSpecificMuse);
126
+ });
127
+ });
128
+
129
+ this._observeStyleElements = root => {
130
+ if (!root) return;
131
+ const targets = [];
132
+ if (root.nodeName === 'STYLE') {
133
+ targets.push(root);
134
+ } else if (typeof root.querySelectorAll === 'function') {
135
+ targets.push(...root.querySelectorAll('style'));
136
+ }
137
+ targets.forEach(styleEl => {
138
+ if (this._observedStyleElements.has(styleEl)) return;
139
+ this._observedStyleElements.add(styleEl);
140
+ this._styleElemObserver.observe(styleEl, {
141
+ childList: true,
142
+ characterData: true,
143
+ subtree: true
144
+ });
145
+ });
146
+ };
147
+
148
+ this._styleHostObserver = new MutationObserver(mutationList => {
149
+ mutationList.forEach(mutation => {
150
+ mutation.addedNodes.forEach(node => {
151
+ if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'STYLE') {
152
+ this._observeStyleElements(node);
153
+ }
154
+ });
155
+ });
156
+ });
157
+
158
+ this._observeStyleElements(this.cellElm);
159
+ if (document.head) {
160
+ this._observeStyleElements(document.head);
161
+ this._styleHostObserver.observe(document.head, {
162
+ childList: true,
163
+ subtree: true
164
+ });
165
+ }
166
+
167
+ // Observe inline style/id/class changes and child mutations
168
+ this._styleObserver = new MutationObserver(mutationList => {
169
+ mutationList.forEach(mutation => {
170
+ if (mutation.target.nodeName === 'CANVAS') return;
171
+
172
+ switch (mutation.type) {
173
+ case 'childList': {
174
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
175
+ const node = mutation.addedNodes[i];
176
+ if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'CANVAS') {
177
+ if (node.nodeName === 'STYLE') {
178
+ this._observeStyleElements(node);
179
+ paintCell(this);
180
+ } else {
181
+ this.ScanElement(node);
182
+ const convict = this.getConvictByDom(node);
183
+ if (convict) {
184
+ paintSpecificMuse(convict);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
190
+ const node = mutation.removedNodes[i];
191
+ if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'CANVAS') {
192
+ this.removeConvict(this._allConvictsByDom.get(node));
193
+ }
194
+ }
195
+ break;
196
+ }
197
+ case 'attributes': {
198
+ const target = mutation.target;
199
+ const convict = target.convict;
200
+ if (!convict) break;
201
+
202
+ if (mutation.attributeName === 'id') {
203
+ convict.userData.domId = target.id;
204
+ } else if (mutation.attributeName === 'class') {
205
+ const nextClasses = Array.from(target.classList).filter(Boolean);
206
+ convict.userData.classList = nextClasses;
207
+ convict.name = nextClasses[0] || '';
208
+ } else if (mutation.attributeName === 'style') {
209
+ // inline style changed; repaint this convict
210
+ paintConvict(target, this);
211
+ }
212
+ break;
213
+ }
214
+ }
215
+ });
216
+ });
217
+
218
+ this._styleObserver.observe(this.cellElm, {
219
+ attributes: true,
220
+ childList: true,
221
+ attributeFilter: ['style', 'id', 'class'],
222
+ subtree: true
223
+ });
224
+
225
+ // Animation loop
226
+ this._running = true;
227
+ this._anim = _MainAnimMethod
228
+ ? _MainAnimMethod.bind(this)
229
+ : () => {
230
+ if (!this._running) return;
231
+ this.updateFunds.forEach(update => update());
232
+ requestAnimationFrame(this._anim);
233
+ if (this.focusedCamera) {
234
+ this.threeRenderer.render(this.loadedScene, this.focusedCamera);
235
+ }
236
+ };
237
+
238
+ // Resize handling
239
+ this._resizeObserver = new ResizeObserver(entries => {
240
+ for (const e of entries) {
241
+ const { width, height } = e.contentRect;
242
+ const dpr = window.devicePixelRatio || 1;
243
+ this.threeRenderer.setPixelRatio(dpr);
244
+
245
+ const safeWidth = Math.max(width, 1);
246
+ const safeHeight = Math.max(height, 1);
247
+ this.threeRenderer.setSize(safeWidth, safeHeight, false);
248
+
249
+ if (this.focusedCamera && this.focusedCamera.isPerspectiveCamera) {
250
+ this.focusedCamera.aspect = safeWidth / safeHeight;
251
+ }
252
+ if (this.focusedCamera) {
253
+ this.focusedCamera.updateProjectionMatrix();
254
+ }
255
+ }
256
+ });
257
+ this._resizeObserver.observe(this.cellElm);
258
+
259
+ this._anim();
260
+ }
261
+
262
+ /**
263
+ * Initial scan of cell children.
264
+ * @private
265
+ */
266
+ _ScanCell() {
267
+ for (let i = 0; i < this.cellElm.children.length; i++) {
268
+ const convictElm = this.cellElm.children[i];
269
+ this.ScanElement(convictElm);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Convert a DOM element into a Three.js object and wire it up.
275
+ *
276
+ * @param {HTMLElement} elm
277
+ */
278
+ ScanElement(elm) {
279
+ if (this._allConvictsByDom.has(elm)) return;
280
+
281
+ const parentObj = this.getConvictByDom(elm.parentElement) || this.loadedScene;
282
+ const instance = this.ConvertDomToObject(elm);
283
+
284
+ if (instance === null) {
285
+ // still recurse children
286
+ for (let i = 0; i < elm.children.length; i++) {
287
+ this.ScanElement(elm.children[i]);
288
+ }
289
+ return;
290
+ }
291
+
292
+ // Camera tags: configure projection
293
+ if (elm.tagName.includes('CAMERA')) {
294
+ const rect = this.cellElm.getBoundingClientRect();
295
+ const aspect = rect.height ? rect.width / rect.height : 1;
296
+
297
+ if (elm.tagName === 'PERSPECTIVECAMERA') {
298
+ instance.fov = 75;
299
+ instance.aspect = aspect;
300
+ instance.far = 1000;
301
+ instance.near = 0.1;
302
+ } else {
303
+ const frustumSize = 20;
304
+ instance.frustumSize = frustumSize;
305
+ instance.aspect = aspect;
306
+ instance.left = (-frustumSize * aspect) / 2;
307
+ instance.right = (frustumSize * aspect) / 2;
308
+ instance.top = frustumSize / 2;
309
+ instance.bottom = -frustumSize / 2;
310
+ instance.refreshLook = fSize => {
311
+ instance.frustumSize = fSize;
312
+ instance.left = (-fSize * instance.aspect) / 2;
313
+ instance.right = (fSize * instance.aspect) / 2;
314
+ instance.top = fSize / 2;
315
+ instance.bottom = -fSize / 2;
316
+ instance.updateProjectionMatrix();
317
+ };
318
+ }
319
+
320
+ const rectW = rect.width || 1;
321
+ const rectH = rect.height || 1;
322
+
323
+ if (elm.hasAttribute('render')) {
324
+ this.focusedCamera = instance;
325
+ this.focusedCamera.updateProjectionMatrix();
326
+ this.threeRenderer.setPixelRatio(window.devicePixelRatio || 1);
327
+ this.threeRenderer.setSize(rectW, rectH, false);
328
+ } else if (!this.focusedCamera) {
329
+ this.focusedCamera = instance;
330
+ this.focusedCamera.updateProjectionMatrix();
331
+ }
332
+ }
333
+
334
+ instance.userData.domEl = elm;
335
+ instance.userData.extraParams = [];
336
+ instance.userData.classList = [];
337
+ instance.transition = null;
338
+
339
+ parentObj.add(instance);
340
+
341
+ if (elm.id) {
342
+ instance.userData.domId = elm.id;
343
+ this.namedConvicts.push(instance);
344
+ if (!this.constantConvicts.includes(instance) && getCSSRule(`#${elm.id}:active`)) {
345
+ this.constantConvicts.push(instance);
346
+ }
347
+ }
348
+
349
+ const classList = Array.from(elm.classList || []).filter(Boolean);
350
+ if (classList.length) {
351
+ instance.userData.classList = classList;
352
+ instance.name = classList[0];
353
+ this.classyConvicts.push(instance);
354
+ const hasActiveRule = classList.some(cls => getCSSRule(`.${cls}:active`));
355
+ if (hasActiveRule && !this.constantConvicts.includes(instance)) {
356
+ this.constantConvicts.push(instance);
357
+ }
358
+ }
359
+
360
+ this._allConvictsByDom.set(elm, instance);
361
+
362
+ for (let i = 0; i < elm.children.length; i++) {
363
+ this.ScanElement(elm.children[i]);
364
+ }
365
+
366
+ if (!Object.prototype.hasOwnProperty.call(elm, 'convict')) {
367
+ Object.defineProperty(elm, 'convict', {
368
+ value: this.getConvictByDom(elm),
369
+ enumerable: false
370
+ });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Tag → THREE.Object3D constructor.
376
+ *
377
+ * @param {HTMLElement} elm
378
+ * @returns {THREE.Object3D|null}
379
+ */
380
+ ConvertDomToObject(elm) {
381
+ if (elm.tagName === 'CANVAS') return null;
382
+
383
+ const key = elm.tagName.replace(/-/g, '');
384
+ const Ctor = getClassMap()[key];
385
+ if (!Ctor) {
386
+ console.warn(`Unknown THREE class for <${elm.tagName.toLowerCase()}>`);
387
+ return null;
388
+ }
389
+ return new Ctor();
390
+ }
391
+
392
+ /**
393
+ * Remove a convict and its children.
394
+ *
395
+ * @param {THREE.Object3D|null} convict
396
+ */
397
+ removeConvict(convict) {
398
+ if (!convict) return;
399
+
400
+ convict.children.slice().forEach(child => {
401
+ const domNode = child.userData?.domEl;
402
+ if (domNode) {
403
+ this.removeConvict(this._allConvictsByDom.get(domNode));
404
+ } else {
405
+ this.removeConvict(child);
406
+ }
407
+ });
408
+
409
+ fastRemove_arry(this.classyConvicts, convict);
410
+ fastRemove_arry(this.namedConvicts, convict);
411
+ fastRemove_arry(this.constantConvicts, convict);
412
+
413
+ if (convict.userData.domEl) {
414
+ this._allConvictsByDom.delete(convict.userData.domEl);
415
+ convict.userData.domEl.remove();
416
+ }
417
+
418
+ if (convict.parent) {
419
+ convict.parent.remove(convict);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Get convict by DOM element.
425
+ *
426
+ * @param {HTMLElement} element
427
+ */
428
+ getConvictByDom(element) {
429
+ return this._allConvictsByDom.get(element);
430
+ }
431
+
432
+ /**
433
+ * Get convict by DOM id (global document lookup).
434
+ *
435
+ * @param {string} id
436
+ */
437
+ getConvictById(id) {
438
+ const el = document.getElementById(id);
439
+ return el ? this._allConvictsByDom.get(el) : undefined;
440
+ }
441
+
442
+ /**
443
+ * Get all convicts with a given class.
444
+ *
445
+ * @param {string} className
446
+ * @returns {Array<THREE.Object3D>}
447
+ */
448
+ getConvictsByClass(className) {
449
+ const elements = Array.from(document.getElementsByClassName(className));
450
+ const out = [];
451
+ elements.forEach(elm => {
452
+ const convict = this.getConvictByDom(elm);
453
+ if (convict) out.push(convict);
454
+ });
455
+ return out;
456
+ }
457
+
458
+ /**
459
+ * Register a per-frame callback.
460
+ *
461
+ * @param {Function} fn
462
+ */
463
+ addUpdateFunction(fn) {
464
+ if (typeof fn === 'function') {
465
+ const bound = fn.bind(this);
466
+ bound.originalFn = fn;
467
+ this.updateFunds.push(bound);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Remove a previously registered per-frame callback.
473
+ *
474
+ * @param {Function} fn
475
+ */
476
+ removeUpdateFunction(fn) {
477
+ const idx = this.updateFunds.findIndex(item => item?.originalFn === fn);
478
+ if (idx >= 0) {
479
+ this.updateFunds.splice(idx, 1);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Tear down observers, handlers and canvas.
485
+ */
486
+ dispose() {
487
+ this._running = false;
488
+
489
+ this._resizeObserver.disconnect();
490
+ this._styleObserver.disconnect();
491
+ this._styleElemObserver.disconnect();
492
+ this._styleHostObserver.disconnect();
493
+
494
+ this.cellElm.removeEventListener('mousemove', this._boundPointerMove);
495
+ this.cellElm.removeEventListener('click', this._boundClick);
496
+ this.cellElm.removeEventListener('mousedown', this._boundMouseDown);
497
+ this.cellElm.removeEventListener('mouseup', this._boundMouseUp);
498
+ this.cellElm.removeEventListener('dblclick', this._boundDoubleClick);
499
+ this.cellElm.removeEventListener('contextmenu', this._boundContextMenu);
500
+
501
+ const canvas = this.threeRenderer.domElement;
502
+ if (canvas && canvas.parentNode) {
503
+ canvas.parentNode.removeChild(canvas);
504
+ }
505
+ }
506
+ }
507
+
508
+ export default Cell;
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // index.js
2
+ //
3
+ // Entry point for the JailedThreeJS module.
4
+ // Re-exports all public API in one place.
5
+
6
+ export { default as Cell } from './cell.js';
7
+ export { default as JThree } from './main.js';
8
+
9
+ export * from './artist.js';
10
+ export * from './NoScope.js';
11
+ export * from './Train.js';
12
+ export * from './utils.js';
package/dist/main.js ADDED
@@ -0,0 +1,136 @@
1
+ // main.js
2
+ //
3
+ // JThree facade: finds <cell> elements, bootstraps renderer/scene/cell
4
+ // for each, and keeps a WeakMap of created Cell instances.
5
+
6
+ import * as THREE from 'three';
7
+ import Cell from './cell.js';
8
+
9
+ class JTHREE {
10
+ static __Loaded_Cells__ = new WeakMap();
11
+ static __StyleTag__ = null;
12
+
13
+ /**
14
+ * Convert all <cell> elements in the document.
15
+ */
16
+ static init_convert() {
17
+ if (!JTHREE.__StyleTag__ && document.head) {
18
+ const styleSheet = document.createElement('style');
19
+ styleSheet.textContent = `
20
+ cell > :not(canvas) {
21
+ display: none;
22
+ }
23
+ `;
24
+ document.head.appendChild(styleSheet);
25
+ JTHREE.__StyleTag__ = styleSheet;
26
+ }
27
+
28
+ document.querySelectorAll('cell').forEach(el => {
29
+ if (JTHREE.__Loaded_Cells__.has(el)) return;
30
+ JTHREE.create_THREEJSRENDERER(el);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Legacy alias.
36
+ */
37
+ static _convert_init_() {
38
+ return JTHREE.init_convert();
39
+ }
40
+
41
+ /**
42
+ * Create renderer + scene for a given <cell> element.
43
+ *
44
+ * @param {HTMLElement} cellEl
45
+ * @returns {Cell}
46
+ */
47
+ static create_THREEJSRENDERER(cellEl) {
48
+ if (JTHREE.__Loaded_Cells__.has(cellEl)) {
49
+ return JTHREE.__Loaded_Cells__.get(cellEl);
50
+ }
51
+
52
+ const { canvas, width, height, dpr } = createWebGLOverlay(cellEl);
53
+ const safeWidth = width || 1;
54
+ const safeHeight = height || 1;
55
+
56
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
57
+ renderer.setPixelRatio(dpr);
58
+ renderer.setSize(safeWidth, safeHeight, false);
59
+ renderer.setClearColor(0x000000, 1);
60
+
61
+ const scene = new THREE.Scene();
62
+
63
+ // Find explicit cameras
64
+ const regex = /camera/i;
65
+ const foundCameraElms = Array.from(cellEl.children).filter(child =>
66
+ regex.test(child.tagName) ||
67
+ regex.test(child.id) ||
68
+ regex.test(child.className)
69
+ );
70
+
71
+ let camera = null;
72
+ if (foundCameraElms.length === 0) {
73
+ camera = new THREE.PerspectiveCamera(75, safeWidth / safeHeight, 0.1, 1000);
74
+ console.warn('No camera found for', cellEl, '. Creating a default camera.');
75
+ }
76
+
77
+ const cell = new Cell(cellEl, renderer, scene, camera || null);
78
+ JTHREE.__Loaded_Cells__.set(cellEl, cell);
79
+
80
+ cellEl.dispatchEvent(
81
+ new CustomEvent('OnStart', { detail: { cell, CellEl: cellEl } })
82
+ );
83
+
84
+ return cell;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create a WebGL canvas overlay on a host element.
90
+ *
91
+ * @param {HTMLElement} hostEl
92
+ * @param {Object} [glOptions={}]
93
+ * @returns {{canvas:HTMLCanvasElement, gl:WebGLRenderingContext, width:number, height:number, dpr:number}}
94
+ */
95
+ function createWebGLOverlay(hostEl, glOptions = {}) {
96
+ const { width, height } = hostEl.getBoundingClientRect();
97
+ const dpr = window.devicePixelRatio || 1;
98
+
99
+ if (getComputedStyle(hostEl).position === 'static') {
100
+ hostEl.style.position = 'relative';
101
+ }
102
+
103
+ const canvas = document.createElement('canvas');
104
+ canvas.width = Math.max(1, Math.round(width * dpr));
105
+ canvas.height = Math.max(1, Math.round(height * dpr));
106
+ Object.assign(canvas.style, {
107
+ position: 'absolute',
108
+ top: '0',
109
+ left: '0',
110
+ width: `${width}px`,
111
+ height: `${height}px`,
112
+ pointerEvents: 'none',
113
+ //zIndex: '-999'
114
+ });
115
+
116
+ hostEl.appendChild(canvas);
117
+
118
+ const gl =
119
+ canvas.getContext('webgl2', glOptions) ||
120
+ canvas.getContext('webgl', glOptions) ||
121
+ canvas.getContext('experimental-webgl', glOptions);
122
+
123
+ if (!gl) {
124
+ throw new Error('Your browser doesn’t support WebGL.');
125
+ }
126
+
127
+ gl.viewport(0, 0, canvas.width, canvas.height);
128
+ return { canvas, gl, width, height, dpr };
129
+ }
130
+
131
+ // Auto-initialise on import.
132
+ JTHREE.init_convert();
133
+ window.JThree = JTHREE;
134
+
135
+ export { JTHREE };
136
+ export default JTHREE;