@harpern/edu-modelhub-viewer-core 0.1.0-beta.4 → 0.1.0-beta.6

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/CHANGELOG.md CHANGED
@@ -8,6 +8,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.1.0-beta.6] - 2026-10-05
12
+
13
+ ### Added
14
+
15
+ - `ai_generated` as a `model.source` value (spec 0.3.0). `ModelSource`
16
+ includes it, and the extractor compiled into this package accepts it, so
17
+ loading a model whose source is `ai_generated` no longer produces a
18
+ validation error. `0.1.0-beta.5` reports that value as invalid.
19
+
20
+ ## [0.1.0-beta.5] - 2026-09-23
21
+
22
+ ### Added
23
+
24
+ - Marker size, so every viewer offers the same control over how big
25
+ annotation markers are. `MARKER_SIZE` is the range (0.5 to 2 times, step
26
+ 0.1, default 1); `clampMarkerSize` snaps and bounds a value, including the
27
+ strings a range input reports; `markerSizeText` gives the control's
28
+ `aria-valuetext` ("1.5 times"). `applyMarkerSpriteSize` resizes scene
29
+ sprites and keeps the selected marker larger at every size, and
30
+ `projectedMarkerSize` sizes HTML markers from `projectMarkers`' perspective
31
+ scale. The package still renders nothing: hosts draw the control.
32
+ - `MARKER_SPRITE_SCALE`, `MARKER_SPRITE_SCALE_ACTIVE`,
33
+ `MARKER_HTML_DIAMETER_REM` and `MARKER_HTML_FONT_REM`, the size-1 values
34
+ those helpers default to. `createMarkerSprite` and `createMarkerSprites`
35
+ now default to `MARKER_SPRITE_SCALE`, the same 0.2 as before.
36
+
11
37
  ## [0.1.0-beta.4] - 2026-09-14
12
38
 
13
39
  ### Fixed
package/README.md CHANGED
@@ -79,6 +79,31 @@ announced order cannot disagree. Use sprites in the scene, or project anchors
79
79
  to screen positions and draw real HTML buttons, which is the accessible
80
80
  option because a sprite cannot be focused.
81
81
 
82
+ **Marker size.** One range for the person viewing the model to adjust,
83
+ `MARKER_SIZE` (0.5 to 2 times, in steps of 0.1, starting at 1), and one rule
84
+ for applying it to either kind of marker: `applyMarkerSpriteSize` for sprites,
85
+ `projectedMarkerSize` for HTML markers. Draw the control yourself, usually a
86
+ native range input, and give it `markerSizeText` as its `aria-valuetext`.
87
+
88
+ ```js
89
+ import {
90
+ MARKER_SIZE,
91
+ applyMarkerSpriteSize,
92
+ clampMarkerSize,
93
+ markerSizeText
94
+ } from "@osuecampus/edu-modelhub-viewer-core";
95
+
96
+ input.min = MARKER_SIZE.min;
97
+ input.max = MARKER_SIZE.max;
98
+ input.step = MARKER_SIZE.step;
99
+ input.value = MARKER_SIZE.default;
100
+ input.addEventListener("input", () => {
101
+ const size = clampMarkerSize(input.value);
102
+ input.setAttribute("aria-valuetext", markerSizeText(size));
103
+ applyMarkerSpriteSize(sprites, size, { activeId: selectedAnnotationId });
104
+ });
105
+ ```
106
+
82
107
  **Camera.** `computeTourPlacement` prefers an authored camera and falls back
83
108
  to a computed standoff. `flyCamera` eases, or cuts straight there when the
84
109
  viewer prefers reduced motion.
package/dist/index.cjs CHANGED
@@ -1,16 +1,16 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("three"),_=new Set(["__proto__","constructor","prototype"]);function U(e,t){const n=new WeakSet,r=[{value:e,path:""}],o=(i,s)=>{r.push({value:i,path:s})};for(;r.length>0;){const{value:i,path:s}=r.pop();if(!(typeof i!="object"||i===null||n.has(i)))if(n.add(i),Array.isArray(i))for(let c=0;c<i.length;c++)o(i[c],`${s}[${c}]`);else t(i,s,o)}}function ie(e){const t=[];return U(e,(n,r,o)=>{for(const i of Object.keys(n)){const s=r===""?i:`${r}.${i}`;_.has(i)?t.push({path:s,key:i}):o(n[i],s)}}),t}function se(e){return U(e,(t,n,r)=>{for(const o of Object.keys(t))_.has(o)?delete t[o]:r(t[o],n)}),e}const E=1179937895,L=2,ae=1313821514,ce=21;class y extends Error{constructor(t){super(t),this.name="GlbParseError"}}function ue(e){return e instanceof ArrayBuffer?new DataView(e):new DataView(e.buffer,e.byteOffset,e.byteLength)}function le(e){return e===null?"null":Array.isArray(e)?"an array":typeof e}function de(e){const t=ue(e);if(t.byteLength<ce)throw new y(`Input too short to be a GLB file (${t.byteLength} bytes).`);const n=t.getUint32(0,!0);if(n!==E)throw new y(`Invalid GLB magic: expected 0x${E.toString(16)}, got 0x${n.toString(16)}.`);const r=t.getUint32(4,!0);if(r!==L)throw new y(`Unsupported GLB version: expected ${L}, got ${r}.`);const o=t.getUint32(8,!0);if(o>t.byteLength)throw new y(`GLB header reports ${o} bytes but only ${t.byteLength} bytes were supplied.`);if(t.byteLength<20)throw new y("GLB file truncated before first chunk header.");const i=t.getUint32(12,!0),s=t.getUint32(16,!0);if(s!==ae)throw new y(`First GLB chunk is not JSON (type 0x${s.toString(16)}).`);if(20+i>o)throw new y(`JSON chunk claims ${i} bytes but the declared GLB length is only ${o} bytes.`);if(20+i>t.byteLength)throw new y(`JSON chunk claims ${i} bytes but only ${t.byteLength-20} bytes remain in the buffer.`);const c=new Uint8Array(t.buffer,t.byteOffset+20,i),l=new TextDecoder("utf-8").decode(c);let a;try{a=JSON.parse(l)}catch(u){throw new y(`Failed to parse GLB JSON chunk: ${u.message}`)}if(a===null||typeof a!="object"||Array.isArray(a))throw new y(`GLB JSON chunk root must be an object, got ${le(a)}.`);return a}function fe(e){const t=de(e).extensions;if(t===null||typeof t!="object"||Array.isArray(t))return null;const n=t.EDU_modelhub;return n===void 0?null:se(n)}const me=/^[a-z][a-z0-9_]*$/,he=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,pe=/^[a-z]{2}$/,ge=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;function d(e,t){return{field:e,message:t,severity:"error"}}function $(e,t){return{field:e,message:t,severity:"warning"}}function x(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function w(e){return typeof e=="string"&&e.trim()!==""}function F(e){return typeof e=="number"&&Number.isInteger(e)&&e>=0}function h(e){if(e===void 0)return"undefined";let t;if(typeof e=="string")t=`"${e}"`;else try{const n=JSON.stringify(e);if(n===void 0)return typeof e;t=n}catch{return Array.isArray(e)?"an array":typeof e}return t.length>60?`${t.slice(0,57)}...`:t}function G(e){const t=[],n=e;if(!x(n))return[d("",`Payload must be a JSON object, got ${h(n)}.`)];ye(n,t),be(n.specVersion,t),$e(n.coordinateSpace,t),we(n.model,t);const r=ve(n.regions,t);return Me(n.annotations,r,t),t}function ye(e,t){for(const n of ie(e))t.push(d(n.path,`Disallowed key "${n.key}" at ${n.path} (prototype-pollution vector).`))}function be(e,t){(typeof e!="string"||!ge.test(e))&&t.push(d("specVersion",`specVersion must be a semver string (e.g. "0.1.0"), got ${h(e)}.`))}function $e(e,t){e!==void 0&&e!=="gltf"&&e!=="blender"&&t.push(d("coordinateSpace",`coordinateSpace must be "gltf" or "blender" when present, got ${h(e)}.`))}function we(e,t){if(!x(e)){t.push(d("model",`model is required and must be an object, got ${h(e)}.`));return}const n=e,r=n.title;w(r)||t.push(d("model.title",`model.title is required and must be a non-empty string, got ${h(r)}.`));const o=n.altText;w(o)?o.length>300&&t.push(d("model.altText",`model.altText exceeds 300 characters (${o.length} chars).`)):t.push(d("model.altText",`model.altText is required and must be a non-empty string, got ${h(o)}.`));const i=n.language;i!==void 0&&(typeof i!="string"||!pe.test(i))&&t.push($("model.language",`model.language should be an ISO 639-1 two-letter code (e.g. "en"), got ${h(i)}.`)),xe(n,t),Ae(n.source,t),Se(n,t),w(n.longDescription)||t.push($("model.longDescription","model.longDescription is recommended."))}function xe(e,t){const n=["CC0","CC BY","CC BY-SA","All Rights Reserved"],r=e.license;r!==void 0&&(typeof r!="string"||!n.includes(r))&&t.push(d("model.license",`model.license must be one of ${n.map(o=>`"${o}"`).join(", ")}, got ${h(r)}.`)),(r==="CC BY"||r==="CC BY-SA")&&!w(e.attribution)&&t.push($("model.attribution",'model.attribution is recommended when license is "CC BY" or "CC BY-SA".'))}function Ae(e,t){if(e===void 0)return;const n=["scan","photogrammetry","manual","other"];if(typeof e!="string"||!n.includes(e)){const r=n.map(o=>`"${o}"`).join(", ");t.push(d("model.source",`model.source must be one of ${r}, got ${h(e)}.`))}}function Se(e,t){for(const n of["created","modified"]){const r=e[n];r!==void 0&&(typeof r!="string"||!he.test(r))&&t.push($(`model.${n}`,`model.${n} should be ISO 8601 UTC (YYYY-MM-DDTHH:MM:SSZ), got ${h(r)}.`))}}function ve(e,t){const n=new Set;if(!Array.isArray(e)||e.length===0)return t.push(d("regions","At least one region is required.")),n;for(let r=0;r<e.length;r++){const o=e[r],i=`regions[${r}]`;if(!x(o)){t.push(d(i,`${i} must be an object, got ${h(o)}.`));continue}Ie(o,i,n,t)}return n}function Ie(e,t,n,r){const o=e.id;typeof o!="string"||!me.test(o)?r.push(d(`${t}.id`,`${t}.id must be a non-empty snake_case string (lowercase letters, digits, underscores; must start with a letter), got ${h(o)}.`)):n.has(o)?r.push(d(`${t}.id`,`Duplicate region id "${o}" at ${t}.`)):n.add(o);const i=e.visibilityDefault;i!==void 0&&i!=="shown"&&i!=="hidden"&&r.push(d(`${t}.visibilityDefault`,`${t}.visibilityDefault must be "shown" or "hidden", got ${h(i)}.`));const s=e.order;s!==void 0&&!F(s)&&r.push($(`${t}.order`,`${t}.order should be a non-negative integer, got ${h(s)}.`))}function Me(e,t,n){if(!Array.isArray(e)){n.push(d("annotations","annotations must be an array."));return}e.length<3&&n.push($("annotations",`Only ${e.length} annotation(s) defined (3–8 recommended).`)),e.length>8&&n.push($("annotations",`${e.length} annotations defined (3–8 recommended).`));const r={seenRegionIds:t,seenAnnotationIds:new Set,seenReadingOrders:new Set};for(let o=0;o<e.length;o++){const i=e[o],s=`annotations[${o}]`;if(!x(i)){n.push(d(s,`${s} must be an object, got ${h(i)}.`));continue}Oe(i,s,r,n)}Be(r.seenReadingOrders,n)}function Oe(e,t,n,r){const o=e.id;w(o)?n.seenAnnotationIds.has(o)?r.push(d(`${t}.id`,`Duplicate annotation id "${o}" at ${t}.`)):n.seenAnnotationIds.add(o):r.push(d(`${t}.id`,`${t}.id is required and must be a non-empty string, got ${h(o)}.`));const i=e.regionId;i!==void 0&&i!==""&&(typeof i!="string"||!n.seenRegionIds.has(i))&&r.push(d(`${t}.regionId`,`${t}.regionId ${h(i)} does not match any region id.`)),Te(e.readingOrder,t,n,r),Ne(e.anchor,t,r),Re(e.camera,t,r)}function Te(e,t,n,r){e!==void 0&&(F(e)?n.seenReadingOrders.has(e)?r.push(d(`${t}.readingOrder`,`Duplicate readingOrder value ${e} at ${t}.`)):n.seenReadingOrders.add(e):r.push(d(`${t}.readingOrder`,`${t}.readingOrder must be a non-negative integer, got ${h(e)}.`)))}function Ne(e,t,n){if(!x(e)){n.push(d(`${t}.anchor`,`${t}.anchor is required and must be an object, got ${h(e)}.`));return}const r=e,o=r.type;if(o==="marker"){const i=r.position;(!Array.isArray(i)||i.length!==3||!i.every(s=>typeof s=="number"))&&n.push(d(`${t}.anchor.position`,`${t}.anchor.position must be an array of exactly 3 numbers for type "marker".`))}else o==="node"||o==="primitive"||o==="aabb"?w(r.ref)||n.push(d(`${t}.anchor.ref`,`${t}.anchor.ref is required for anchor type "${o}".`)):n.push(d(`${t}.anchor.type`,`${t}.anchor.type must be "marker", "node", "primitive", or "aabb", got ${h(o)}.`))}function Re(e,t,n){if(e===void 0)return;if(!x(e)){n.push(d(`${t}.camera`,`${t}.camera must be an object when present, got ${h(e)}.`));return}for(const o of["eye","target"]){const i=e[o];(!Array.isArray(i)||i.length!==3||!i.every(s=>typeof s=="number"&&Number.isFinite(s)))&&n.push(d(`${t}.camera.${o}`,`${t}.camera.${o} must be an array of exactly 3 finite numbers.`))}const r=e.fov;r!==void 0&&(typeof r!="number"||!(r>0))&&n.push(d(`${t}.camera.fov`,`${t}.camera.fov must be a number greater than 0 when present, got ${h(r)}.`))}function Be(e,t){if(e.size===0)return;const n=[...e].sort((r,o)=>r-o);for(let r=0;r<n.length;r++)if(n[r]!==r){t.push($("annotations[*].readingOrder",`readingOrder values are not sequential from 0. Expected 0–${n.length-1} but found gaps or wrong start.`));break}}class k extends Error{constructor(t){super(t),this.name="AdaptError"}}class Ce{adapt(t){var n,r;const o=t.model,i=((n=o==null?void 0:o.title)==null?void 0:n.trim())??"",s=((r=o==null?void 0:o.altText)==null?void 0:r.trim())??"";if(!i)throw new k("Sidecar is missing required field: model.title");if(!s)throw new k("Sidecar is missing required field: model.altText");return t}}const Ee=new Ce;function Le(e){return Ee.adapt(e)}function V(e){return e.filter(t=>t.severity==="error")}function P(e){const[t,n=""]=e.split("?",2);if(!/\.glb$/i.test(t))return null;const r=t.replace(/\.glb$/i,".modelhub.json");return n?`${r}?${n}`:r}function ke(e,t){var i;if(!e||e.startsWith("blob:")||e.startsWith("data:"))return"";const n=t??((i=globalThis.location)==null?void 0:i.href),r=n?new URL(e,n).href:e,o=r.lastIndexOf("/");return o===-1?"":r.slice(0,o+1)}async function z(e,t=fetch){const n=await t(e);if(!n.ok)throw new Error(`Failed to fetch ${e} (HTTP ${n.status}).`);return n.arrayBuffer()}function j(e){const t=fe(e);if(!t)return{payload:null,source:"none",findings:[],problem:"This model carries no EDU_modelhub metadata."};const n=G(t);return{payload:t,source:"embedded",findings:n,problem:null}}async function De(e,t={}){const n=t.fetchImpl??fetch,r=await z(e,n),o=j(r);if(o.payload)return{...o,bytes:r};if(t.sidecar===!1)return{...o,bytes:r};const s=(t.sidecarUrl??P)(e);if(!s)return{...o,bytes:r};try{const c=await n(s);if(c.ok){const l=Le(await c.json());return{payload:l,source:"sidecar",findings:G(l),problem:null,bytes:r}}}catch{}return{payload:null,source:"none",findings:[],problem:"This model has no EDU_modelhub metadata, either embedded in the file or in a sidecar beside it.",bytes:r}}function _e(e,t="EDU_modelhub",n=console){if(!(e!=null&&e.length))return;const r=V(e),o=e.filter(i=>i.severity!=="error");r.length&&n.warn(`${t}: ${r.length} validation error(s)`,r),o.length&&n.info(`${t}: ${o.length} validation warning(s)`,o)}function Ue(e){return(e==null?void 0:e.name)==="GlbParseError"}function T(e){return Array.isArray(e)&&e.length===3&&e.every(t=>typeof t=="number"&&Number.isFinite(t))}function N(e){return e==="blender"}function A(e,t){if(!T(e))return null;const[n,r,o]=e;return N(t)?[n,o,-r]:[n,r,o]}function Fe(e,t){if(!T(e))return null;const[n,r,o]=e;return N(t)?[n,-o,r]:[n,r,o]}function W(e){if(!Array.isArray(e))return[];const t=new Set;return e.map((n,r)=>{const i=(typeof(n==null?void 0:n.id)=="string"?n.id.trim():"")||`annotation-${r+1}`;let s=i,c=2;for(;t.has(s);)s=`${i}-${c++}`;return t.add(s),{...n??{},id:s}})}function I(e){const t=W(e==null?void 0:e.annotations),n=r=>Number.isFinite(r.readingOrder)?r.readingOrder:Number.MAX_SAFE_INTEGER;return t.map((r,o)=>({annotation:r,index:o})).sort((r,o)=>n(r.annotation)-n(o.annotation)||r.index-o.index).map(r=>r.annotation)}function R(e){return(Array.isArray(e==null?void 0:e.regions)?e.regions:[]).map((n,r)=>({region:n,index:r})).sort((n,r)=>(n.region.order??Number.MAX_SAFE_INTEGER)-(r.region.order??Number.MAX_SAFE_INTEGER)||n.index-r.index).map(n=>n.region)}const Ge="Auto-created from object ";function q(e){var n;const t=(n=e==null?void 0:e.description)==null?void 0:n.trim();return t?!t.startsWith(Ge):!1}function Ve(e){return Array.isArray(e)?e.filter(t=>{var n;return q(t)||!((n=t.description)!=null&&n.trim())}):[]}function M(e){var t;return e?((t=e.label)==null?void 0:t.trim())||e.id||"Unnamed region":"Unknown region"}function Pe(e){if(!e)return[];const t=[],n=/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;let r=0,o;for(;(o=n.exec(e))!==null;)o.index>r&&t.push({text:e.slice(r,o.index)}),t.push({text:o[1],url:o[2]}),r=o.index+o[0].length;return r<e.length&&t.push({text:e.slice(r)}),t.filter(i=>i.text.trim()||i.url)}function ze(e){const t=e==null?void 0:e.coordinateSpace,n=R(e),r=I(e),o=new Map(n.map(a=>[a.id,a])),i=new Map(r.map(a=>[a.id,a])),s=new Map;for(const a of r){const u=a.regionId;if(!u)continue;const f=s.get(u);f?f.push(a):s.set(u,[a])}const c=[];for(const a of r){const u=a.anchor;if(!u||u.type!=="marker")continue;const f=A(u.position,t);if(!f)continue;const m=a.camera,g=m?A(m.eye,t):null,b=m?A(m.target,t):null;c.push({annotation:a,position:f,...g&&b?{camera:{eye:g,target:b,...m!=null&&m.fov?{fov:m.fov}:{}}}:{}})}const l=new Set(n.filter(a=>a.visibilityDefault==="hidden").map(a=>a.id));return{payload:e,regions:n,regionsById:o,annotations:r,annotationsById:i,annotationsByRegion:s,markerAnchors:c,hiddenRegionIds:l}}function je(e){return e.isMesh===!0}function We(e,t,n=[]){var l;const r=new Map,o=new Map,i=new Map,s=new Map;for(const[a,u]of((t==null?void 0:t.meshes)??[]).entries())for(const f of u.primitives??[]){const m=(l=f.extras)==null?void 0:l.modelhubRegionId;typeof m=="string"&&m&&(s.set(a,m),u.name&&o.set(u.name,m))}for(const a of(t==null?void 0:t.nodes)??[]){if(a.mesh===void 0||!a.name)continue;const u=s.get(a.mesh);u&&i.set(a.name,u)}const c=new Set(n.map(a=>a.id));return e.traverse(a=>{var g,b;if(!je(a))return;const u=(b=(g=a.geometry)==null?void 0:g.userData)==null?void 0:b.modelhubRegionId,f=typeof u=="string"&&u||i.get(a.name)||o.get(a.name)||(c.has(a.name)?a.name:void 0);if(!f)return;const m=r.get(f);m?m.push(a):r.set(f,[a]),a.userData.modelhubOriginalMaterial===void 0&&(a.userData.modelhubOriginalMaterial=a.material)}),r}function qe(e){const t={};for(const[n,r]of e)t[n]=r.length;return t}function H(e,t,n){for(const r of e.get(t)??[])r.visible=n}function He(e,t){for(const n of e.keys())H(e,n,!t.has(n))}function Ye(e,t,n){for(const r of e.get(t)??[])r.material=n}function Ke(e,t){for(const n of e.get(t)??[]){const r=n.userData.modelhubOriginalMaterial;r&&(n.material=r)}}const Y="#2196f3",Ze="#ff5722";function B(e,t={}){const{fill:n=Y,text:r="#ffffff",textStroke:o="rgba(0, 0, 0, 0.45)"}=t,i=256,s=document.createElement("canvas");s.width=i,s.height=i;const c=s.getContext("2d");return c&&(c.beginPath(),c.arc(i/2,i/2,i/2-4,0,Math.PI*2),c.fillStyle=n,c.fill(),c.strokeStyle="#ffffff",c.lineWidth=8,c.stroke(),c.fillStyle=r,c.strokeStyle=o,c.lineWidth=10,c.font="bold 132px Arial, sans-serif",c.textAlign="center",c.textBaseline="middle",c.strokeText(String(e),i/2,i/2),c.fillText(String(e),i/2,i/2)),s}function K(e,t=.2,n){const r=new p.SpriteMaterial({map:new p.CanvasTexture(B(e,n)),transparent:!0,depthTest:!1}),o=new p.Sprite(r);return o.scale.set(t,t,1),o.renderOrder=999,o}function Xe(e,t,n){var o;const r=e.material;(o=r.map)==null||o.dispose(),r.map=new p.CanvasTexture(B(t,{fill:n})),r.needsUpdate=!0}function Qe(e,t,n=.2,r){return t.map((o,i)=>{const s=K(i+1,n,r);return s.position.set(...o.position),s.name=`modelhub_marker_${o.annotation.id}`,s.userData.annotationId=o.annotation.id,s.userData.displayNumber=i+1,e.add(s),s})}function Je(e){const{camera:t,model:n,anchors:r,width:o,height:i,isOccluded:s}=e,c=e.referenceDistance??1,l=new p.Vector3,a={};n.updateMatrixWorld();for(const u of r){l.set(...u.position).applyMatrix4(n.matrixWorld);const f=l.distanceTo(t.position),m=s?s(l,f):!1,g=l.clone().project(t);a[u.annotation.id]={x:(g.x*.5+.5)*o,y:(-g.y*.5+.5)*i,scale:Math.min(1.6,Math.max(.55,c/Math.max(f,.001))),zIndex:Math.max(1,Math.round((1-g.z)*1e5))+(m?0:2e5),inView:g.z<1&&g.x>-1.05&&g.x<1.05&&g.y>-1.05&&g.y<1.05,occluded:m}}return a}const Z=.03,et={enabled:!1,isOccluded:()=>!1,dispose:()=>{}};async function tt(e){var i;let t=[],n=[];if(e.traverse(s=>{if(!s.isMesh)return;const c=s.geometry;c!=null&&c.computeBoundsTree&&(t.push(c),n.push(s))}),!t.length)return et;for(const s of t)s.boundsTree||(i=s.computeBoundsTree)==null||i.call(s);const r=new p.Raycaster;r.firstHitOnly=!0;const o=new p.Vector3;return{enabled:!0,isOccluded(s,c,l){return o.subVectors(c,s.position).normalize(),r.set(s.position,o),r.near=0,r.far=Math.max(0,l-Z),r.camera=s,r.intersectObjects(n,!1).length>0},dispose(){var s;for(const c of t)(s=c.disposeBoundsTree)==null||s.call(c);t=[],n=[]}}}const v={headlight:{ambient:.7,lights:[{color:[1,1,1],intensity:.9,position:[4,8,6]},{color:[1,1,1],intensity:2.4,position:[0,0,0],attach:!0,spot:!0}]},studio:{ambient:.85,lights:[{color:[.78,.8,1],intensity:1.45,position:[1.35,6.75,-7.26]},{color:[1,.88,.71],intensity:1.5,position:[-5.71,3.5,7.43]},{color:[.42,.74,1],intensity:.25,position:[7.23,6.78,1.29]}]},"dual-cam":{ambient:.6,lights:[{color:[.6,.48,.3],intensity:1.6,position:[-1,0,1],attach:!0},{color:[.42,.42,.6],intensity:1.6,position:[1,0,1],attach:!0}]},"studio-cam":{ambient:.7,lights:[{color:[.78,.8,1],intensity:1.35,position:[-1,.3,1],attach:!0},{color:[1,.88,.71],intensity:1.4,position:[1,.3,1],attach:!0},{color:[.42,.74,1],intensity:.25,position:[7.23,6.78,1.29]}]}},X="headlight";function nt(){return Object.keys(v)}function rt(e){var t;(t=e.parent)==null||t.remove(e)}function ot(e){var c;const{scene:t,camera:n,exposure:r=1}=e;(c=e.previous)==null||c.remove();const o=v[e.preset??""]??v[X],i=[],s=new p.AmbientLight(16777215,o.ambient*r);t.add(s),i.push(s);for(const l of o.lights){let a;if(l.spot){const f=new p.SpotLight(new p.Color(...l.color));f.angle=.5,f.penumbra=.9,f.decay=0,a=f}else a=new p.DirectionalLight(new p.Color(...l.color));a.intensity=l.intensity,a.position.set(...l.position);const u=a.target;l.attach?(u==null||u.position.set(0,0,l.spot?-1:0),n.add(a),u&&n.add(u)):(t.add(a),u&&t.add(u)),i.push(a),u&&i.push(u)}return{lights:i,remove(){for(const l of i)rt(l);i.length=0}}}const S=[{id:"white",label:"White",hex:"#ffffff",light:!0},{id:"light-grey",label:"Light gray",hex:"#d4d4d4",light:!0},{id:"medium-grey",label:"Medium gray",hex:"#808080",light:!1},{id:"dark-grey",label:"Dark gray",hex:"#252525",light:!1},{id:"black",label:"Black",hex:"#000000",light:!1}],Q="dark-grey";function J(e){if(typeof e!="string")return"";const t=e.trim().replace(/^#/,"");return/^[0-9a-f]{6}$/i.test(t)?`#${t.toLowerCase()}`:/^[0-9a-f]{3}$/i.test(t)?`#${[...t].map(n=>n+n).join("").toLowerCase()}`:""}function ee(e){const t=Number.parseInt(e.replace("#",""),16);return Number.isFinite(t)?(.299*(t>>16&255)+.587*(t>>8&255)+.114*(t&255))/255:0}function it(e,t){const n=J(t);return n?{id:"custom",label:"Custom",hex:n,light:ee(n)>.5}:S.find(r=>r.id===e)??S.find(r=>r.id===Q)??S[0]}const te=2;function O(e){e.updateMatrixWorld(!0);const t=new p.Box3,n=new p.Box3;return e.traverse(r=>{const o=r,i=r;o.isSkinnedMesh?(o.computeBoundingBox(),o.boundingBox&&(n.copy(o.boundingBox).applyMatrix4(o.matrixWorld),t.union(n))):i.isMesh&&i.geometry&&(i.geometry.boundingBox||i.geometry.computeBoundingBox(),i.geometry.boundingBox&&(n.copy(i.geometry.boundingBox).applyMatrix4(i.matrixWorld),t.union(n)))}),t.isEmpty()?new p.Box3().setFromObject(e):t}function st(e,t=te){const r=O(e).getSize(new p.Vector3),o=Math.max(r.x,r.y,r.z)||1,i=t/o;e.scale.multiplyScalar(i);const s=O(e);return e.position.sub(s.getCenter(new p.Vector3)),i}function at(e,t=1.3){return t/Math.tan(e*Math.PI/360)}const ne=.5,re=1.5;function ct(e,t,n={}){t.updateMatrixWorld();const r=new p.Vector3(...e.position).applyMatrix4(t.matrixWorld);if(e.camera){const l=new p.Vector3(...e.camera.eye).applyMatrix4(t.matrixWorld),u=new p.Vector3(...e.camera.target).applyMatrix4(t.matrixWorld).clone().sub(l);u.lengthSq()<1e-10&&u.copy(r).sub(l),u.normalize();const f=Math.max(r.distanceTo(l),.2);return{position:l,target:l.clone().addScaledVector(u,f),...e.camera.fov?{fov:e.camera.fov}:{}}}const o=n.outward??!0,i=new p.Vector3(r.x,0,r.z);i.lengthSq()<1e-8&&i.set(0,0,1),i.normalize();const s=o?-(n.outwardDistance??ne):n.inwardDistance??re,c=r.clone().addScaledVector(i,s);return c.y=r.y,{position:c,target:r}}const ut={cancel:()=>{}};function lt(e){const{camera:t,controls:n,to:r,onFrame:o,onDone:i}=e,s=()=>{o==null||o(),i==null||i()};if(e.reducedMotion)return t.position.copy(r.position),n.target.copy(r.target),n.update(),s(),ut;const c=e.durationMs??900,l=t.position.clone(),a=n.target.clone(),u=performance.now();let f=null;const m=g=>{const b=Math.min((g-u)/c,1),C=b*(2-b);t.position.lerpVectors(l,r.position,C),n.target.lerpVectors(a,r.target,C),n.update(),o==null||o(),b<1?f=requestAnimationFrame(m):(f=null,i==null||i())};return f=requestAnimationFrame(m),{cancel(){f!==null&&cancelAnimationFrame(f),f=null}}}function dt(e,t,n){const r=t||n;e.fov!==r&&(e.fov=r,e.updateProjectionMatrix())}const oe=500;function ft(e){var o,i,s;if(!e)return"Model loaded, but it carries no educational metadata.";const t=[];(o=e.model)!=null&&o.title&&t.push(`${e.model.title} loaded`),(i=e.model)!=null&&i.altText&&t.push(e.model.altText);const n=((s=e.regions)==null?void 0:s.length)??0,r=I(e).length;return n&&t.push(`${n} labeled ${n===1?"region":"regions"}`),r&&t.push(`${r} ${r===1?"annotation":"annotations"}`),t.map(c=>c.replace(/\.$/,"")).join(". ")+"."}function mt(e,t,n){return`Annotation ${t+1} of ${n}: ${e||"Untitled"}.`}function ht(e,t=oe,n=()=>Date.now()){let r=-1/0,o=null,i=null;const s=a=>{r=n(),o=null,e(a)};return{announce:a=>{const u=n()-r;if(u>=t){s(a);return}o=a,i||(i=setTimeout(()=>{i=null,o!==null&&s(o)},t-u))},flush:()=>{i&&(clearTimeout(i),i=null),o!==null&&s(o)}}}function D(e,t){return`## ${e}
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("three"),V=new Set(["__proto__","constructor","prototype"]);function z(e,t){const n=new WeakSet,r=[{value:e,path:""}],o=(i,s)=>{r.push({value:i,path:s})};for(;r.length>0;){const{value:i,path:s}=r.pop();if(!(typeof i!="object"||i===null||n.has(i)))if(n.add(i),Array.isArray(i))for(let c=0;c<i.length;c++)o(i[c],`${s}[${c}]`);else t(i,s,o)}}function de(e){const t=[];return z(e,(n,r,o)=>{for(const i of Object.keys(n)){const s=r===""?i:`${r}.${i}`;V.has(i)?t.push({path:s,key:i}):o(n[i],s)}}),t}function fe(e){return z(e,(t,n,r)=>{for(const o of Object.keys(t))V.has(o)?delete t[o]:r(t[o],n)}),e}const B=1179937895,D=2,me=1313821514,pe=21;class y extends Error{constructor(t){super(t),this.name="GlbParseError"}}function he(e){return e instanceof ArrayBuffer?new DataView(e):new DataView(e.buffer,e.byteOffset,e.byteLength)}function ge(e){return e===null?"null":Array.isArray(e)?"an array":typeof e}function ye(e){const t=he(e);if(t.byteLength<pe)throw new y(`Input too short to be a GLB file (${t.byteLength} bytes).`);const n=t.getUint32(0,!0);if(n!==B)throw new y(`Invalid GLB magic: expected 0x${B.toString(16)}, got 0x${n.toString(16)}.`);const r=t.getUint32(4,!0);if(r!==D)throw new y(`Unsupported GLB version: expected ${D}, got ${r}.`);const o=t.getUint32(8,!0);if(o>t.byteLength)throw new y(`GLB header reports ${o} bytes but only ${t.byteLength} bytes were supplied.`);if(t.byteLength<20)throw new y("GLB file truncated before first chunk header.");const i=t.getUint32(12,!0),s=t.getUint32(16,!0);if(s!==me)throw new y(`First GLB chunk is not JSON (type 0x${s.toString(16)}).`);if(20+i>o)throw new y(`JSON chunk claims ${i} bytes but the declared GLB length is only ${o} bytes.`);if(20+i>t.byteLength)throw new y(`JSON chunk claims ${i} bytes but only ${t.byteLength-20} bytes remain in the buffer.`);const c=new Uint8Array(t.buffer,t.byteOffset+20,i),l=new TextDecoder("utf-8").decode(c);let a;try{a=JSON.parse(l)}catch(u){throw new y(`Failed to parse GLB JSON chunk: ${u.message}`)}if(a===null||typeof a!="object"||Array.isArray(a))throw new y(`GLB JSON chunk root must be an object, got ${ge(a)}.`);return a}function be(e){const t=ye(e).extensions;if(t===null||typeof t!="object"||Array.isArray(t))return null;const n=t.EDU_modelhub;return n===void 0?null:fe(n)}const $e=/^[a-z][a-z0-9_]*$/,xe=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,Ae=/^[a-z]{2}$/,we=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;function d(e,t){return{field:e,message:t,severity:"error"}}function $(e,t){return{field:e,message:t,severity:"warning"}}function A(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function x(e){return typeof e=="string"&&e.trim()!==""}function P(e){return typeof e=="number"&&Number.isInteger(e)&&e>=0}function p(e){if(e===void 0)return"undefined";let t;if(typeof e=="string")t=`"${e}"`;else try{const n=JSON.stringify(e);if(n===void 0)return typeof e;t=n}catch{return Array.isArray(e)?"an array":typeof e}return t.length>60?`${t.slice(0,57)}...`:t}function G(e){const t=[],n=e;if(!A(n))return[d("",`Payload must be a JSON object, got ${p(n)}.`)];Se(n,t),Me(n.specVersion,t),Ie(n.coordinateSpace,t),Re(n.model,t);const r=Oe(n.regions,t);return Ne(n.annotations,r,t),t}function Se(e,t){for(const n of de(e))t.push(d(n.path,`Disallowed key "${n.key}" at ${n.path} (prototype-pollution vector).`))}function Me(e,t){(typeof e!="string"||!we.test(e))&&t.push(d("specVersion",`specVersion must be a semver string (e.g. "0.1.0"), got ${p(e)}.`))}function Ie(e,t){e!==void 0&&e!=="gltf"&&e!=="blender"&&t.push(d("coordinateSpace",`coordinateSpace must be "gltf" or "blender" when present, got ${p(e)}.`))}function Re(e,t){if(!A(e)){t.push(d("model",`model is required and must be an object, got ${p(e)}.`));return}const n=e,r=n.title;x(r)||t.push(d("model.title",`model.title is required and must be a non-empty string, got ${p(r)}.`));const o=n.altText;x(o)?o.length>300&&t.push(d("model.altText",`model.altText exceeds 300 characters (${o.length} chars).`)):t.push(d("model.altText",`model.altText is required and must be a non-empty string, got ${p(o)}.`));const i=n.language;i!==void 0&&(typeof i!="string"||!Ae.test(i))&&t.push($("model.language",`model.language should be an ISO 639-1 two-letter code (e.g. "en"), got ${p(i)}.`)),Te(n,t),Ee(n.source,t),ve(n,t),x(n.longDescription)||t.push($("model.longDescription","model.longDescription is recommended."))}function Te(e,t){const n=["CC0","CC BY","CC BY-SA","All Rights Reserved"],r=e.license;r!==void 0&&(typeof r!="string"||!n.includes(r))&&t.push(d("model.license",`model.license must be one of ${n.map(o=>`"${o}"`).join(", ")}, got ${p(r)}.`)),(r==="CC BY"||r==="CC BY-SA")&&!x(e.attribution)&&t.push($("model.attribution",'model.attribution is recommended when license is "CC BY" or "CC BY-SA".'))}function Ee(e,t){if(e===void 0)return;const n=["scan","photogrammetry","manual","ai_generated","other"];if(typeof e!="string"||!n.includes(e)){const r=n.map(o=>`"${o}"`).join(", ");t.push(d("model.source",`model.source must be one of ${r}, got ${p(e)}.`))}}function ve(e,t){for(const n of["created","modified"]){const r=e[n];r!==void 0&&(typeof r!="string"||!xe.test(r))&&t.push($(`model.${n}`,`model.${n} should be ISO 8601 UTC (YYYY-MM-DDTHH:MM:SSZ), got ${p(r)}.`))}}function Oe(e,t){const n=new Set;if(!Array.isArray(e)||e.length===0)return t.push(d("regions","At least one region is required.")),n;for(let r=0;r<e.length;r++){const o=e[r],i=`regions[${r}]`;if(!A(o)){t.push(d(i,`${i} must be an object, got ${p(o)}.`));continue}_e(o,i,n,t)}return n}function _e(e,t,n,r){const o=e.id;typeof o!="string"||!$e.test(o)?r.push(d(`${t}.id`,`${t}.id must be a non-empty snake_case string (lowercase letters, digits, underscores; must start with a letter), got ${p(o)}.`)):n.has(o)?r.push(d(`${t}.id`,`Duplicate region id "${o}" at ${t}.`)):n.add(o);const i=e.visibilityDefault;i!==void 0&&i!=="shown"&&i!=="hidden"&&r.push(d(`${t}.visibilityDefault`,`${t}.visibilityDefault must be "shown" or "hidden", got ${p(i)}.`));const s=e.order;s!==void 0&&!P(s)&&r.push($(`${t}.order`,`${t}.order should be a non-negative integer, got ${p(s)}.`))}function Ne(e,t,n){if(!Array.isArray(e)){n.push(d("annotations","annotations must be an array."));return}e.length<3&&n.push($("annotations",`Only ${e.length} annotation(s) defined (3–8 recommended).`)),e.length>8&&n.push($("annotations",`${e.length} annotations defined (3–8 recommended).`));const r={seenRegionIds:t,seenAnnotationIds:new Set,seenReadingOrders:new Set};for(let o=0;o<e.length;o++){const i=e[o],s=`annotations[${o}]`;if(!A(i)){n.push(d(s,`${s} must be an object, got ${p(i)}.`));continue}Ce(i,s,r,n)}De(r.seenReadingOrders,n)}function Ce(e,t,n,r){const o=e.id;x(o)?n.seenAnnotationIds.has(o)?r.push(d(`${t}.id`,`Duplicate annotation id "${o}" at ${t}.`)):n.seenAnnotationIds.add(o):r.push(d(`${t}.id`,`${t}.id is required and must be a non-empty string, got ${p(o)}.`));const i=e.regionId;i!==void 0&&i!==""&&(typeof i!="string"||!n.seenRegionIds.has(i))&&r.push(d(`${t}.regionId`,`${t}.regionId ${p(i)} does not match any region id.`)),Le(e.readingOrder,t,n,r),ke(e.anchor,t,r),Be(e.camera,t,r)}function Le(e,t,n,r){e!==void 0&&(P(e)?n.seenReadingOrders.has(e)?r.push(d(`${t}.readingOrder`,`Duplicate readingOrder value ${e} at ${t}.`)):n.seenReadingOrders.add(e):r.push(d(`${t}.readingOrder`,`${t}.readingOrder must be a non-negative integer, got ${p(e)}.`)))}function ke(e,t,n){if(!A(e)){n.push(d(`${t}.anchor`,`${t}.anchor is required and must be an object, got ${p(e)}.`));return}const r=e,o=r.type;if(o==="marker"){const i=r.position;(!Array.isArray(i)||i.length!==3||!i.every(s=>typeof s=="number"))&&n.push(d(`${t}.anchor.position`,`${t}.anchor.position must be an array of exactly 3 numbers for type "marker".`))}else o==="node"||o==="primitive"||o==="aabb"?x(r.ref)||n.push(d(`${t}.anchor.ref`,`${t}.anchor.ref is required for anchor type "${o}".`)):n.push(d(`${t}.anchor.type`,`${t}.anchor.type must be "marker", "node", "primitive", or "aabb", got ${p(o)}.`))}function Be(e,t,n){if(e===void 0)return;if(!A(e)){n.push(d(`${t}.camera`,`${t}.camera must be an object when present, got ${p(e)}.`));return}for(const o of["eye","target"]){const i=e[o];(!Array.isArray(i)||i.length!==3||!i.every(s=>typeof s=="number"&&Number.isFinite(s)))&&n.push(d(`${t}.camera.${o}`,`${t}.camera.${o} must be an array of exactly 3 finite numbers.`))}const r=e.fov;r!==void 0&&(typeof r!="number"||!(r>0))&&n.push(d(`${t}.camera.fov`,`${t}.camera.fov must be a number greater than 0 when present, got ${p(r)}.`))}function De(e,t){if(e.size===0)return;const n=[...e].sort((r,o)=>r-o);for(let r=0;r<n.length;r++)if(n[r]!==r){t.push($("annotations[*].readingOrder",`readingOrder values are not sequential from 0. Expected 0–${n.length-1} but found gaps or wrong start.`));break}}class U extends Error{constructor(t){super(t),this.name="AdaptError"}}class Ue{adapt(t){var n,r;const o=t.model,i=((n=o==null?void 0:o.title)==null?void 0:n.trim())??"",s=((r=o==null?void 0:o.altText)==null?void 0:r.trim())??"";if(!i)throw new U("Sidecar is missing required field: model.title");if(!s)throw new U("Sidecar is missing required field: model.altText");return t}}const Fe=new Ue;function Ve(e){return Fe.adapt(e)}function j(e){return e.filter(t=>t.severity==="error")}function W(e){const[t,n=""]=e.split("?",2);if(!/\.glb$/i.test(t))return null;const r=t.replace(/\.glb$/i,".modelhub.json");return n?`${r}?${n}`:r}function ze(e,t){var i;if(!e||e.startsWith("blob:")||e.startsWith("data:"))return"";const n=t??((i=globalThis.location)==null?void 0:i.href),r=n?new URL(e,n).href:e,o=r.lastIndexOf("/");return o===-1?"":r.slice(0,o+1)}async function H(e,t=fetch){const n=await t(e);if(!n.ok)throw new Error(`Failed to fetch ${e} (HTTP ${n.status}).`);return n.arrayBuffer()}function K(e){const t=be(e);if(!t)return{payload:null,source:"none",findings:[],problem:"This model carries no EDU_modelhub metadata."};const n=G(t);return{payload:t,source:"embedded",findings:n,problem:null}}async function Pe(e,t={}){const n=t.fetchImpl??fetch,r=await H(e,n),o=K(r);if(o.payload)return{...o,bytes:r};if(t.sidecar===!1)return{...o,bytes:r};const s=(t.sidecarUrl??W)(e);if(!s)return{...o,bytes:r};try{const c=await n(s);if(c.ok){const l=Ve(await c.json());return{payload:l,source:"sidecar",findings:G(l),problem:null,bytes:r}}}catch{}return{payload:null,source:"none",findings:[],problem:"This model has no EDU_modelhub metadata, either embedded in the file or in a sidecar beside it.",bytes:r}}function Ge(e,t="EDU_modelhub",n=console){if(!(e!=null&&e.length))return;const r=j(e),o=e.filter(i=>i.severity!=="error");r.length&&n.warn(`${t}: ${r.length} validation error(s)`,r),o.length&&n.info(`${t}: ${o.length} validation warning(s)`,o)}function je(e){return(e==null?void 0:e.name)==="GlbParseError"}function _(e){return Array.isArray(e)&&e.length===3&&e.every(t=>typeof t=="number"&&Number.isFinite(t))}function N(e){return e==="blender"}function w(e,t){if(!_(e))return null;const[n,r,o]=e;return N(t)?[n,o,-r]:[n,r,o]}function We(e,t){if(!_(e))return null;const[n,r,o]=e;return N(t)?[n,-o,r]:[n,r,o]}function q(e){if(!Array.isArray(e))return[];const t=new Set;return e.map((n,r)=>{const i=(typeof(n==null?void 0:n.id)=="string"?n.id.trim():"")||`annotation-${r+1}`;let s=i,c=2;for(;t.has(s);)s=`${i}-${c++}`;return t.add(s),{...n??{},id:s}})}function I(e){const t=q(e==null?void 0:e.annotations),n=r=>Number.isFinite(r.readingOrder)?r.readingOrder:Number.MAX_SAFE_INTEGER;return t.map((r,o)=>({annotation:r,index:o})).sort((r,o)=>n(r.annotation)-n(o.annotation)||r.index-o.index).map(r=>r.annotation)}function C(e){return(Array.isArray(e==null?void 0:e.regions)?e.regions:[]).map((n,r)=>({region:n,index:r})).sort((n,r)=>(n.region.order??Number.MAX_SAFE_INTEGER)-(r.region.order??Number.MAX_SAFE_INTEGER)||n.index-r.index).map(n=>n.region)}const He="Auto-created from object ";function Y(e){var n;const t=(n=e==null?void 0:e.description)==null?void 0:n.trim();return t?!t.startsWith(He):!1}function Ke(e){return Array.isArray(e)?e.filter(t=>{var n;return Y(t)||!((n=t.description)!=null&&n.trim())}):[]}function E(e){var t;return e?((t=e.label)==null?void 0:t.trim())||e.id||"Unnamed region":"Unknown region"}function qe(e){if(!e)return[];const t=[],n=/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;let r=0,o;for(;(o=n.exec(e))!==null;)o.index>r&&t.push({text:e.slice(r,o.index)}),t.push({text:o[1],url:o[2]}),r=o.index+o[0].length;return r<e.length&&t.push({text:e.slice(r)}),t.filter(i=>i.text.trim()||i.url)}function Ye(e){const t=e==null?void 0:e.coordinateSpace,n=C(e),r=I(e),o=new Map(n.map(a=>[a.id,a])),i=new Map(r.map(a=>[a.id,a])),s=new Map;for(const a of r){const u=a.regionId;if(!u)continue;const f=s.get(u);f?f.push(a):s.set(u,[a])}const c=[];for(const a of r){const u=a.anchor;if(!u||u.type!=="marker")continue;const f=w(u.position,t);if(!f)continue;const m=a.camera,g=m?w(m.eye,t):null,b=m?w(m.target,t):null;c.push({annotation:a,position:f,...g&&b?{camera:{eye:g,target:b,...m!=null&&m.fov?{fov:m.fov}:{}}}:{}})}const l=new Set(n.filter(a=>a.visibilityDefault==="hidden").map(a=>a.id));return{payload:e,regions:n,regionsById:o,annotations:r,annotationsById:i,annotationsByRegion:s,markerAnchors:c,hiddenRegionIds:l}}function Ze(e){return e.isMesh===!0}function Xe(e,t,n=[]){var l;const r=new Map,o=new Map,i=new Map,s=new Map;for(const[a,u]of((t==null?void 0:t.meshes)??[]).entries())for(const f of u.primitives??[]){const m=(l=f.extras)==null?void 0:l.modelhubRegionId;typeof m=="string"&&m&&(s.set(a,m),u.name&&o.set(u.name,m))}for(const a of(t==null?void 0:t.nodes)??[]){if(a.mesh===void 0||!a.name)continue;const u=s.get(a.mesh);u&&i.set(a.name,u)}const c=new Set(n.map(a=>a.id));return e.traverse(a=>{var g,b;if(!Ze(a))return;const u=(b=(g=a.geometry)==null?void 0:g.userData)==null?void 0:b.modelhubRegionId,f=typeof u=="string"&&u||i.get(a.name)||o.get(a.name)||(c.has(a.name)?a.name:void 0);if(!f)return;const m=r.get(f);m?m.push(a):r.set(f,[a]),a.userData.modelhubOriginalMaterial===void 0&&(a.userData.modelhubOriginalMaterial=a.material)}),r}function Qe(e){const t={};for(const[n,r]of e)t[n]=r.length;return t}function Z(e,t,n){for(const r of e.get(t)??[])r.visible=n}function Je(e,t){for(const n of e.keys())Z(e,n,!t.has(n))}function et(e,t,n){for(const r of e.get(t)??[])r.material=n}function tt(e,t){for(const n of e.get(t)??[]){const r=n.userData.modelhubOriginalMaterial;r&&(n.material=r)}}const X="#2196f3",nt="#ff5722";function L(e,t={}){const{fill:n=X,text:r="#ffffff",textStroke:o="rgba(0, 0, 0, 0.45)"}=t,i=256,s=document.createElement("canvas");s.width=i,s.height=i;const c=s.getContext("2d");return c&&(c.beginPath(),c.arc(i/2,i/2,i/2-4,0,Math.PI*2),c.fillStyle=n,c.fill(),c.strokeStyle="#ffffff",c.lineWidth=8,c.stroke(),c.fillStyle=r,c.strokeStyle=o,c.lineWidth=10,c.font="bold 132px Arial, sans-serif",c.textAlign="center",c.textBaseline="middle",c.strokeText(String(e),i/2,i/2),c.fillText(String(e),i/2,i/2)),s}function Q(e,t=R,n){const r=new h.SpriteMaterial({map:new h.CanvasTexture(L(e,n)),transparent:!0,depthTest:!1}),o=new h.Sprite(r);return o.scale.set(t,t,1),o.renderOrder=999,o}function rt(e,t,n){var o;const r=e.material;(o=r.map)==null||o.dispose(),r.map=new h.CanvasTexture(L(t,{fill:n})),r.needsUpdate=!0}function ot(e,t,n=R,r){return t.map((o,i)=>{const s=Q(i+1,n,r);return s.position.set(...o.position),s.name=`modelhub_marker_${o.annotation.id}`,s.userData.annotationId=o.annotation.id,s.userData.displayNumber=i+1,e.add(s),s})}const v=Object.freeze({min:.5,max:2,step:.1,default:1}),R=.2,J=.3;function T(e){let t=Number.NaN;if(typeof e=="number"?t=e:typeof e=="string"&&e.trim()!==""&&(t=Number(e)),!Number.isFinite(t))return v.default;const{min:n,max:r,step:o}=v,i=Number((Math.round(t/o)*o).toFixed(6));return Math.min(r,Math.max(n,i))}function it(e){return`${T(e).toFixed(1)} times`}function st(e,t,n={}){const r=T(t),{base:o=R,activeBase:i=J,activeId:s=null}=n;for(const c of e){const a=(s!==null&&c.userData.annotationId===s?i:o)*r;c.scale.set(a,a,1)}}function at(e){const{camera:t,model:n,anchors:r,width:o,height:i,isOccluded:s}=e,c=e.referenceDistance??1,l=new h.Vector3,a={};n.updateMatrixWorld();for(const u of r){l.set(...u.position).applyMatrix4(n.matrixWorld);const f=l.distanceTo(t.position),m=s?s(l,f):!1,g=l.clone().project(t);a[u.annotation.id]={x:(g.x*.5+.5)*o,y:(-g.y*.5+.5)*i,scale:Math.min(1.6,Math.max(.55,c/Math.max(f,.001))),zIndex:Math.max(1,Math.round((1-g.z)*1e5))+(m?0:2e5),inView:g.z<1&&g.x>-1.05&&g.x<1.05&&g.y>-1.05&&g.y<1.05,occluded:m}}return a}const ee=1.65,te=.8;function ct(e,t,n={}){const r=T(t)*e;return{diameter:(n.diameter??ee)*r,fontSize:(n.fontSize??te)*r}}const ne=.03,ut={enabled:!1,isOccluded:()=>!1,dispose:()=>{}};async function lt(e){var i;let t=[],n=[];if(e.traverse(s=>{if(!s.isMesh)return;const c=s.geometry;c!=null&&c.computeBoundsTree&&(t.push(c),n.push(s))}),!t.length)return ut;for(const s of t)s.boundsTree||(i=s.computeBoundsTree)==null||i.call(s);const r=new h.Raycaster;r.firstHitOnly=!0;const o=new h.Vector3;return{enabled:!0,isOccluded(s,c,l){return o.subVectors(c,s.position).normalize(),r.set(s.position,o),r.near=0,r.far=Math.max(0,l-ne),r.camera=s,r.intersectObjects(n,!1).length>0},dispose(){var s;for(const c of t)(s=c.disposeBoundsTree)==null||s.call(c);t=[],n=[]}}}const M={headlight:{ambient:.7,lights:[{color:[1,1,1],intensity:.9,position:[4,8,6]},{color:[1,1,1],intensity:2.4,position:[0,0,0],attach:!0,spot:!0}]},studio:{ambient:.85,lights:[{color:[.78,.8,1],intensity:1.45,position:[1.35,6.75,-7.26]},{color:[1,.88,.71],intensity:1.5,position:[-5.71,3.5,7.43]},{color:[.42,.74,1],intensity:.25,position:[7.23,6.78,1.29]}]},"dual-cam":{ambient:.6,lights:[{color:[.6,.48,.3],intensity:1.6,position:[-1,0,1],attach:!0},{color:[.42,.42,.6],intensity:1.6,position:[1,0,1],attach:!0}]},"studio-cam":{ambient:.7,lights:[{color:[.78,.8,1],intensity:1.35,position:[-1,.3,1],attach:!0},{color:[1,.88,.71],intensity:1.4,position:[1,.3,1],attach:!0},{color:[.42,.74,1],intensity:.25,position:[7.23,6.78,1.29]}]}},re="headlight";function dt(){return Object.keys(M)}function ft(e){var t;(t=e.parent)==null||t.remove(e)}function mt(e){var c;const{scene:t,camera:n,exposure:r=1}=e;(c=e.previous)==null||c.remove();const o=M[e.preset??""]??M[re],i=[],s=new h.AmbientLight(16777215,o.ambient*r);t.add(s),i.push(s);for(const l of o.lights){let a;if(l.spot){const f=new h.SpotLight(new h.Color(...l.color));f.angle=.5,f.penumbra=.9,f.decay=0,a=f}else a=new h.DirectionalLight(new h.Color(...l.color));a.intensity=l.intensity,a.position.set(...l.position);const u=a.target;l.attach?(u==null||u.position.set(0,0,l.spot?-1:0),n.add(a),u&&n.add(u)):(t.add(a),u&&t.add(u)),i.push(a),u&&i.push(u)}return{lights:i,remove(){for(const l of i)ft(l);i.length=0}}}const S=[{id:"white",label:"White",hex:"#ffffff",light:!0},{id:"light-grey",label:"Light gray",hex:"#d4d4d4",light:!0},{id:"medium-grey",label:"Medium gray",hex:"#808080",light:!1},{id:"dark-grey",label:"Dark gray",hex:"#252525",light:!1},{id:"black",label:"Black",hex:"#000000",light:!1}],oe="dark-grey";function ie(e){if(typeof e!="string")return"";const t=e.trim().replace(/^#/,"");return/^[0-9a-f]{6}$/i.test(t)?`#${t.toLowerCase()}`:/^[0-9a-f]{3}$/i.test(t)?`#${[...t].map(n=>n+n).join("").toLowerCase()}`:""}function se(e){const t=Number.parseInt(e.replace("#",""),16);return Number.isFinite(t)?(.299*(t>>16&255)+.587*(t>>8&255)+.114*(t&255))/255:0}function pt(e,t){const n=ie(t);return n?{id:"custom",label:"Custom",hex:n,light:se(n)>.5}:S.find(r=>r.id===e)??S.find(r=>r.id===oe)??S[0]}const ae=2;function O(e){e.updateMatrixWorld(!0);const t=new h.Box3,n=new h.Box3;return e.traverse(r=>{const o=r,i=r;o.isSkinnedMesh?(o.computeBoundingBox(),o.boundingBox&&(n.copy(o.boundingBox).applyMatrix4(o.matrixWorld),t.union(n))):i.isMesh&&i.geometry&&(i.geometry.boundingBox||i.geometry.computeBoundingBox(),i.geometry.boundingBox&&(n.copy(i.geometry.boundingBox).applyMatrix4(i.matrixWorld),t.union(n)))}),t.isEmpty()?new h.Box3().setFromObject(e):t}function ht(e,t=ae){const r=O(e).getSize(new h.Vector3),o=Math.max(r.x,r.y,r.z)||1,i=t/o;e.scale.multiplyScalar(i);const s=O(e);return e.position.sub(s.getCenter(new h.Vector3)),i}function gt(e,t=1.3){return t/Math.tan(e*Math.PI/360)}const ce=.5,ue=1.5;function yt(e,t,n={}){t.updateMatrixWorld();const r=new h.Vector3(...e.position).applyMatrix4(t.matrixWorld);if(e.camera){const l=new h.Vector3(...e.camera.eye).applyMatrix4(t.matrixWorld),u=new h.Vector3(...e.camera.target).applyMatrix4(t.matrixWorld).clone().sub(l);u.lengthSq()<1e-10&&u.copy(r).sub(l),u.normalize();const f=Math.max(r.distanceTo(l),.2);return{position:l,target:l.clone().addScaledVector(u,f),...e.camera.fov?{fov:e.camera.fov}:{}}}const o=n.outward??!0,i=new h.Vector3(r.x,0,r.z);i.lengthSq()<1e-8&&i.set(0,0,1),i.normalize();const s=o?-(n.outwardDistance??ce):n.inwardDistance??ue,c=r.clone().addScaledVector(i,s);return c.y=r.y,{position:c,target:r}}const bt={cancel:()=>{}};function $t(e){const{camera:t,controls:n,to:r,onFrame:o,onDone:i}=e,s=()=>{o==null||o(),i==null||i()};if(e.reducedMotion)return t.position.copy(r.position),n.target.copy(r.target),n.update(),s(),bt;const c=e.durationMs??900,l=t.position.clone(),a=n.target.clone(),u=performance.now();let f=null;const m=g=>{const b=Math.min((g-u)/c,1),k=b*(2-b);t.position.lerpVectors(l,r.position,k),n.target.lerpVectors(a,r.target,k),n.update(),o==null||o(),b<1?f=requestAnimationFrame(m):(f=null,i==null||i())};return f=requestAnimationFrame(m),{cancel(){f!==null&&cancelAnimationFrame(f),f=null}}}function xt(e,t,n){const r=t||n;e.fov!==r&&(e.fov=r,e.updateProjectionMatrix())}const le=500;function At(e){var o,i,s;if(!e)return"Model loaded, but it carries no educational metadata.";const t=[];(o=e.model)!=null&&o.title&&t.push(`${e.model.title} loaded`),(i=e.model)!=null&&i.altText&&t.push(e.model.altText);const n=((s=e.regions)==null?void 0:s.length)??0,r=I(e).length;return n&&t.push(`${n} labeled ${n===1?"region":"regions"}`),r&&t.push(`${r} ${r===1?"annotation":"annotations"}`),t.map(c=>c.replace(/\.$/,"")).join(". ")+"."}function wt(e,t,n){return`Annotation ${t+1} of ${n}: ${e||"Untitled"}.`}function St(e,t=le,n=()=>Date.now()){let r=-1/0,o=null,i=null;const s=a=>{r=n(),o=null,e(a)};return{announce:a=>{const u=n()-r;if(u>=t){s(a);return}o=a,i||(i=setTimeout(()=>{i=null,o!==null&&s(o)},t-u))},flush:()=>{i&&(clearTimeout(i),i=null),o!==null&&s(o)}}}function F(e,t){return`## ${e}
2
2
  ${t}
3
3
 
4
- `}function pt(e,t={}){const n=e.model??{},r=R(e),o=I(e);let i=`# ${n.title||"Untitled model"}
4
+ `}function Mt(e,t={}){const n=e.model??{},r=C(e),o=I(e);let i=`# ${n.title||"Untitled model"}
5
5
 
6
- `;if(n.altText&&(i+=D(t.summaryHeading??"Summary",n.altText)),n.longDescription&&(i+=D("Full description",n.longDescription)),r.length){i+=`## Regions (${r.length})
6
+ `;if(n.altText&&(i+=F(t.summaryHeading??"Summary",n.altText)),n.longDescription&&(i+=F("Full description",n.longDescription)),r.length){i+=`## Regions (${r.length})
7
7
 
8
- `;for(const s of r)i+=`### ${M(s)}
8
+ `;for(const s of r)i+=`### ${E(s)}
9
9
  ${s.description||"No description."}
10
10
 
11
11
  `}return o.length&&(i+=`## Annotations (${o.length})
12
12
 
13
- `,o.forEach((s,c)=>{const l=s.regionId?` (${M(r.find(u=>u.id===s.regionId))})`:"",a=`${c+1}. ${s.title||"Untitled"}${l}`;i+=`### ${a}
13
+ `,o.forEach((s,c)=>{const l=s.regionId?` (${E(r.find(u=>u.id===s.regionId))})`:"",a=`${c+1}. ${s.title||"Untitled"}${l}`;i+=`### ${a}
14
14
  ${s.body||"No description."}
15
15
 
16
16
  `})),i+=`## Licensing
@@ -18,4 +18,4 @@ ${s.body||"No description."}
18
18
  `,i+=`- **Attribution:** ${n.attribution||"Not specified"}
19
19
  `,i+=`- **Creator:** ${n.creator||"Not specified"}
20
20
  `,i+=`- **Source:** ${n.source||"Not specified"}
21
- `,i}function gt(e){var n;return`${(((n=e.model)==null?void 0:n.title)||"model").trim().replace(/[^\w\-. ]+/g,"").replace(/\s+/g,"_")||"model"}_transcript.md`}function yt(e){e&&e.traverse(t=>{var i,s;const n=t,r=n.geometry;r&&(r.boundsTree&&r.disposeBoundsTree&&r.disposeBoundsTree(),(i=r.dispose)==null||i.call(r));const o=[].concat(n.material??[]);for(const c of o)if(c){for(const l of Object.values(c)){const a=l;a&&a.isTexture&&a.dispose()}(s=c.dispose)==null||s.call(c)}})}exports.BACKGROUND_OPTIONS=S;exports.CAMERA_ANNOUNCE_INTERVAL=oe;exports.DEFAULT_BACKGROUND=Q;exports.DEFAULT_LIGHTING=X;exports.LIGHTING_PRESETS=v;exports.MARKER_FILL=Y;exports.MARKER_FILL_ACTIVE=Ze;exports.NORMALIZED_SIZE=te;exports.OCCLUSION_BIAS=Z;exports.TOUR_DISTANCE_INWARD=re;exports.TOUR_DISTANCE_OUTWARD=ne;exports.annotationSummary=mt;exports.annotationsInReadingOrder=I;exports.applyFov=dt;exports.applyLighting=ot;exports.applyVisibilityDefaults=He;exports.buildRegionMeshMap=We;exports.buildTranscript=pt;exports.clearRegionHighlight=Ke;exports.computeDisplayBox=O;exports.computeTourPlacement=ct;exports.createAnnouncer=ht;exports.createMarkerSprite=K;exports.createMarkerSprites=Qe;exports.createOcclusionIndex=tt;exports.disposeSceneGraph=yt;exports.distanceForFov=at;exports.drawMarkerCanvas=B;exports.errorsOf=V;exports.fetchBytes=z;exports.flyCamera=lt;exports.frameModel=st;exports.fromViewerPosition=Fe;exports.highlightRegion=Ye;exports.isAuthoredRegion=q;exports.isGlbParseError=Ue;exports.isVec3=T;exports.lightingPresetNames=nt;exports.loadModel=De;exports.logFindings=_e;exports.luminance=ee;exports.modelSummary=ft;exports.needsConversion=N;exports.normalizeAnnotationIds=W;exports.normalizeHexColor=J;exports.normalizeModel=ze;exports.parseBodySegments=Pe;exports.projectMarkers=Je;exports.readEmbedded=j;exports.readableRegions=Ve;exports.regionBindingCounts=qe;exports.regionLabel=M;exports.regionsInOrder=R;exports.resolveBackground=it;exports.resourcePathFor=ke;exports.setMarkerSpriteColor=Xe;exports.setRegionVisible=H;exports.sidecarUrlFor=P;exports.toViewerPosition=A;exports.transcriptFileName=gt;
21
+ `,i}function It(e){var n;return`${(((n=e.model)==null?void 0:n.title)||"model").trim().replace(/[^\w\-. ]+/g,"").replace(/\s+/g,"_")||"model"}_transcript.md`}function Rt(e){e&&e.traverse(t=>{var i,s;const n=t,r=n.geometry;r&&(r.boundsTree&&r.disposeBoundsTree&&r.disposeBoundsTree(),(i=r.dispose)==null||i.call(r));const o=[].concat(n.material??[]);for(const c of o)if(c){for(const l of Object.values(c)){const a=l;a&&a.isTexture&&a.dispose()}(s=c.dispose)==null||s.call(c)}})}exports.BACKGROUND_OPTIONS=S;exports.CAMERA_ANNOUNCE_INTERVAL=le;exports.DEFAULT_BACKGROUND=oe;exports.DEFAULT_LIGHTING=re;exports.LIGHTING_PRESETS=M;exports.MARKER_FILL=X;exports.MARKER_FILL_ACTIVE=nt;exports.MARKER_HTML_DIAMETER_REM=ee;exports.MARKER_HTML_FONT_REM=te;exports.MARKER_SIZE=v;exports.MARKER_SPRITE_SCALE=R;exports.MARKER_SPRITE_SCALE_ACTIVE=J;exports.NORMALIZED_SIZE=ae;exports.OCCLUSION_BIAS=ne;exports.TOUR_DISTANCE_INWARD=ue;exports.TOUR_DISTANCE_OUTWARD=ce;exports.annotationSummary=wt;exports.annotationsInReadingOrder=I;exports.applyFov=xt;exports.applyLighting=mt;exports.applyMarkerSpriteSize=st;exports.applyVisibilityDefaults=Je;exports.buildRegionMeshMap=Xe;exports.buildTranscript=Mt;exports.clampMarkerSize=T;exports.clearRegionHighlight=tt;exports.computeDisplayBox=O;exports.computeTourPlacement=yt;exports.createAnnouncer=St;exports.createMarkerSprite=Q;exports.createMarkerSprites=ot;exports.createOcclusionIndex=lt;exports.disposeSceneGraph=Rt;exports.distanceForFov=gt;exports.drawMarkerCanvas=L;exports.errorsOf=j;exports.fetchBytes=H;exports.flyCamera=$t;exports.frameModel=ht;exports.fromViewerPosition=We;exports.highlightRegion=et;exports.isAuthoredRegion=Y;exports.isGlbParseError=je;exports.isVec3=_;exports.lightingPresetNames=dt;exports.loadModel=Pe;exports.logFindings=Ge;exports.luminance=se;exports.markerSizeText=it;exports.modelSummary=At;exports.needsConversion=N;exports.normalizeAnnotationIds=q;exports.normalizeHexColor=ie;exports.normalizeModel=Ye;exports.parseBodySegments=qe;exports.projectMarkers=at;exports.projectedMarkerSize=ct;exports.readEmbedded=K;exports.readableRegions=Ke;exports.regionBindingCounts=Qe;exports.regionLabel=E;exports.regionsInOrder=C;exports.resolveBackground=pt;exports.resourcePathFor=ze;exports.setMarkerSpriteColor=rt;exports.setRegionVisible=Z;exports.sidecarUrlFor=W;exports.toViewerPosition=w;exports.transcriptFileName=It;
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { Annotation, AnnotationCamera, EduModelHub, ModelLicense, ModelMeta
15
15
  export { fromViewerPosition, isVec3, needsConversion, toViewerPosition, type Vec3 } from "./coordinates.js";
16
16
  export { annotationsInReadingOrder, isAuthoredRegion, normalizeAnnotationIds, normalizeModel, parseBodySegments, readableRegions, regionLabel, regionsInOrder, type BodySegment, type IdentifiedAnnotation, type MarkerAnchor, type NormalizedModel } from "./normalize.js";
17
17
  export { applyVisibilityDefaults, buildRegionMeshMap, clearRegionHighlight, highlightRegion, regionBindingCounts, setRegionVisible, type GltfJsonLike, type RegionMeshMap } from "./regions.js";
18
- export { MARKER_FILL, MARKER_FILL_ACTIVE, createMarkerSprite, createMarkerSprites, drawMarkerCanvas, projectMarkers, setMarkerSpriteColor, type MarkerColors, type MarkerScreenPosition, type ProjectMarkersOptions } from "./markers.js";
18
+ export { MARKER_FILL, MARKER_FILL_ACTIVE, MARKER_HTML_DIAMETER_REM, MARKER_HTML_FONT_REM, MARKER_SIZE, MARKER_SPRITE_SCALE, MARKER_SPRITE_SCALE_ACTIVE, applyMarkerSpriteSize, clampMarkerSize, createMarkerSprite, createMarkerSprites, drawMarkerCanvas, markerSizeText, projectMarkers, projectedMarkerSize, setMarkerSpriteColor, type MarkerColors, type MarkerScreenPosition, type MarkerSizeRange, type MarkerSpriteSizeOptions, type ProjectMarkersOptions, type ProjectedMarkerSize } from "./markers.js";
19
19
  export { OCCLUSION_BIAS, createOcclusionIndex, type OcclusionIndex } from "./occlusion.js";
20
20
  export { DEFAULT_LIGHTING, LIGHTING_PRESETS, applyLighting, lightingPresetNames, type LightSpec, type LightingHandle, type LightingOptions, type LightingPreset } from "./lighting.js";
21
21
  export { BACKGROUND_OPTIONS, DEFAULT_BACKGROUND, luminance, normalizeHexColor, resolveBackground, type BackgroundOption } from "./backgrounds.js";