phoria2 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +9 -6
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +436 -436
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1240 -862
  7. package/dist/core.js.map +1 -1
  8. package/dist/debug.js +297 -0
  9. package/dist/debug.js.map +1 -0
  10. package/dist/edit.js +40 -40
  11. package/dist/edit.js.map +1 -1
  12. package/dist/explain.js +32 -13
  13. package/dist/explain.js.map +1 -1
  14. package/dist/gltf.js +128 -128
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +19 -19
  17. package/dist/keyboard.js.map +1 -1
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +36 -36
  21. package/dist/particles.js.map +1 -1
  22. package/dist/passes.js +116 -104
  23. package/dist/passes.js.map +1 -1
  24. package/dist/phoria2.js +1 -1
  25. package/dist/picking.js +90 -71
  26. package/dist/picking.js.map +1 -1
  27. package/dist/snapshot.js +1 -1
  28. package/dist/tooltip.js +1 -1
  29. package/dist/types/View.d.ts +16 -0
  30. package/dist/types/debug.d.ts +74 -0
  31. package/dist/types/explain.d.ts +4 -0
  32. package/dist/types/frame.d.ts +5 -0
  33. package/dist/types/material/Material.d.ts +52 -0
  34. package/dist/types/watch.d.ts +8 -1
  35. package/dist/types/webgl/hooks.d.ts +48 -0
  36. package/dist/types/webgl/passes.d.ts +4 -0
  37. package/dist/types/webgl/programs.d.ts +15 -5
  38. package/dist/types/webgl/shaders.d.ts +36 -5
  39. package/llms-full.txt +227 -0
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/schema/scene.schema.json +27 -0
  43. package/src/View.js +105 -7
  44. package/src/debug.js +283 -0
  45. package/src/explain.js +19 -0
  46. package/src/frame.js +7 -2
  47. package/src/material/Material.js +121 -2
  48. package/src/scene/drawables.js +21 -4
  49. package/src/watch.js +18 -2
  50. package/src/webgl/WebGLRenderer.js +19 -2
  51. package/src/webgl/hooks.js +121 -0
  52. package/src/webgl/passes.js +17 -2
  53. package/src/webgl/picking.js +23 -2
  54. package/src/webgl/programs.js +20 -9
  55. package/src/webgl/resources.js +1 -1
  56. package/src/webgl/shaders.js +443 -35
package/dist/edit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"edit.js","names":["checkOneOf","checkNumber","checkOptions","Mesh","fail","describe","Group","cylinder","Quat","torus","Vec3","HANDLE_OPTIONS","HANDLE_HINTS","COLOURS","ON_TOP","TransformHandle","options","#view","bool","step","v","k","key","fn","#build","#follow","#target","node","#taken","#drag","drag","material","add","name","mesh","color","part","#parts","arrow","along","axis","d","target","at","view","cam","height","perPixel","box","size","r","center","eye","facing","start","p","#from","yawOf","from","proposal","angle","turn","move","result","placed","turned","x","y","z","w","q","HISTORY_OPTIONS","HINTS","History","entry","#done","#undone","list","nodes","before","placeOf","label","after","i","finalLabel","place","n"],"sources":["../src/edit/TransformHandle.js","../src/edit/History.js"],"sourcesContent":["// A move and turn handle for editors: arrows to slide what is selected along an axis, a disc to\n// slide it freely across the floor, and a ring round it to turn it - drawn over everything, the\n// same size on screen however far away, snapping to a grid and to angles, and recording undo.\nimport { Group } from '../scene/Node.js'\nimport { Mesh } from '../scene/drawables.js'\nimport { Quat } from '../value/Quat.js'\nimport { Vec3 } from '../value/Vec3.js'\nimport { cylinder, torus } from '../geometry/generators.js'\nimport { checkNumber, checkOneOf, checkOptions, describe, fail } from '../validate.js'\n\n/** @import { Node, Drag } from '../scene/Node.js' */\n/** @import { History } from './History.js' */\n/** @import { View } from '../View.js' */\n\n/**\n * A move or turn the handle proposes, for constrain() to check: where the target would be, and\n * its turn about the vertical.\n * @typedef {{ position: Vec3, rotation: number }} Placement\n */\n\n/**\n * @typedef {object} TransformHandleOptions\n * @property {View} view the view it is used in - it adds itself to the view's scene\n * @property {Node | null} [target] what it moves; change it later with handle.target\n * @property {'xz' | 'xyz' | false} [move] which ways it moves things: across the floor (default), up and down too, or not at all\n * @property {boolean} [turn] a ring to turn things about the vertical (default true)\n * @property {{ move?: number, turn?: number } | null} [snap] snap positions to a grid of this spacing, and turns to steps of\n * this many radians (default none)\n * @property {number} [size] how big it is on screen, in pixels across its ring (default 120); it grows to go round a big target\n * @property {boolean} [dragTarget] drag the target itself across the floor too, as well as by the handle (default true)\n * @property {History | null} [history] record each move and turn here, for undo (default none)\n * @property {((placement: Placement, target: Node) => Placement | null | void) | null} [constrain] check a move or turn before it\n * happens: return a changed placement (kept inside a room, out of other things), null to refuse it, or nothing to allow it\n * @property {((target: Node, phase: 'start' | 'move' | 'end') => void) | null} [onChange] told as the target is moved or turned\n */\n\nexport const HANDLE_OPTIONS = ['view', 'target', 'move', 'turn', 'snap', 'size', 'dragTarget', 'history', 'constrain', 'onChange']\nexport const HANDLE_HINTS = {\n object: \"use 'target'\",\n mode: \"use 'move' ('xz', 'xyz' or false) and 'turn' (true or false)\",\n rotate: \"use 'turn' - a ring for turning about the vertical\",\n translate: \"use 'move': 'xz', 'xyz' or false\",\n translationSnap: \"use snap: { move, turn }\",\n rotationSnap: \"use snap: { move, turn }\",\n onDrag: \"use 'onChange': (target, phase) => ...\",\n}\n\nconst COLOURS = { x: '#ff5a52', y: '#5ad07a', z: '#4f8cff', ring: '#ffcc33', disc: '#f2f2f2' }\n// drawn after everything, over it\nconst ON_TOP = 100000\n\nexport class TransformHandle {\n /** The handle's own nodes, in the view's scene. */\n node = new Group({ name: 'transform-handle', highlight: '#ffffff' })\n /** @type {'xz' | 'xyz' | false} */ move = 'xz'\n turn = true\n /** @type {{ move: number, turn: number } | null} */ snap = null\n size = 120\n dragTarget = true\n /** @type {History | null} */ history = null\n /** @type {((placement: Placement, target: Node) => Placement | null | void) | null} */ constrain = null\n /** @type {((target: Node, phase: 'start' | 'move' | 'end') => void) | null} */ onChange = null\n\n /** @type {View} */ #view\n /** @type {Node | null} */ #target = null\n /** The node whose onDrag the handle took over, and that onDrag, to give back. @type {{ node: Node, onDrag: ((drag: Drag) => void) | null } | null} */ #taken = null\n /** @type {Record<string, Mesh>} */ #parts = {}\n /** Where the target was as a drag began, and the undo it will record. @type {{ position: Vec3, rotation: number, quaternion: Quat, angle: number, done: ((label?: string) => boolean) | null } | null} */ #from = null\n\n /** @param {TransformHandleOptions} options */\n constructor(options) {\n checkOptions('TransformHandle', options, HANDLE_OPTIONS, HANDLE_HINTS)\n if (!options?.view?.scene) fail('TransformHandle', \"'view' is required - the View it is used in\")\n this.#view = options.view\n if (options.move !== undefined) this.move = options.move === false ? false : /** @type {'xz' | 'xyz'} */ (checkOneOf('TransformHandle', 'move', options.move, ['xz', 'xyz']))\n if (options.turn !== undefined) this.turn = bool('turn', options.turn)\n if (options.dragTarget !== undefined) this.dragTarget = bool('dragTarget', options.dragTarget)\n if (options.size !== undefined) {\n this.size = checkNumber('TransformHandle', 'size', options.size)\n if (!(this.size > 0)) fail('TransformHandle', `'size' is in pixels, above 0, got ${this.size}`)\n }\n if (options.snap != null) {\n checkOptions('TransformHandle', options.snap, ['move', 'turn'], {}, 'snap')\n const step = (k) => {\n const v = options.snap?.[k]\n if (v === undefined) return 0\n if (typeof v !== 'number' || !(v >= 0)) fail('TransformHandle', `'snap.${k}' must be a step of 0 or more, got ${describe(v)}`)\n return v\n }\n this.snap = { move: step('move'), turn: step('turn') }\n }\n for (const key of ['constrain', 'onChange']) {\n const fn = options[key]\n if (fn == null) continue\n if (typeof fn !== 'function') fail('TransformHandle', `'${key}' must be a function, got ${describe(fn)}`)\n this[key] = fn\n }\n if (options.history != null) {\n if (typeof options.history.transform !== 'function') fail('TransformHandle', `'history' must be a History, got ${describe(options.history)}`)\n this.history = options.history\n }\n this.#build()\n this.node.visible = false\n this.node.onUpdate(() => this.#follow())\n this.#view.scene.add(this.node)\n if (options.target != null) this.target = options.target\n }\n\n /** What the handle moves, or null to hide it. @returns {Node | null} */\n get target() { return this.#target }\n /** @param {Node | null} node */\n set target(node) {\n if (node === this.#target) return\n // give back the old target's own drag - whatever dragTarget says now\n if (this.#taken) this.#taken.node.onDrag = this.#taken.onDrag\n this.#taken = null\n this.#target = node ?? null\n this.node.visible = !!node\n if (node && this.dragTarget) {\n this.#taken = { node, onDrag: node.onDrag }\n node.onDrag = (drag) => this.#drag('free', drag)\n }\n if (node) this.#follow()\n this.#view.invalidate()\n }\n\n /** Take the handle out of the scene, giving the target back its own drag. */\n dispose() {\n this.target = null\n this.node.removeFromParent()\n }\n\n #build() {\n const material = (color) => ({ color, unlit: true, depthTest: false, depthWrite: false, opacity: 0.95 })\n /** @param {string} name @param {any} geometry @param {string} color @param {number[]} [position] @param {any} [part] */\n const add = (name, geometry, color, position = [0, 0, 0], part = name) => {\n const mesh = this.node.add(new Mesh({ name, geometry, material: material(color), position, renderOrder: ON_TOP, castShadow: false }))\n mesh.onDrag = (drag) => this.#drag(part, drag)\n this.#parts[name] = mesh\n return mesh\n }\n // the ring round it, for turning\n add('ring', torus({ radius: 1, tube: 0.035, segments: 72, tubeSegments: 8 }), COLOURS.ring)\n // arrows out beyond the ring along each way it moves, and a disc in the middle to move it freely\n const arrow = (axis, color) => {\n const along = (d) => /** @type {number[]} */ (axis === 'x' ? [d, 0, 0] : axis === 'y' ? [0, d, 0] : [0, 0, d])\n add(`${axis}-shaft`, cylinder({ radius: 0.035, height: 0.4, segments: 10, axis }), color, along(1.3), axis)\n add(`${axis}-tip`, cylinder({ radius: 0.1, radiusTop: 0, height: 0.22, segments: 16, axis }), color, along(1.6), axis)\n }\n arrow('x', COLOURS.x)\n arrow('z', COLOURS.z)\n arrow('y', COLOURS.y)\n add('disc', cylinder({ radius: 0.16, height: 0.02, segments: 32 }), COLOURS.disc, [0, 0, 0], 'free')\n }\n\n // sit on the target, the same size on screen at any distance - but always round it\n #follow() {\n const target = this.#target\n if (!target) return\n for (const [name, mesh] of Object.entries(this.#parts)) {\n const axis = name[0]\n mesh.visible = name === 'ring' ? this.turn : name === 'disc' ? !!this.move : !!this.move && (axis !== 'y' || this.move === 'xyz')\n }\n target.updateWorld()\n const at = target.worldPosition\n this.node.position.copy(at)\n const view = this.#view, cam = /** @type {any} */ (view.camera)\n const height = view.canvas.clientHeight || 1\n const perPixel = cam.constructor.type === 'orthographic-camera' ? cam.height / height\n : 2 * cam.position.distanceTo(at) * Math.tan(cam.fov * Math.PI / 360) / height\n // round the target's footprint, at least\n const box = target.bounds, size = box.isEmpty() ? 0 : Math.hypot(box.max[0] - box.min[0], box.max[2] - box.min[2]) / 2\n const r = Math.max(this.size / 2 * perPixel, size * 1.1)\n this.node.scale.set(r, r, r)\n }\n\n /** @param {'x' | 'y' | 'z' | 'free' | 'ring'} part @param {Drag} drag */\n #drag(part, drag) {\n const target = this.#target\n if (!target) return\n if (drag.phase === 'start') {\n const center = target.worldPosition\n // up and down: across an upright plane facing the camera\n if (part === 'y') {\n const eye = this.#view.camera.position\n const facing = new Vec3(eye[0] - center[0], 0, eye[2] - center[2])\n drag.plane = { normal: facing.len() > 1e-6 ? facing.normalize() : new Vec3(0, 0, 1), point: center.clone() }\n } else {\n // the rest across the floor the target stands on\n drag.plane = { normal: new Vec3(0, 1, 0), point: center.clone() }\n }\n const start = drag.ray.intersectPlane(drag.plane.normal, drag.plane.point)\n const p = start !== null ? drag.ray.at(start) : center\n this.#from = {\n position: target.position.clone(), rotation: yawOf(target.quaternion), quaternion: target.quaternion.clone(),\n angle: Math.atan2(p[2] - center[2], p[0] - center[0]),\n done: this.history ? this.history.transform(target) : null,\n }\n this.onChange?.(target, 'start')\n return\n }\n const from = this.#from\n if (!from) return\n if (drag.phase === 'end') {\n from.done?.(part === 'ring' ? 'Turn' : 'Move')\n this.#from = null\n this.onChange?.(target, 'end')\n return\n }\n if (!drag.point) return\n const proposal = { position: from.position.clone(), rotation: from.rotation }\n if (part === 'ring') {\n const center = this.node.position\n const angle = Math.atan2(drag.point[2] - center[2], drag.point[0] - center[0])\n // turning anticlockwise seen from above is a positive turn about y, which runs the other way to the angle in x, z\n let turn = from.rotation - (angle - from.angle)\n if (this.snap?.turn) turn = Math.round(turn / this.snap.turn) * this.snap.turn\n proposal.rotation = turn\n } else {\n const d = drag.delta\n const move = part === 'x' ? [d[0], 0, 0] : part === 'z' ? [0, 0, d[2]] : part === 'y' ? [0, d[1], 0] : [d[0], 0, d[2]]\n proposal.position.add(move)\n const step = this.snap?.move\n if (step) {\n // snap only the axes this drag moves, so the others stay where they were\n for (let k = 0; k < 3; k++) if (move[k] !== 0) proposal.position[k] = Math.round(proposal.position[k] / step) * step\n }\n }\n const result = this.constrain ? this.constrain(proposal, target) : proposal\n if (result === null) return\n const placed = /** @type {Placement} */ (result ?? proposal)\n target.position.copy(placed.position)\n // the turn about the vertical since the drag began, before the rotation it began with - so a\n // yaw past 90 degrees, or a tilt, comes through as it was\n const turned = new Quat().setFromAxisAngle([0, 1, 0], placed.rotation - from.rotation)\n target.quaternion.copy(turned.multiply(from.quaternion))\n this.#follow()\n this.onChange?.(target, 'move')\n }\n}\n\n/**\n * Which way a rotation faces about the vertical, in radians: where it turns the x axis. Euler y\n * can't say - a quaternion read back as Euler angles gives y between -90 and 90 degrees, with x and z\n * flipped to make up the rest.\n * @param {Quat} q\n * @returns {number}\n */\nfunction yawOf(q) {\n const [x, y, z, w] = q\n // the rotated x axis, (1 - 2(y² + z²), 2(xy + zw), 2(xz - yw)), seen from above\n return Math.atan2(-2 * (x * z - y * w), 1 - 2 * (y * y + z * z))\n}\n\n/** @param {string} name @param {unknown} v */\nfunction bool(name, v) {\n if (typeof v !== 'boolean') fail('TransformHandle', `'${name}' must be true or false, got ${describe(v)}`)\n return v\n}\n","// Undo and redo for editors: a list of what was done, each with how to undo and redo it.\nimport { checkOptions, describe, fail } from '../validate.js'\n\n/** @import { Node } from '../scene/Node.js' */\n\n/**\n * Something done, that can be undone and done again.\n * @typedef {object} HistoryEntry\n * @property {string} [label] what it was, for an \"Undo move\" menu item\n * @property {() => void} undo\n * @property {() => void} redo\n */\n\n/**\n * @typedef {object} HistoryOptions\n * @property {number} [limit] how many steps back it keeps (default 100)\n * @property {((history: History) => void) | null} [onChange] called whenever what can be undone or redone changes\n */\n\nexport const HISTORY_OPTIONS = ['limit', 'onChange']\nconst HINTS = { max: \"use 'limit'\", size: \"use 'limit'\", onUpdate: \"use 'onChange'\" }\n\n/**\n * Undo and redo. Record each change once it has been made, with how to undo and redo it; or let\n * transform() work that out for moves, turns and resizes.\n *\n * const history = new History()\n * const done = history.transform([chair]) // before moving it\n * chair.position.x += 1\n * done('Move chair') // after: recorded, if it moved\n * history.undo()\n */\nexport class History {\n /** How many steps back are kept. */\n limit = 100\n /** @type {((history: History) => void) | null} */\n onChange = null\n /** @type {HistoryEntry[]} */ #done = []\n /** @type {HistoryEntry[]} */ #undone = []\n\n /** @param {HistoryOptions} [options] */\n constructor(options = {}) {\n checkOptions('History', options, HISTORY_OPTIONS, HINTS)\n if (options.limit !== undefined) {\n if (!Number.isInteger(options.limit) || options.limit < 1) fail('History', `'limit' must be a whole number 1 or more, got ${describe(options.limit)}`)\n this.limit = options.limit\n }\n if (options.onChange != null) {\n if (typeof options.onChange !== 'function') fail('History', `'onChange' must be a function, got ${describe(options.onChange)}`)\n this.onChange = options.onChange\n }\n }\n\n /**\n * Record a change already made. Anything undone before it can no longer be redone.\n * @param {HistoryEntry} entry\n * @returns {this}\n */\n push(entry) {\n if (!entry || typeof entry.undo !== 'function' || typeof entry.redo !== 'function') {\n fail('History', `push() takes { label, undo, redo } with undo and redo functions, got ${describe(entry)}`)\n }\n this.#done.push(entry)\n if (this.#done.length > this.limit) this.#done.shift()\n this.#undone = []\n this.onChange?.(this)\n return this\n }\n\n /** Undo the last change. @returns {boolean} whether there was one */\n undo() {\n const entry = this.#done.pop()\n if (!entry) return false\n entry.undo()\n this.#undone.push(entry)\n this.onChange?.(this)\n return true\n }\n\n /** Do again the last change undone. @returns {boolean} whether there was one */\n redo() {\n const entry = this.#undone.pop()\n if (!entry) return false\n entry.redo()\n this.#done.push(entry)\n this.onChange?.(this)\n return true\n }\n\n get canUndo() { return this.#done.length > 0 }\n get canRedo() { return this.#undone.length > 0 }\n /** How many steps undo() can go back - for a button that says \"Undo (3)\". */\n get undoCount() { return this.#done.length }\n /** How many steps redo() can go forward. */\n get redoCount() { return this.#undone.length }\n /** What undo() would undo, or null. @returns {string | null} */\n get undoLabel() { return this.#done.at(-1)?.label ?? null }\n /** What redo() would redo, or null. @returns {string | null} */\n get redoLabel() { return this.#undone.at(-1)?.label ?? null }\n\n /** Forget everything. */\n clear() {\n this.#done = []\n this.#undone = []\n this.onChange?.(this)\n }\n\n /**\n * Note where some nodes are now - position, rotation and scale - and get a function to call once\n * they have changed, which records undoing back to here. It records nothing if they haven't moved.\n * @param {Node | Node[]} nodes\n * @param {string} [label]\n * @returns {(label?: string) => boolean} records the change; true if there was one\n */\n transform(nodes, label) {\n const list = [nodes].flat()\n const before = list.map(placeOf)\n return (finalLabel = label) => {\n const after = list.map(placeOf)\n if (after.every((p, i) => p.every((v, k) => v === before[i][k]))) return false\n this.push({ label: finalLabel, undo: () => list.forEach((n, i) => place(n, before[i])), redo: () => list.forEach((n, i) => place(n, after[i])) })\n return true\n }\n }\n}\n\n/** A node's position, rotation (as a quaternion) and scale, as numbers. @param {Node} n */\nconst placeOf = (n) => [...n.position, ...n.quaternion, ...n.scale]\n\n/** @param {Node} n @param {number[]} p */\nfunction place(n, p) {\n n.position.set(p[0], p[1], p[2])\n n.quaternion.set(p[3], p[4], p[5], p[6])\n n.scale.set(p[7], p[8], p[9])\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"edit.js","names":["checkNumber","fail","checkOptions","describe","Vec3","Mesh","checkOneOf","Quat","Group","cylinder","torus","HANDLE_OPTIONS","HANDLE_HINTS","COLOURS","ON_TOP","TransformHandle","options","#view","bool","step","v","k","key","fn","#build","#follow","#target","node","#taken","#drag","drag","material","add","name","mesh","color","part","#parts","arrow","along","axis","d","target","at","view","cam","height","perPixel","box","size","r","center","eye","facing","start","p","#from","yawOf","from","proposal","angle","turn","move","result","placed","turned","x","y","z","w","q","HISTORY_OPTIONS","HINTS","History","entry","#done","#undone","list","nodes","before","placeOf","label","after","i","finalLabel","place","n"],"sources":["../src/edit/TransformHandle.js","../src/edit/History.js"],"sourcesContent":["// A move and turn handle for editors: arrows to slide what is selected along an axis, a disc to\n// slide it freely across the floor, and a ring round it to turn it - drawn over everything, the\n// same size on screen however far away, snapping to a grid and to angles, and recording undo.\nimport { Group } from '../scene/Node.js'\nimport { Mesh } from '../scene/drawables.js'\nimport { Quat } from '../value/Quat.js'\nimport { Vec3 } from '../value/Vec3.js'\nimport { cylinder, torus } from '../geometry/generators.js'\nimport { checkNumber, checkOneOf, checkOptions, describe, fail } from '../validate.js'\n\n/** @import { Node, Drag } from '../scene/Node.js' */\n/** @import { History } from './History.js' */\n/** @import { View } from '../View.js' */\n\n/**\n * A move or turn the handle proposes, for constrain() to check: where the target would be, and\n * its turn about the vertical.\n * @typedef {{ position: Vec3, rotation: number }} Placement\n */\n\n/**\n * @typedef {object} TransformHandleOptions\n * @property {View} view the view it is used in - it adds itself to the view's scene\n * @property {Node | null} [target] what it moves; change it later with handle.target\n * @property {'xz' | 'xyz' | false} [move] which ways it moves things: across the floor (default), up and down too, or not at all\n * @property {boolean} [turn] a ring to turn things about the vertical (default true)\n * @property {{ move?: number, turn?: number } | null} [snap] snap positions to a grid of this spacing, and turns to steps of\n * this many radians (default none)\n * @property {number} [size] how big it is on screen, in pixels across its ring (default 120); it grows to go round a big target\n * @property {boolean} [dragTarget] drag the target itself across the floor too, as well as by the handle (default true)\n * @property {History | null} [history] record each move and turn here, for undo (default none)\n * @property {((placement: Placement, target: Node) => Placement | null | void) | null} [constrain] check a move or turn before it\n * happens: return a changed placement (kept inside a room, out of other things), null to refuse it, or nothing to allow it\n * @property {((target: Node, phase: 'start' | 'move' | 'end') => void) | null} [onChange] told as the target is moved or turned\n */\n\nexport const HANDLE_OPTIONS = ['view', 'target', 'move', 'turn', 'snap', 'size', 'dragTarget', 'history', 'constrain', 'onChange']\nexport const HANDLE_HINTS = {\n object: \"use 'target'\",\n mode: \"use 'move' ('xz', 'xyz' or false) and 'turn' (true or false)\",\n rotate: \"use 'turn' - a ring for turning about the vertical\",\n translate: \"use 'move': 'xz', 'xyz' or false\",\n translationSnap: \"use snap: { move, turn }\",\n rotationSnap: \"use snap: { move, turn }\",\n onDrag: \"use 'onChange': (target, phase) => ...\",\n}\n\nconst COLOURS = { x: '#ff5a52', y: '#5ad07a', z: '#4f8cff', ring: '#ffcc33', disc: '#f2f2f2' }\n// drawn after everything, over it\nconst ON_TOP = 100000\n\nexport class TransformHandle {\n /** The handle's own nodes, in the view's scene. */\n node = new Group({ name: 'transform-handle', highlight: '#ffffff' })\n /** @type {'xz' | 'xyz' | false} */ move = 'xz'\n turn = true\n /** @type {{ move: number, turn: number } | null} */ snap = null\n size = 120\n dragTarget = true\n /** @type {History | null} */ history = null\n /** @type {((placement: Placement, target: Node) => Placement | null | void) | null} */ constrain = null\n /** @type {((target: Node, phase: 'start' | 'move' | 'end') => void) | null} */ onChange = null\n\n /** @type {View} */ #view\n /** @type {Node | null} */ #target = null\n /** The node whose onDrag the handle took over, and that onDrag, to give back. @type {{ node: Node, onDrag: ((drag: Drag) => void) | null } | null} */ #taken = null\n /** @type {Record<string, Mesh>} */ #parts = {}\n /** Where the target was as a drag began, and the undo it will record. @type {{ position: Vec3, rotation: number, quaternion: Quat, angle: number, done: ((label?: string) => boolean) | null } | null} */ #from = null\n\n /** @param {TransformHandleOptions} options */\n constructor(options) {\n checkOptions('TransformHandle', options, HANDLE_OPTIONS, HANDLE_HINTS)\n if (!options?.view?.scene) fail('TransformHandle', \"'view' is required - the View it is used in\")\n this.#view = options.view\n if (options.move !== undefined) this.move = options.move === false ? false : /** @type {'xz' | 'xyz'} */ (checkOneOf('TransformHandle', 'move', options.move, ['xz', 'xyz']))\n if (options.turn !== undefined) this.turn = bool('turn', options.turn)\n if (options.dragTarget !== undefined) this.dragTarget = bool('dragTarget', options.dragTarget)\n if (options.size !== undefined) {\n this.size = checkNumber('TransformHandle', 'size', options.size)\n if (!(this.size > 0)) fail('TransformHandle', `'size' is in pixels, above 0, got ${this.size}`)\n }\n if (options.snap != null) {\n checkOptions('TransformHandle', options.snap, ['move', 'turn'], {}, 'snap')\n const step = (k) => {\n const v = options.snap?.[k]\n if (v === undefined) return 0\n if (typeof v !== 'number' || !(v >= 0)) fail('TransformHandle', `'snap.${k}' must be a step of 0 or more, got ${describe(v)}`)\n return v\n }\n this.snap = { move: step('move'), turn: step('turn') }\n }\n for (const key of ['constrain', 'onChange']) {\n const fn = options[key]\n if (fn == null) continue\n if (typeof fn !== 'function') fail('TransformHandle', `'${key}' must be a function, got ${describe(fn)}`)\n this[key] = fn\n }\n if (options.history != null) {\n if (typeof options.history.transform !== 'function') fail('TransformHandle', `'history' must be a History, got ${describe(options.history)}`)\n this.history = options.history\n }\n this.#build()\n this.node.visible = false\n this.node.onUpdate(() => this.#follow())\n this.#view.scene.add(this.node)\n if (options.target != null) this.target = options.target\n }\n\n /** What the handle moves, or null to hide it. @returns {Node | null} */\n get target() { return this.#target }\n /** @param {Node | null} node */\n set target(node) {\n if (node === this.#target) return\n // give back the old target's own drag - whatever dragTarget says now\n if (this.#taken) this.#taken.node.onDrag = this.#taken.onDrag\n this.#taken = null\n this.#target = node ?? null\n this.node.visible = !!node\n if (node && this.dragTarget) {\n this.#taken = { node, onDrag: node.onDrag }\n node.onDrag = (drag) => this.#drag('free', drag)\n }\n if (node) this.#follow()\n this.#view.invalidate()\n }\n\n /** Take the handle out of the scene, giving the target back its own drag. */\n dispose() {\n this.target = null\n this.node.removeFromParent()\n }\n\n #build() {\n const material = (color) => ({ color, unlit: true, depthTest: false, depthWrite: false, opacity: 0.95 })\n /** @param {string} name @param {any} geometry @param {string} color @param {number[]} [position] @param {any} [part] */\n const add = (name, geometry, color, position = [0, 0, 0], part = name) => {\n const mesh = this.node.add(new Mesh({ name, geometry, material: material(color), position, renderOrder: ON_TOP, castShadow: false }))\n mesh.onDrag = (drag) => this.#drag(part, drag)\n this.#parts[name] = mesh\n return mesh\n }\n // the ring round it, for turning\n add('ring', torus({ radius: 1, tube: 0.035, segments: 72, tubeSegments: 8 }), COLOURS.ring)\n // arrows out beyond the ring along each way it moves, and a disc in the middle to move it freely\n const arrow = (axis, color) => {\n const along = (d) => /** @type {number[]} */ (axis === 'x' ? [d, 0, 0] : axis === 'y' ? [0, d, 0] : [0, 0, d])\n add(`${axis}-shaft`, cylinder({ radius: 0.035, height: 0.4, segments: 10, axis }), color, along(1.3), axis)\n add(`${axis}-tip`, cylinder({ radius: 0.1, radiusTop: 0, height: 0.22, segments: 16, axis }), color, along(1.6), axis)\n }\n arrow('x', COLOURS.x)\n arrow('z', COLOURS.z)\n arrow('y', COLOURS.y)\n add('disc', cylinder({ radius: 0.16, height: 0.02, segments: 32 }), COLOURS.disc, [0, 0, 0], 'free')\n }\n\n // sit on the target, the same size on screen at any distance - but always round it\n #follow() {\n const target = this.#target\n if (!target) return\n for (const [name, mesh] of Object.entries(this.#parts)) {\n const axis = name[0]\n mesh.visible = name === 'ring' ? this.turn : name === 'disc' ? !!this.move : !!this.move && (axis !== 'y' || this.move === 'xyz')\n }\n target.updateWorld()\n const at = target.worldPosition\n this.node.position.copy(at)\n const view = this.#view, cam = /** @type {any} */ (view.camera)\n const height = view.canvas.clientHeight || 1\n const perPixel = cam.constructor.type === 'orthographic-camera' ? cam.height / height\n : 2 * cam.position.distanceTo(at) * Math.tan(cam.fov * Math.PI / 360) / height\n // round the target's footprint, at least\n const box = target.bounds, size = box.isEmpty() ? 0 : Math.hypot(box.max[0] - box.min[0], box.max[2] - box.min[2]) / 2\n const r = Math.max(this.size / 2 * perPixel, size * 1.1)\n this.node.scale.set(r, r, r)\n }\n\n /** @param {'x' | 'y' | 'z' | 'free' | 'ring'} part @param {Drag} drag */\n #drag(part, drag) {\n const target = this.#target\n if (!target) return\n if (drag.phase === 'start') {\n const center = target.worldPosition\n // up and down: across an upright plane facing the camera\n if (part === 'y') {\n const eye = this.#view.camera.position\n const facing = new Vec3(eye[0] - center[0], 0, eye[2] - center[2])\n drag.plane = { normal: facing.len() > 1e-6 ? facing.normalize() : new Vec3(0, 0, 1), point: center.clone() }\n } else {\n // the rest across the floor the target stands on\n drag.plane = { normal: new Vec3(0, 1, 0), point: center.clone() }\n }\n const start = drag.ray.intersectPlane(drag.plane.normal, drag.plane.point)\n const p = start !== null ? drag.ray.at(start) : center\n this.#from = {\n position: target.position.clone(), rotation: yawOf(target.quaternion), quaternion: target.quaternion.clone(),\n angle: Math.atan2(p[2] - center[2], p[0] - center[0]),\n done: this.history ? this.history.transform(target) : null,\n }\n this.onChange?.(target, 'start')\n return\n }\n const from = this.#from\n if (!from) return\n if (drag.phase === 'end') {\n from.done?.(part === 'ring' ? 'Turn' : 'Move')\n this.#from = null\n this.onChange?.(target, 'end')\n return\n }\n if (!drag.point) return\n const proposal = { position: from.position.clone(), rotation: from.rotation }\n if (part === 'ring') {\n const center = this.node.position\n const angle = Math.atan2(drag.point[2] - center[2], drag.point[0] - center[0])\n // turning anticlockwise seen from above is a positive turn about y, which runs the other way to the angle in x, z\n let turn = from.rotation - (angle - from.angle)\n if (this.snap?.turn) turn = Math.round(turn / this.snap.turn) * this.snap.turn\n proposal.rotation = turn\n } else {\n const d = drag.delta\n const move = part === 'x' ? [d[0], 0, 0] : part === 'z' ? [0, 0, d[2]] : part === 'y' ? [0, d[1], 0] : [d[0], 0, d[2]]\n proposal.position.add(move)\n const step = this.snap?.move\n if (step) {\n // snap only the axes this drag moves, so the others stay where they were\n for (let k = 0; k < 3; k++) if (move[k] !== 0) proposal.position[k] = Math.round(proposal.position[k] / step) * step\n }\n }\n const result = this.constrain ? this.constrain(proposal, target) : proposal\n if (result === null) return\n const placed = /** @type {Placement} */ (result ?? proposal)\n target.position.copy(placed.position)\n // the turn about the vertical since the drag began, before the rotation it began with - so a\n // yaw past 90 degrees, or a tilt, comes through as it was\n const turned = new Quat().setFromAxisAngle([0, 1, 0], placed.rotation - from.rotation)\n target.quaternion.copy(turned.multiply(from.quaternion))\n this.#follow()\n this.onChange?.(target, 'move')\n }\n}\n\n/**\n * Which way a rotation faces about the vertical, in radians: where it turns the x axis. Euler y\n * can't say - a quaternion read back as Euler angles gives y between -90 and 90 degrees, with x and z\n * flipped to make up the rest.\n * @param {Quat} q\n * @returns {number}\n */\nfunction yawOf(q) {\n const [x, y, z, w] = q\n // the rotated x axis, (1 - 2(y² + z²), 2(xy + zw), 2(xz - yw)), seen from above\n return Math.atan2(-2 * (x * z - y * w), 1 - 2 * (y * y + z * z))\n}\n\n/** @param {string} name @param {unknown} v */\nfunction bool(name, v) {\n if (typeof v !== 'boolean') fail('TransformHandle', `'${name}' must be true or false, got ${describe(v)}`)\n return v\n}\n","// Undo and redo for editors: a list of what was done, each with how to undo and redo it.\nimport { checkOptions, describe, fail } from '../validate.js'\n\n/** @import { Node } from '../scene/Node.js' */\n\n/**\n * Something done, that can be undone and done again.\n * @typedef {object} HistoryEntry\n * @property {string} [label] what it was, for an \"Undo move\" menu item\n * @property {() => void} undo\n * @property {() => void} redo\n */\n\n/**\n * @typedef {object} HistoryOptions\n * @property {number} [limit] how many steps back it keeps (default 100)\n * @property {((history: History) => void) | null} [onChange] called whenever what can be undone or redone changes\n */\n\nexport const HISTORY_OPTIONS = ['limit', 'onChange']\nconst HINTS = { max: \"use 'limit'\", size: \"use 'limit'\", onUpdate: \"use 'onChange'\" }\n\n/**\n * Undo and redo. Record each change once it has been made, with how to undo and redo it; or let\n * transform() work that out for moves, turns and resizes.\n *\n * const history = new History()\n * const done = history.transform([chair]) // before moving it\n * chair.position.x += 1\n * done('Move chair') // after: recorded, if it moved\n * history.undo()\n */\nexport class History {\n /** How many steps back are kept. */\n limit = 100\n /** @type {((history: History) => void) | null} */\n onChange = null\n /** @type {HistoryEntry[]} */ #done = []\n /** @type {HistoryEntry[]} */ #undone = []\n\n /** @param {HistoryOptions} [options] */\n constructor(options = {}) {\n checkOptions('History', options, HISTORY_OPTIONS, HINTS)\n if (options.limit !== undefined) {\n if (!Number.isInteger(options.limit) || options.limit < 1) fail('History', `'limit' must be a whole number 1 or more, got ${describe(options.limit)}`)\n this.limit = options.limit\n }\n if (options.onChange != null) {\n if (typeof options.onChange !== 'function') fail('History', `'onChange' must be a function, got ${describe(options.onChange)}`)\n this.onChange = options.onChange\n }\n }\n\n /**\n * Record a change already made. Anything undone before it can no longer be redone.\n * @param {HistoryEntry} entry\n * @returns {this}\n */\n push(entry) {\n if (!entry || typeof entry.undo !== 'function' || typeof entry.redo !== 'function') {\n fail('History', `push() takes { label, undo, redo } with undo and redo functions, got ${describe(entry)}`)\n }\n this.#done.push(entry)\n if (this.#done.length > this.limit) this.#done.shift()\n this.#undone = []\n this.onChange?.(this)\n return this\n }\n\n /** Undo the last change. @returns {boolean} whether there was one */\n undo() {\n const entry = this.#done.pop()\n if (!entry) return false\n entry.undo()\n this.#undone.push(entry)\n this.onChange?.(this)\n return true\n }\n\n /** Do again the last change undone. @returns {boolean} whether there was one */\n redo() {\n const entry = this.#undone.pop()\n if (!entry) return false\n entry.redo()\n this.#done.push(entry)\n this.onChange?.(this)\n return true\n }\n\n get canUndo() { return this.#done.length > 0 }\n get canRedo() { return this.#undone.length > 0 }\n /** How many steps undo() can go back - for a button that says \"Undo (3)\". */\n get undoCount() { return this.#done.length }\n /** How many steps redo() can go forward. */\n get redoCount() { return this.#undone.length }\n /** What undo() would undo, or null. @returns {string | null} */\n get undoLabel() { return this.#done.at(-1)?.label ?? null }\n /** What redo() would redo, or null. @returns {string | null} */\n get redoLabel() { return this.#undone.at(-1)?.label ?? null }\n\n /** Forget everything. */\n clear() {\n this.#done = []\n this.#undone = []\n this.onChange?.(this)\n }\n\n /**\n * Note where some nodes are now - position, rotation and scale - and get a function to call once\n * they have changed, which records undoing back to here. It records nothing if they haven't moved.\n * @param {Node | Node[]} nodes\n * @param {string} [label]\n * @returns {(label?: string) => boolean} records the change; true if there was one\n */\n transform(nodes, label) {\n const list = [nodes].flat()\n const before = list.map(placeOf)\n return (finalLabel = label) => {\n const after = list.map(placeOf)\n if (after.every((p, i) => p.every((v, k) => v === before[i][k]))) return false\n this.push({ label: finalLabel, undo: () => list.forEach((n, i) => place(n, before[i])), redo: () => list.forEach((n, i) => place(n, after[i])) })\n return true\n }\n }\n}\n\n/** A node's position, rotation (as a quaternion) and scale, as numbers. @param {Node} n */\nconst placeOf = (n) => [...n.position, ...n.quaternion, ...n.scale]\n\n/** @param {Node} n @param {number[]} p */\nfunction place(n, p) {\n n.position.set(p[0], p[1], p[2])\n n.quaternion.set(p[3], p[4], p[5], p[6])\n n.scale.set(p[7], p[8], p[9])\n}\n"],"mappings":";;AAoCA,IAAaW,IAAiB;CAAC;CAAQ;CAAU;CAAQ;CAAQ;CAAQ;CAAQ;CAAc;CAAW;CAAa;AAAU,GACpHC,IAAe;CAC1B,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,WAAW;CACX,iBAAiB;CACjB,cAAc;CACd,QAAQ;AACV,GAEMC,IAAU;CAAE,GAAG;CAAW,GAAG;CAAW,GAAG;CAAW,MAAM;CAAW,MAAM;AAAU,GAEvFC,IAAS,KAEFC,IAAb,MAA6B;CAE3B,OAAO,IAAIP,EAAM;EAAE,MAAM;EAAoB,WAAW;CAAU,CAAC;CAC/B,OAAO;CAC3C,OAAO;CAC8C,OAAO;CAC5D,OAAO;CACP,aAAa;CACiB,UAAU;CACgD,YAAY;CACpB,WAAW;CAEvE;CACO,KAAU;CACkH,KAAS;CAC5H,KAAS,CAAC;CAC6J,KAAQ;CAGnN,YAAY,GAAS;EAWnB,IAVAN,EAAa,mBAAmBc,GAASL,GAAgBC,CAAY,GAChEI,GAAS,MAAM,SAAOf,EAAK,mBAAmB,6CAA6C,GAChG,KAAKgB,KAAQD,EAAQ,MACjBA,EAAQ,SAAS,KAAA,MAAW,KAAK,OAAOA,EAAQ,SAAS,MAA6CV,EAAW,mBAAmB,QAAQU,EAAQ,MAAM,CAAC,MAAM,KAAK,CAAC,IACvKA,EAAQ,SAAS,KAAA,MAAW,KAAK,OAAOE,EAAK,QAAQF,EAAQ,IAAI,IACjEA,EAAQ,eAAe,KAAA,MAAW,KAAK,aAAaE,EAAK,cAAcF,EAAQ,UAAU,IACzFA,EAAQ,SAAS,KAAA,MACnB,KAAK,OAAOhB,EAAY,mBAAmB,QAAQgB,EAAQ,IAAI,GACzD,KAAK,OAAO,KAAIf,EAAK,mBAAmB,qCAAqC,KAAK,MAAM,IAE5Fe,EAAQ,QAAQ,MAAM;GACxB,EAAa,mBAAmBA,EAAQ,MAAM,CAAC,QAAQ,MAAM,GAAG,CAAC,GAAG,MAAM;GAC1E,IAAMG,KAAQ,MAAM;IAClB,IAAMC,IAAIJ,EAAQ,OAAOK;IAGzB,OAFID,MAAM,KAAA,IAAkB,KACxB,OAAOA,KAAM,YAAcA,KAAK,KAAInB,EAAK,mBAAmB,SAASoB,EAAE,qCAAqClB,EAASiB,CAAC,GAAG,GACtHA;GACT;GACA,KAAK,OAAO;IAAE,MAAMD,EAAK,MAAM;IAAG,MAAMA,EAAK,MAAM;GAAE;EACvD;EACA,KAAK,IAAMG,KAAO,CAAC,aAAa,UAAU,GAAG;GAC3C,IAAMC,IAAKP,EAAQM;GACnB,AAAIC,KAAM,SACN,OAAOA,KAAO,cAAYtB,EAAK,mBAAmB,IAAIqB,EAAI,4BAA4BnB,EAASoB,CAAE,GAAG,GACxG,KAAKD,KAAOC;EACd;EASA,AARIP,EAAQ,WAAW,SACjB,OAAOA,EAAQ,QAAQ,aAAc,cAAYf,EAAK,mBAAmB,oCAAoCE,EAASa,EAAQ,OAAO,GAAG,GAC5I,KAAK,UAAUA,EAAQ,UAEzB,KAAKQ,GAAO,GACZ,KAAK,KAAK,UAAU,IACpB,KAAK,KAAK,eAAe,KAAKC,GAAQ,CAAC,GACvC,KAAKR,GAAM,MAAM,IAAI,KAAK,IAAI,GAC1BD,EAAQ,UAAU,SAAM,KAAK,SAASA,EAAQ;CACpD;CAGA,IAAI,SAAS;EAAE,OAAO,KAAKU;CAAQ;CAEnC,IAAI,OAAO,GAAM;EACf,AAAIC,MAAS,KAAKD,OAEd,KAAKE,OAAQ,KAAKA,GAAO,KAAK,SAAS,KAAKA,GAAO,SACvD,KAAKA,KAAS,MACd,KAAKF,KAAUC,KAAQ,MACvB,KAAK,KAAK,UAAU,CAAC,CAACA,GAClBA,KAAQ,KAAK,eACf,KAAKC,KAAS;GAAE;GAAM,QAAQD,EAAK;EAAO,GAC1C,EAAK,UAAU,MAAS,KAAKE,GAAM,QAAQC,CAAI,IAE7CH,KAAM,KAAKF,GAAQ,GACvB,KAAKR,GAAM,WAAW;CACxB;CAGA,UAAU;EAER,AADA,KAAK,SAAS,MACd,KAAK,KAAK,iBAAiB;CAC7B;CAEA,KAAS;EACP,IAAMc,KAAY,OAAW;GAAE;GAAO,OAAO;GAAM,WAAW;GAAO,YAAY;GAAO,SAAS;EAAK,IAEhGC,KAAO,GAAM,GAAU,GAAO,IAAW;GAAC;GAAG;GAAG;EAAC,GAAG,IAAOC,MAAS;GACxE,IAAMC,IAAO,KAAK,KAAK,IAAI,IAAI7B,EAAK;IAAE;IAAM;IAAU,UAAU0B,EAASI,CAAK;IAAG;IAAU,aAAarB;IAAQ,YAAY;GAAM,CAAC,CAAC;GAGpI,OAFA,EAAK,UAAU,MAAS,KAAKe,GAAMO,GAAMN,CAAI,GAC7C,KAAKO,GAAOJ,KAAQC,GACbA;EACT;EAEA,EAAI,QAAQxB,EAAM;GAAE,QAAQ;GAAG,MAAM;GAAO,UAAU;GAAI,cAAc;EAAE,CAAC,GAAGG,EAAQ,IAAI;EAE1F,IAAMyB,KAAS,GAAM,MAAU;GAC7B,IAAMC,KAAS,MAA+BC,MAAS,MAAM;IAACC;IAAG;IAAG;GAAC,IAAID,MAAS,MAAM;IAAC;IAAGC;IAAG;GAAC,IAAI;IAAC;IAAG;IAAGA;GAAC;GAE5G,AADAT,EAAI,GAAGQ,EAAK,SAAS/B,EAAS;IAAE,QAAQ;IAAO,QAAQ;IAAK,UAAU;IAAI;GAAK,CAAC,GAAG0B,GAAOI,EAAM,GAAG,GAAGC,CAAI,GAC1GR,EAAI,GAAGQ,EAAK,OAAO/B,EAAS;IAAE,QAAQ;IAAK,WAAW;IAAG,QAAQ;IAAM,UAAU;IAAI;GAAK,CAAC,GAAG0B,GAAOI,EAAM,GAAG,GAAGC,CAAI;EACvH;EAIA,AAHAF,EAAM,KAAKzB,EAAQ,CAAC,GACpByB,EAAM,KAAKzB,EAAQ,CAAC,GACpByB,EAAM,KAAKzB,EAAQ,CAAC,GACpBmB,EAAI,QAAQvB,EAAS;GAAE,QAAQ;GAAM,QAAQ;GAAM,UAAU;EAAG,CAAC,GAAGI,EAAQ,MAAM;GAAC;GAAG;GAAG;EAAC,GAAG,MAAM;CACrG;CAGA,KAAU;EACR,IAAM6B,IAAS,KAAKhB;EACpB,IAAI,CAACgB,GAAQ;EACb,KAAK,IAAM,CAACT,GAAMC,MAAS,OAAO,QAAQ,KAAKG,EAAM,GAAG;GACtD,IAAMG,IAAOP,EAAK;GAClB,EAAK,UAAUA,MAAS,SAAS,KAAK,OAAOA,MAAS,SAAS,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,KAAK,SAASO,MAAS,OAAO,KAAK,SAAS;EAC7H;EACA,EAAO,YAAY;EACnB,IAAMG,IAAKD,EAAO;EAClB,KAAK,KAAK,SAAS,KAAKC,CAAE;EAC1B,IAAMC,IAAO,KAAK3B,IAAO4B,IAA0BD,EAAK,QAClDE,IAASF,EAAK,OAAO,gBAAgB,GACrCG,IAAWF,EAAI,YAAY,SAAS,wBAAwBA,EAAI,SAASC,IAC3E,IAAID,EAAI,SAAS,WAAWF,CAAE,IAAI,KAAK,IAAIE,EAAI,MAAM,KAAK,KAAK,GAAG,IAAIC,GAEpEE,IAAMN,EAAO,QAAQO,IAAOD,EAAI,QAAQ,IAAI,IAAI,KAAK,MAAMA,EAAI,IAAI,KAAKA,EAAI,IAAI,IAAIA,EAAI,IAAI,KAAKA,EAAI,IAAI,EAAE,IAAI,GAC/GE,IAAI,KAAK,IAAI,KAAK,OAAO,IAAIH,GAAUE,IAAO,GAAG;EACvD,KAAK,KAAK,MAAM,IAAIC,GAAGA,GAAGA,CAAC;CAC7B;CAGA,GAAM,GAAM,GAAM;EAChB,IAAMR,IAAS,KAAKhB;EACpB,IAAI,CAACgB,GAAQ;EACb,IAAIZ,EAAK,UAAU,SAAS;GAC1B,IAAMqB,IAAST,EAAO;GAEtB,IAAIN,MAAS,KAAK;IAChB,IAAMgB,IAAM,KAAKnC,GAAM,OAAO,UACxBoC,IAAS,IAAIjD,EAAKgD,EAAI,KAAKD,EAAO,IAAI,GAAGC,EAAI,KAAKD,EAAO,EAAE;IACjE,EAAK,QAAQ;KAAE,QAAQE,EAAO,IAAI,IAAI,OAAOA,EAAO,UAAU,IAAI,IAAIjD,EAAK,GAAG,GAAG,CAAC;KAAG,OAAO+C,EAAO,MAAM;IAAE;GAC7G,OAEE,EAAK,QAAQ;IAAE,QAAQ,IAAI/C,EAAK,GAAG,GAAG,CAAC;IAAG,OAAO+C,EAAO,MAAM;GAAE;GAElE,IAAMG,IAAQxB,EAAK,IAAI,eAAeA,EAAK,MAAM,QAAQA,EAAK,MAAM,KAAK,GACnEyB,IAAID,MAAU,OAA4BH,IAArBrB,EAAK,IAAI,GAAGwB,CAAK;GAM5C,AALA,KAAKE,KAAQ;IACX,UAAUd,EAAO,SAAS,MAAM;IAAG,UAAUe,EAAMf,EAAO,UAAU;IAAG,YAAYA,EAAO,WAAW,MAAM;IAC3G,OAAO,KAAK,MAAMa,EAAE,KAAKJ,EAAO,IAAII,EAAE,KAAKJ,EAAO,EAAE;IACpD,MAAM,KAAK,UAAU,KAAK,QAAQ,UAAUT,CAAM,IAAI;GACxD,GACA,KAAK,WAAWA,GAAQ,OAAO;GAC/B;EACF;EACA,IAAMgB,IAAO,KAAKF;EAClB,IAAI,CAACE,GAAM;EACX,IAAI5B,EAAK,UAAU,OAAO;GAGxB,AAFA4B,EAAK,OAAOtB,MAAS,SAAS,SAAS,MAAM,GAC7C,KAAKoB,KAAQ,MACb,KAAK,WAAWd,GAAQ,KAAK;GAC7B;EACF;EACA,IAAI,CAACZ,EAAK,OAAO;EACjB,IAAM6B,IAAW;GAAE,UAAUD,EAAK,SAAS,MAAM;GAAG,UAAUA,EAAK;EAAS;EAC5E,IAAItB,MAAS,QAAQ;GACnB,IAAMe,IAAS,KAAK,KAAK,UACnBS,IAAQ,KAAK,MAAM9B,EAAK,MAAM,KAAKqB,EAAO,IAAIrB,EAAK,MAAM,KAAKqB,EAAO,EAAE,GAEzEU,IAAOH,EAAK,YAAYE,IAAQF,EAAK;GAEzC,AADI,KAAK,MAAM,SAAM,IAAO,KAAK,MAAMG,IAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,OAC1E,EAAS,WAAWA;EACtB,OAAO;GACL,IAAMpB,IAAIX,EAAK,OACTgC,IAAO1B,MAAS,MAAM;IAACK,EAAE;IAAI;IAAG;GAAC,IAAIL,MAAS,MAAM;IAAC;IAAG;IAAGK,EAAE;GAAE,IAAIL,MAAS,MAAM;IAAC;IAAGK,EAAE;IAAI;GAAC,IAAI;IAACA,EAAE;IAAI;IAAGA,EAAE;GAAE;GACrH,EAAS,SAAS,IAAIqB,CAAI;GAC1B,IAAM3C,IAAO,KAAK,MAAM;GACxB,IAAIA,GAEG,KAAA,IAAIE,IAAI,GAAGA,IAAI,GAAG,KAAK,AAAIyC,EAAKzC,OAAO,MAAG,EAAS,SAASA,KAAK,KAAK,MAAMsC,EAAS,SAAStC,KAAKF,CAAI,IAAIA;EAEpH;EACA,IAAM4C,IAAS,KAAK,YAAY,KAAK,UAAUJ,GAAUjB,CAAM,IAAIiB;EACnE,IAAII,MAAW,MAAM;EACrB,IAAMC,IAAmCD,KAAUJ;EACnD,EAAO,SAAS,KAAKK,EAAO,QAAQ;EAGpC,IAAMC,IAAS,IAAI1D,EAAK,CAAC,CAAC,iBAAiB;GAAC;GAAG;GAAG;EAAC,GAAGyD,EAAO,WAAWN,EAAK,QAAQ;EAGrF,AAFAhB,EAAO,WAAW,KAAKuB,EAAO,SAASP,EAAK,UAAU,CAAC,GACvD,KAAKjC,GAAQ,GACb,KAAK,WAAWiB,GAAQ,MAAM;CAChC;AACF;AASA,SAASe,EAAM,GAAG;CAChB,IAAM,CAACS,GAAGC,GAAGC,GAAGC,KAAKC;CAErB,OAAO,KAAK,MAAM,MAAMJ,IAAIE,IAAID,IAAIE,IAAI,IAAI,KAAKF,IAAIA,IAAIC,IAAIA,EAAE;AACjE;AAGA,SAASlD,EAAK,GAAM,GAAG;CAErB,OADI,OAAOE,KAAM,aAAWnB,EAAK,mBAAmB,IAAIgC,EAAK,+BAA+B9B,EAASiB,CAAC,GAAG,GAClGA;AACT;;AC/OA,IAAamD,IAAkB,CAAC,SAAS,UAAU,GAC7CC,IAAQ;CAAE,KAAK;CAAe,MAAM;CAAe,UAAU;AAAiB,GAYvEC,IAAb,MAAqB;CAEnB,QAAQ;CAER,WAAW;CACmB,KAAQ,CAAC;CACT,KAAU,CAAC;CAGzC,YAAY,IAAU,CAAC,GAAG;EAMxB,AALAvE,EAAa,WAAWc,GAASuD,GAAiBC,CAAK,GACnDxD,EAAQ,UAAU,KAAA,OAChB,CAAC,OAAO,UAAUA,EAAQ,KAAK,KAAKA,EAAQ,QAAQ,MAAGf,EAAK,WAAW,iDAAiDE,EAASa,EAAQ,KAAK,GAAG,GACrJ,KAAK,QAAQA,EAAQ,QAEnBA,EAAQ,YAAY,SAClB,OAAOA,EAAQ,YAAa,cAAYf,EAAK,WAAW,sCAAsCE,EAASa,EAAQ,QAAQ,GAAG,GAC9H,KAAK,WAAWA,EAAQ;CAE5B;CAOA,KAAK,GAAO;EAQV,OAPK0D,KAAS,OAAOA,EAAM,QAAS,cAAc,OAAOA,EAAM,QAAS,cACtEzE,EAAK,WAAW,wEAAwEE,EAASuE,CAAK,GAAG,GAE3G,KAAKC,GAAM,KAAKD,CAAK,GACjB,KAAKC,GAAM,SAAS,KAAK,SAAO,KAAKA,GAAM,MAAM,GACrD,KAAKC,KAAU,CAAC,GAChB,KAAK,WAAW,IAAI,GACb;CACT;CAGA,OAAO;EACL,IAAMF,IAAQ,KAAKC,GAAM,IAAI;EAK7B,OAJKD,KACLA,EAAM,KAAK,GACX,KAAKE,GAAQ,KAAKF,CAAK,GACvB,KAAK,WAAW,IAAI,GACb,MAJY;CAKrB;CAGA,OAAO;EACL,IAAMA,IAAQ,KAAKE,GAAQ,IAAI;EAK/B,OAJKF,KACLA,EAAM,KAAK,GACX,KAAKC,GAAM,KAAKD,CAAK,GACrB,KAAK,WAAW,IAAI,GACb,MAJY;CAKrB;CAEA,IAAI,UAAU;EAAE,OAAO,KAAKC,GAAM,SAAS;CAAE;CAC7C,IAAI,UAAU;EAAE,OAAO,KAAKC,GAAQ,SAAS;CAAE;CAE/C,IAAI,YAAY;EAAE,OAAO,KAAKD,GAAM;CAAO;CAE3C,IAAI,YAAY;EAAE,OAAO,KAAKC,GAAQ;CAAO;CAE7C,IAAI,YAAY;EAAE,OAAO,KAAKD,GAAM,GAAG,EAAE,CAAC,EAAE,SAAS;CAAK;CAE1D,IAAI,YAAY;EAAE,OAAO,KAAKC,GAAQ,GAAG,EAAE,CAAC,EAAE,SAAS;CAAK;CAG5D,QAAQ;EAGN,AAFA,KAAKD,KAAQ,CAAC,GACd,KAAKC,KAAU,CAAC,GAChB,KAAK,WAAW,IAAI;CACtB;CASA,UAAU,GAAO,GAAO;EACtB,IAAMC,IAAO,CAACC,CAAK,CAAC,CAAC,KAAK,GACpBC,IAASF,EAAK,IAAIG,CAAO;EAC/B,QAAQ,IAAaC,MAAU;GAC7B,IAAMC,IAAQL,EAAK,IAAIG,CAAO;GAG9B,OAFA,CAAIE,EAAM,OAAO,GAAG,MAAM3B,EAAE,OAAO,GAAG,MAAMnC,MAAM2D,EAAOI,EAAE,CAAC9D,EAAE,CAAC,MAC/D,KAAK,KAAK;IAAE,OAAO+D;IAAY,YAAYP,EAAK,SAAS,GAAG,MAAMQ,EAAMC,GAAGP,EAAOI,EAAE,CAAC;IAAG,YAAYN,EAAK,SAAS,GAAG,MAAMQ,EAAMC,GAAGJ,EAAMC,EAAE,CAAC;GAAE,CAAC,GACzI;EACT;CACF;AACF,GAGMH,KAAW,MAAM;CAAC,GAAGM,EAAE;CAAU,GAAGA,EAAE;CAAY,GAAGA,EAAE;AAAK;AAGlE,SAASD,EAAM,GAAG,GAAG;CAGnB,AAFAC,EAAE,SAAS,IAAI/B,EAAE,IAAIA,EAAE,IAAIA,EAAE,EAAE,GAC/B+B,EAAE,WAAW,IAAI/B,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,EAAE,GACvC+B,EAAE,MAAM,IAAI/B,EAAE,IAAIA,EAAE,IAAIA,EAAE,EAAE;AAC9B"}
package/dist/explain.js CHANGED
@@ -1,4 +1,4 @@
1
- import { St as e } from "./core.js";
1
+ import { Et as e } from "./core.js";
2
2
  // src/explain.js
3
3
  var t = (e) => Math.abs(e) >= 1e4 || e !== 0 && Math.abs(e) < .001 ? e.toExponential(2) : String(+e.toFixed(2)), n = (e) => `[${t(e[0])}, ${t(e[1])}, ${t(e[2])}]`, r = (e, t) => `${e} ${t}${e === 1 ? "" : "s"}`;
4
4
  function i(t) {
@@ -35,21 +35,40 @@ function a(e) {
35
35
  let v = d.filter((e) => !e.material.unlit && e.node.drawMode === "triangles");
36
36
  v.length && !a.lights.length && a.environment === "none" && !e.autoLight && u.push(`${r(v.length, "lit object")} but no lights and autoLight is off - they draw black; add a light, set scene.environment = 'studio' or use unlit materials`);
37
37
  let y = d.filter((e) => e.material.opacity === 0).length;
38
- if (y && u.push(`${r(y, "object")} with material opacity 0 - drawn but invisible`), e.labels && e.labels.total) {
38
+ y && u.push(`${r(y, "object")} with material opacity 0 - drawn but invisible`);
39
+ let b = d.filter((e) => e.material.shader);
40
+ if (b.length) {
41
+ let e = (e) => [
42
+ "vertex",
43
+ "fragment",
44
+ "lit"
45
+ ].filter((t) => e[t]).join(", ");
46
+ l.push(`Shader hooks: ${b.map((t) => {
47
+ let n = t.material.shader, r = t.node, i = [.../\buTime\b/.test(n.vertex + n.fragment + n.lit) ? ["uTime"] : [], ...Object.keys(n.uniforms)];
48
+ return `${r.constructor.name}${r.name ? ` '${r.name}'` : ""} (${e(n)}${i.length ? `; ${i.join(", ")}` : ""})`;
49
+ }).join(", ")}`);
50
+ let t = b.filter((e) => e.material.shader?.vertex && e.material.shader.bounds === null).length;
51
+ t && l.push(` ${r(t, "object")} moved by a vertex hook without shader.bounds - not frustum culled, and picked on the GPU`);
52
+ }
53
+ if (e.labels && e.labels.total) {
39
54
  let t = e.labels;
40
55
  l.push(`Labels: ${t.shown} shown, ${t.dimmed ? `${t.dimmed} dimmed (covered by labels or geometry), ` : ""}${t.overlapped} hidden by overlap, ${t.occluded} hidden behind geometry, ${t.offscreen} off screen${t.zoomedOut ? `, ${t.zoomedOut} waiting to be zoomed in to (minScale)` : ""}`), t.overlapped > t.shown * 3 && u.push(`${t.overlapped} labels are hidden by overlap - give the important ones a higher priority, label fewer things, or use overlap: 'dim' or 'show'`), t.offscreen === t.total && u.push("every label is off screen - check their positions, or call view.frame()");
41
56
  }
42
- let b = e.interaction;
43
- return (b.hover || b.click || b.tooltip || b.highlight) && l.push(`Interaction: ${[
44
- b.hover && r(b.hover, "hover handler"),
45
- b.click && r(b.click, "click handler"),
46
- b.tooltip && "tooltip",
47
- b.highlight && "hover highlight"
48
- ].filter(Boolean).join(", ")}`), (b.keyboard || b.reducedMotion || b.description) && l.push(`Accessibility: ${[
49
- b.keyboard && "keyboard (arrow keys step through what can be hovered)",
50
- b.reducedMotion && "reduced motion",
51
- b.description && `described as "${b.description}"`
52
- ].filter(Boolean).join(", ")}`), (e.width < 2 || e.height < 2) && u.unshift(`the canvas is ${e.width} x ${e.height} - give it a size in the page (CSS width and height)`), e.contextLost && u.unshift("the WebGL context is lost - nothing draws until the browser restores it"), o.isEmpty() ? u.unshift("the scene has nothing to draw - add a Mesh, InstancedMesh, Points or Lines") : !a.stats.drawn && a.stats.culled && u.unshift(`nothing is in view: ${r(a.stats.culled, "object")} are outside the camera's view - call view.frame() to fit the camera to the scene`), l.push(u.length ? `Warnings:\n${u.map((e) => `- ${e}`).join("\n")}` : "No warnings."), l.join("\n");
57
+ let x = e.interaction;
58
+ if ((x.hover || x.click || x.tooltip || x.highlight) && l.push(`Interaction: ${[
59
+ x.hover && r(x.hover, "hover handler"),
60
+ x.click && r(x.click, "click handler"),
61
+ x.tooltip && "tooltip",
62
+ x.highlight && "hover highlight"
63
+ ].filter(Boolean).join(", ")}`), (x.keyboard || x.reducedMotion || x.description) && l.push(`Accessibility: ${[
64
+ x.keyboard && "keyboard (arrow keys step through what can be hovered)",
65
+ x.reducedMotion && "reduced motion",
66
+ x.description && `described as "${x.description}"`
67
+ ].filter(Boolean).join(", ")}`), (e.width < 2 || e.height < 2) && u.unshift(`the canvas is ${e.width} x ${e.height} - give it a size in the page (CSS width and height)`), e.contextLost && u.unshift("the WebGL context is lost - nothing draws until the browser restores it"), o.isEmpty() ? u.unshift("the scene has nothing to draw - add a Mesh, InstancedMesh, Points or Lines") : !a.stats.drawn && a.stats.culled && u.unshift(`nothing is in view: ${r(a.stats.culled, "object")} are outside the camera's view - call view.frame() to fit the camera to the scene`), e.debug) {
68
+ let t = Object.entries(e.debug).filter(([, e]) => e).map(([e, t]) => t === !0 ? e : `${e} (${t})`);
69
+ l.push(`Debug drawing: ${t.join(", ") || "nothing"} - helpers aren't part of the scene, picked or counted above`);
70
+ }
71
+ return l.push(u.length ? `Warnings:\n${u.map((e) => `- ${e}`).join("\n")}` : "No warnings."), l.join("\n");
53
72
  }
54
73
  export { a as explain };
55
74
 
@@ -1 +1 @@
1
- {"version":3,"file":"explain.js","names":["Box3","fmt","n","vec","v","plural","word","solidBounds","box","items","node","explain","info","cam","frame","ortho","lines","warnings","kinds","kind","instances","k","drawn","note","bounds","centre","radius","distance","fills","viewHeight","fill","pct","f","solid","lightKinds","l","env","envText","receivers","i","lit","invisible","s","ix","w"],"sources":["../src/explain.js"],"sourcesContent":["// view.explain(): what the View is drawing, in plain text, with warnings for the usual reasons a\n// picture is wrong. A coding agent can't see the canvas; this lets it debug without screenshots.\n\nimport { Box3 } from './value/Box3.js'\n\n/** @import { FrameDescription } from './frame.js' */\n/** @import { LabelStats } from './labels.js' */\n\n/**\n * @typedef {object} ExplainInfo\n * @property {number} width CSS pixels\n * @property {number} height\n * @property {number} pixelRatio\n * @property {string} renderMode\n * @property {boolean} running\n * @property {FrameDescription} frame\n * @property {LabelStats | null} labels\n * @property {Box3} bounds the world bounds of what the camera draws of the scene\n * @property {{ width: number, height: number } | null} [fills] the share of the view's width and height\n * that drawn box covers on screen; null when part of it is behind the camera\n * @property {number} nodes\n * @property {boolean} autoLight\n * @property {boolean} contextLost\n * @property {boolean} [sharedContext] drawn with the page's shared WebGL context\n * @property {{ hover: number, click: number, tooltip: boolean, highlight: boolean, keyboard?: boolean, reducedMotion?: boolean, description?: string | null }} interaction\n * @property {string[]} [notes] lines from nodes that describe themselves, such as charts\n */\n\nconst fmt = (n) => (Math.abs(n) >= 1e4 || (n !== 0 && Math.abs(n) < 1e-3) ? n.toExponential(2) : String(+n.toFixed(2)))\nconst vec = (v) => `[${fmt(v[0])}, ${fmt(v[1])}, ${fmt(v[2])}]`\nconst plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`\n\n/** World bounds of the drawn meshes, or null when there are none. @param {import('./frame.js').DrawItem[]} items */\nfunction solidBounds(items) {\n let box = null\n for (const { node } of items) {\n if (/** @type {any} */ (node).drawMode !== 'triangles') continue\n box = node.expandBounds(box ?? new Box3())\n }\n return box\n}\n\n/** @param {ExplainInfo} info @returns {string} */\nexport function explain(info) {\n const { frame, bounds } = info\n const cam = /** @type {any} */ (frame.camera.camera)\n const ortho = cam.constructor.type === 'orthographic-camera'\n const lines = []\n const warnings = []\n\n lines.push(`View ${info.width} x ${info.height} CSS px at pixel ratio ${fmt(info.pixelRatio)}; ` +\n `draws ${info.renderMode === 'auto' ? 'when something changes' : info.renderMode === 'always' ? 'every frame' : 'only on view.render()'}; ` +\n `loop ${info.running ? 'running' : 'stopped'}${info.sharedContext ? '; shared WebGL context' : ''}`)\n lines.push(`Camera: ${ortho ? `orthographic, ${fmt(cam.height)} units high` : `perspective, fov ${fmt(cam.fov)}`}, ` +\n `at ${vec(cam.position)}${cam.target ? ` looking at ${vec(cam.target)}` : ''}, near ${fmt(cam.near)}, far ${fmt(cam.far)}`)\n\n // what was drawn, by kind\n const items = [...frame.opaque, ...frame.transparent]\n const kinds = new Map()\n for (const { node, instances } of items) {\n const kind = instances ? 'instanced mesh' : /** @type {any} */ (node).drawMode === 'points' ? 'points' : /** @type {any} */ (node).drawMode === 'lines' ? 'lines' : 'mesh'\n const k = kinds.get(kind) ?? { n: 0, items: 0 }\n k.n++\n k.items += instances ? instances.count : kind === 'points' ? node.geometry.drawnVertices : 0\n kinds.set(kind, k)\n }\n const drawn = [...kinds].map(([kind, k]) => `${k.n} ${kind}${k.items ? ` (${k.items.toLocaleString('en')} ${kind === 'points' ? 'points' : 'instances'})` : ''}`)\n lines.push(`Scene: ${plural(info.nodes, 'node')}; drew ${frame.stats.drawn}${drawn.length ? ` - ${drawn.join(', ')}` : ''}; ` +\n `${frame.stats.culled} outside the view`)\n for (const note of info.notes ?? []) lines.push(note)\n\n // how much of the view the scene fills\n if (!bounds.isEmpty()) {\n const centre = bounds.getCenter(), radius = bounds.getRadius()\n const distance = Math.hypot(cam.position[0] - centre[0], cam.position[1] - centre[1], cam.position[2] - centre[2])\n // the drawn box as it lands on screen; a sphere's guess when part of it is behind the camera\n const fills = info.fills\n const viewHeight = ortho ? cam.height : 2 * distance * Math.tan(cam.fov * Math.PI / 360)\n const fill = fills ? Math.max(fills.width, fills.height) : (2 * radius) / viewHeight\n const pct = (f) => `${Math.round(f * 100)}%`\n lines.push(`Bounds: ${vec(bounds.min)} to ${vec(bounds.max)}, centre ${vec(centre)}, radius ${fmt(radius)}; ` +\n (fills ? `on screen it spans ${pct(fills.width)} of the view's width and ${pct(fills.height)} of its height`\n : `about ${pct(fill)} of the view's height (part of it is behind the camera)`))\n if (frame.stats.drawn && fill < 0.05) warnings.push(`the scene fills only ${Math.round(fill * 100)}% of the view - call view.frame() to fit the camera to it`)\n // only solid meshes count here: starfields and floor grids are meant to surround the camera\n const solid = solidBounds(items)\n if (!ortho && solid && solid.containsPoint(cam.position)) warnings.push(\"the camera is inside the solid objects' bounds - call view.frame() to step back\")\n if (distance - radius > cam.far) warnings.push(`the scene is beyond camera.far (${fmt(cam.far)}) - raise camera.far or call view.frame()`)\n else if (distance + radius > cam.far) warnings.push(`the far side of the scene is beyond camera.far (${fmt(cam.far)}) and is cut off`)\n }\n\n // lights\n const lightKinds = new Map()\n for (const l of frame.lights) lightKinds.set(l.kind, (lightKinds.get(l.kind) ?? 0) + 1)\n const env = frame.environment\n const envText = typeof env === 'string' ? (env !== 'none' ? `; environment '${env}'` : '')\n : `; environment ${env.url ? `'${env.url}'` : 'image'} (${!env.image ? 'loading' : env.isHDR ? 'HDR' : 'not HDR - ignored; use a .hdr file'})`\n lines.push(`Lights: ${lightKinds.size ? [...lightKinds].map(([k, n]) => `${n} ${k}`).join(', ') : 'none'}${envText}`)\n // shadows: one directional light casts; things opt in to casting and receiving\n const receivers = items.filter((i) => /** @type {any} */ (i.node).receiveShadow).length\n if (frame.shadow) {\n lines.push(`Shadows: ${plural(frame.shadow.casters.length, 'caster')}, ${plural(receivers, 'receiver')} in view, ` +\n `${fmt(frame.shadow.texelSize)} units per shadow texel`)\n if (!receivers) warnings.push('shadows are cast but nothing in view has receiveShadow: true - set it on the ground and other surfaces')\n } else if (frame.lights.some((l) => l.castShadow) && !frame.spotShadows.length && !frame.pointShadow) {\n warnings.push('a light has castShadow but nothing casts - set castShadow: true on the meshes too')\n }\n const lit = items.filter((i) => !i.material.unlit && /** @type {any} */ (i.node).drawMode === 'triangles')\n if (lit.length && !frame.lights.length && frame.environment === 'none' && !info.autoLight) {\n warnings.push(`${plural(lit.length, 'lit object')} but no lights and autoLight is off - they draw black; add a light, set scene.environment = 'studio' or use unlit materials`)\n }\n\n const invisible = items.filter((i) => i.material.opacity === 0).length\n if (invisible) warnings.push(`${plural(invisible, 'object')} with material opacity 0 - drawn but invisible`)\n\n if (info.labels && info.labels.total) {\n const s = info.labels\n lines.push(`Labels: ${s.shown} shown, ${s.dimmed ? `${s.dimmed} dimmed (covered by labels or geometry), ` : ''}${s.overlapped} hidden by overlap, ${s.occluded} hidden behind geometry, ${s.offscreen} off screen${s.zoomedOut ? `, ${s.zoomedOut} waiting to be zoomed in to (minScale)` : ''}`)\n if (s.overlapped > s.shown * 3) warnings.push(`${s.overlapped} labels are hidden by overlap - give the important ones a higher priority, label fewer things, or use overlap: 'dim' or 'show'`)\n if (s.offscreen === s.total) warnings.push('every label is off screen - check their positions, or call view.frame()')\n }\n\n const ix = info.interaction\n if (ix.hover || ix.click || ix.tooltip || ix.highlight) {\n lines.push(`Interaction: ${[ix.hover && plural(ix.hover, 'hover handler'), ix.click && plural(ix.click, 'click handler'),\n ix.tooltip && 'tooltip', ix.highlight && 'hover highlight'].filter(Boolean).join(', ')}`)\n }\n if (ix.keyboard || ix.reducedMotion || ix.description) {\n lines.push(`Accessibility: ${[ix.keyboard && 'keyboard (arrow keys step through what can be hovered)', ix.reducedMotion && 'reduced motion',\n ix.description && `described as \"${ix.description}\"`].filter(Boolean).join(', ')}`)\n }\n\n if (info.width < 2 || info.height < 2) warnings.unshift(`the canvas is ${info.width} x ${info.height} - give it a size in the page (CSS width and height)`)\n if (info.contextLost) warnings.unshift('the WebGL context is lost - nothing draws until the browser restores it')\n if (bounds.isEmpty()) warnings.unshift('the scene has nothing to draw - add a Mesh, InstancedMesh, Points or Lines')\n else if (!frame.stats.drawn && frame.stats.culled) {\n warnings.unshift(`nothing is in view: ${plural(frame.stats.culled, 'object')} are outside the camera's view - call view.frame() to fit the camera to the scene`)\n }\n\n lines.push(warnings.length ? `Warnings:\\n${warnings.map((w) => `- ${w}`).join('\\n')}` : 'No warnings.')\n return lines.join('\\n')\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"explain.js","names":["Box3","fmt","n","vec","v","plural","word","solidBounds","box","items","node","explain","info","cam","frame","ortho","lines","warnings","kinds","kind","instances","k","drawn","note","bounds","centre","radius","distance","fills","viewHeight","fill","pct","f","solid","lightKinds","l","env","envText","receivers","i","lit","invisible","hooked","parts","s","p","reads","unculled","ix","shown","w"],"sources":["../src/explain.js"],"sourcesContent":["// view.explain(): what the View is drawing, in plain text, with warnings for the usual reasons a\n// picture is wrong. A coding agent can't see the canvas; this lets it debug without screenshots.\n\nimport { Box3 } from './value/Box3.js'\n\n/** @import { FrameDescription } from './frame.js' */\n/** @import { LabelStats } from './labels.js' */\n\n/**\n * @typedef {object} ExplainInfo\n * @property {number} width CSS pixels\n * @property {number} height\n * @property {number} pixelRatio\n * @property {string} renderMode\n * @property {boolean} running\n * @property {FrameDescription} frame\n * @property {LabelStats | null} labels\n * @property {Box3} bounds the world bounds of what the camera draws of the scene\n * @property {{ width: number, height: number } | null} [fills] the share of the view's width and height\n * that drawn box covers on screen; null when part of it is behind the camera\n * @property {number} nodes\n * @property {boolean} autoLight\n * @property {boolean} contextLost\n * @property {boolean} [sharedContext] drawn with the page's shared WebGL context\n * @property {{ hover: number, click: number, tooltip: boolean, highlight: boolean, keyboard?: boolean, reducedMotion?: boolean, description?: string | null }} interaction\n * @property {string[]} [notes] lines from nodes that describe themselves, such as charts\n * @property {Record<string, unknown> | null} [debug] what debug drawing shows, when it is on\n */\n\nconst fmt = (n) => (Math.abs(n) >= 1e4 || (n !== 0 && Math.abs(n) < 1e-3) ? n.toExponential(2) : String(+n.toFixed(2)))\nconst vec = (v) => `[${fmt(v[0])}, ${fmt(v[1])}, ${fmt(v[2])}]`\nconst plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`\n\n/** World bounds of the drawn meshes, or null when there are none. @param {import('./frame.js').DrawItem[]} items */\nfunction solidBounds(items) {\n let box = null\n for (const { node } of items) {\n if (/** @type {any} */ (node).drawMode !== 'triangles') continue\n box = node.expandBounds(box ?? new Box3())\n }\n return box\n}\n\n/** @param {ExplainInfo} info @returns {string} */\nexport function explain(info) {\n const { frame, bounds } = info\n const cam = /** @type {any} */ (frame.camera.camera)\n const ortho = cam.constructor.type === 'orthographic-camera'\n const lines = []\n const warnings = []\n\n lines.push(`View ${info.width} x ${info.height} CSS px at pixel ratio ${fmt(info.pixelRatio)}; ` +\n `draws ${info.renderMode === 'auto' ? 'when something changes' : info.renderMode === 'always' ? 'every frame' : 'only on view.render()'}; ` +\n `loop ${info.running ? 'running' : 'stopped'}${info.sharedContext ? '; shared WebGL context' : ''}`)\n lines.push(`Camera: ${ortho ? `orthographic, ${fmt(cam.height)} units high` : `perspective, fov ${fmt(cam.fov)}`}, ` +\n `at ${vec(cam.position)}${cam.target ? ` looking at ${vec(cam.target)}` : ''}, near ${fmt(cam.near)}, far ${fmt(cam.far)}`)\n\n // what was drawn, by kind\n const items = [...frame.opaque, ...frame.transparent]\n const kinds = new Map()\n for (const { node, instances } of items) {\n const kind = instances ? 'instanced mesh' : /** @type {any} */ (node).drawMode === 'points' ? 'points' : /** @type {any} */ (node).drawMode === 'lines' ? 'lines' : 'mesh'\n const k = kinds.get(kind) ?? { n: 0, items: 0 }\n k.n++\n k.items += instances ? instances.count : kind === 'points' ? node.geometry.drawnVertices : 0\n kinds.set(kind, k)\n }\n const drawn = [...kinds].map(([kind, k]) => `${k.n} ${kind}${k.items ? ` (${k.items.toLocaleString('en')} ${kind === 'points' ? 'points' : 'instances'})` : ''}`)\n lines.push(`Scene: ${plural(info.nodes, 'node')}; drew ${frame.stats.drawn}${drawn.length ? ` - ${drawn.join(', ')}` : ''}; ` +\n `${frame.stats.culled} outside the view`)\n for (const note of info.notes ?? []) lines.push(note)\n\n // how much of the view the scene fills\n if (!bounds.isEmpty()) {\n const centre = bounds.getCenter(), radius = bounds.getRadius()\n const distance = Math.hypot(cam.position[0] - centre[0], cam.position[1] - centre[1], cam.position[2] - centre[2])\n // the drawn box as it lands on screen; a sphere's guess when part of it is behind the camera\n const fills = info.fills\n const viewHeight = ortho ? cam.height : 2 * distance * Math.tan(cam.fov * Math.PI / 360)\n const fill = fills ? Math.max(fills.width, fills.height) : (2 * radius) / viewHeight\n const pct = (f) => `${Math.round(f * 100)}%`\n lines.push(`Bounds: ${vec(bounds.min)} to ${vec(bounds.max)}, centre ${vec(centre)}, radius ${fmt(radius)}; ` +\n (fills ? `on screen it spans ${pct(fills.width)} of the view's width and ${pct(fills.height)} of its height`\n : `about ${pct(fill)} of the view's height (part of it is behind the camera)`))\n if (frame.stats.drawn && fill < 0.05) warnings.push(`the scene fills only ${Math.round(fill * 100)}% of the view - call view.frame() to fit the camera to it`)\n // only solid meshes count here: starfields and floor grids are meant to surround the camera\n const solid = solidBounds(items)\n if (!ortho && solid && solid.containsPoint(cam.position)) warnings.push(\"the camera is inside the solid objects' bounds - call view.frame() to step back\")\n if (distance - radius > cam.far) warnings.push(`the scene is beyond camera.far (${fmt(cam.far)}) - raise camera.far or call view.frame()`)\n else if (distance + radius > cam.far) warnings.push(`the far side of the scene is beyond camera.far (${fmt(cam.far)}) and is cut off`)\n }\n\n // lights\n const lightKinds = new Map()\n for (const l of frame.lights) lightKinds.set(l.kind, (lightKinds.get(l.kind) ?? 0) + 1)\n const env = frame.environment\n const envText = typeof env === 'string' ? (env !== 'none' ? `; environment '${env}'` : '')\n : `; environment ${env.url ? `'${env.url}'` : 'image'} (${!env.image ? 'loading' : env.isHDR ? 'HDR' : 'not HDR - ignored; use a .hdr file'})`\n lines.push(`Lights: ${lightKinds.size ? [...lightKinds].map(([k, n]) => `${n} ${k}`).join(', ') : 'none'}${envText}`)\n // shadows: one directional light casts; things opt in to casting and receiving\n const receivers = items.filter((i) => /** @type {any} */ (i.node).receiveShadow).length\n if (frame.shadow) {\n lines.push(`Shadows: ${plural(frame.shadow.casters.length, 'caster')}, ${plural(receivers, 'receiver')} in view, ` +\n `${fmt(frame.shadow.texelSize)} units per shadow texel`)\n if (!receivers) warnings.push('shadows are cast but nothing in view has receiveShadow: true - set it on the ground and other surfaces')\n } else if (frame.lights.some((l) => l.castShadow) && !frame.spotShadows.length && !frame.pointShadow) {\n warnings.push('a light has castShadow but nothing casts - set castShadow: true on the meshes too')\n }\n const lit = items.filter((i) => !i.material.unlit && /** @type {any} */ (i.node).drawMode === 'triangles')\n if (lit.length && !frame.lights.length && frame.environment === 'none' && !info.autoLight) {\n warnings.push(`${plural(lit.length, 'lit object')} but no lights and autoLight is off - they draw black; add a light, set scene.environment = 'studio' or use unlit materials`)\n }\n\n const invisible = items.filter((i) => i.material.opacity === 0).length\n if (invisible) warnings.push(`${plural(invisible, 'object')} with material opacity 0 - drawn but invisible`)\n\n // shader hooks: which drawn things have GLSL of their own, and what they read\n const hooked = items.filter((i) => i.material.shader)\n if (hooked.length) {\n const parts = (/** @type {any} */ s) => ['vertex', 'fragment', 'lit'].filter((p) => s[p]).join(', ')\n lines.push(`Shader hooks: ${hooked.map((i) => {\n const s = /** @type {any} */ (i.material.shader), n = /** @type {any} */ (i.node)\n const reads = [...(/\\buTime\\b/.test(s.vertex + s.fragment + s.lit) ? ['uTime'] : []), ...Object.keys(s.uniforms)]\n return `${n.constructor.name}${n.name ? ` '${n.name}'` : ''} (${parts(s)}${reads.length ? `; ${reads.join(', ')}` : ''})`\n }).join(', ')}`)\n const unculled = hooked.filter((i) => i.material.shader?.vertex && i.material.shader.bounds === null).length\n if (unculled) lines.push(` ${plural(unculled, 'object')} moved by a vertex hook without shader.bounds - not frustum culled, and picked on the GPU`)\n }\n\n if (info.labels && info.labels.total) {\n const s = info.labels\n lines.push(`Labels: ${s.shown} shown, ${s.dimmed ? `${s.dimmed} dimmed (covered by labels or geometry), ` : ''}${s.overlapped} hidden by overlap, ${s.occluded} hidden behind geometry, ${s.offscreen} off screen${s.zoomedOut ? `, ${s.zoomedOut} waiting to be zoomed in to (minScale)` : ''}`)\n if (s.overlapped > s.shown * 3) warnings.push(`${s.overlapped} labels are hidden by overlap - give the important ones a higher priority, label fewer things, or use overlap: 'dim' or 'show'`)\n if (s.offscreen === s.total) warnings.push('every label is off screen - check their positions, or call view.frame()')\n }\n\n const ix = info.interaction\n if (ix.hover || ix.click || ix.tooltip || ix.highlight) {\n lines.push(`Interaction: ${[ix.hover && plural(ix.hover, 'hover handler'), ix.click && plural(ix.click, 'click handler'),\n ix.tooltip && 'tooltip', ix.highlight && 'hover highlight'].filter(Boolean).join(', ')}`)\n }\n if (ix.keyboard || ix.reducedMotion || ix.description) {\n lines.push(`Accessibility: ${[ix.keyboard && 'keyboard (arrow keys step through what can be hovered)', ix.reducedMotion && 'reduced motion',\n ix.description && `described as \"${ix.description}\"`].filter(Boolean).join(', ')}`)\n }\n\n if (info.width < 2 || info.height < 2) warnings.unshift(`the canvas is ${info.width} x ${info.height} - give it a size in the page (CSS width and height)`)\n if (info.contextLost) warnings.unshift('the WebGL context is lost - nothing draws until the browser restores it')\n if (bounds.isEmpty()) warnings.unshift('the scene has nothing to draw - add a Mesh, InstancedMesh, Points or Lines')\n else if (!frame.stats.drawn && frame.stats.culled) {\n warnings.unshift(`nothing is in view: ${plural(frame.stats.culled, 'object')} are outside the camera's view - call view.frame() to fit the camera to the scene`)\n }\n\n // debug drawing on: said, so a picture full of lines isn't a mystery\n if (info.debug) {\n const shown = Object.entries(info.debug).filter(([, v]) => v).map(([k, v]) => (v === true ? k : `${k} (${v})`))\n lines.push(`Debug drawing: ${shown.join(', ') || 'nothing'} - helpers aren't part of the scene, picked or counted above`)\n }\n lines.push(warnings.length ? `Warnings:\\n${warnings.map((w) => `- ${w}`).join('\\n')}` : 'No warnings.')\n return lines.join('\\n')\n}\n"],"mappings":"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"}