@harpern/edu-modelhub-viewer-core 0.1.0-beta.2 → 0.1.0-beta.4
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 +49 -1
- package/README.md +3 -3
- package/dist/backgrounds.d.ts +7 -7
- package/dist/camera.d.ts +3 -3
- package/dist/framing.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -17
- package/dist/lighting.d.ts +1 -1
- package/dist/markers.d.ts +4 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,47 @@ here.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.1.0-beta.4] - 2026-09-14
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- The occlusion index no longer passes `maxLeafTris` when it builds each
|
|
16
|
+
mesh's bounding-volume tree. `three-mesh-bvh` 0.9.7 deprecated that option
|
|
17
|
+
and 0.9.11 renamed its replacement to `targetLeafSize`, so on current
|
|
18
|
+
releases every model load printed one deprecation warning per mesh. The
|
|
19
|
+
tree is now built with the library's default leaf size, ten triangles
|
|
20
|
+
instead of eight, which behaves the same on every version the peer range
|
|
21
|
+
allows.
|
|
22
|
+
|
|
23
|
+
## [0.1.0-beta.3] - 2026-09-13
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- American spellings throughout the source, tests and README, matching the
|
|
28
|
+
rest of the project. The only user-visible effect is the background swatch
|
|
29
|
+
labels, which now read "Light gray", "Medium gray" and "Dark gray". The
|
|
30
|
+
swatch ids are deliberately unchanged, still `light-grey`, `medium-grey`
|
|
31
|
+
and `dark-grey`, because both viewers and any saved collection or model
|
|
32
|
+
settings refer to them by id.
|
|
33
|
+
|
|
34
|
+
## [0.1.0-beta.2] - 2026-09-13
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `readableRegions` and `isAuthoredRegion`, for prose views such as a reading
|
|
39
|
+
mode. The workspace addon invents one region per object when
|
|
40
|
+
nobody authored any, described as "Auto-created from object …"; these
|
|
41
|
+
helpers drop those while keeping named regions that simply have no
|
|
42
|
+
description. Interactive region lists should keep using every region,
|
|
43
|
+
because an invented region still highlights real geometry.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The spoken model summary says "labeled regions" instead of "labelled
|
|
48
|
+
regions".
|
|
49
|
+
|
|
9
50
|
## [0.1.0-beta.1] - 2026-09-13
|
|
10
51
|
|
|
11
52
|
### Fixed
|
|
@@ -21,7 +62,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
62
|
which checks the error name: with the parser bundled in, a class identity
|
|
22
63
|
check against a separate copy would silently fail.
|
|
23
64
|
|
|
24
|
-
## [
|
|
65
|
+
## [0.1.0-beta.0] - 2026-09-13
|
|
66
|
+
|
|
67
|
+
First publish. Deprecated: it declared a runtime dependency on an unpublished
|
|
68
|
+
package and cannot be installed. Use `0.1.0-beta.1` or later.
|
|
25
69
|
|
|
26
70
|
### Added
|
|
27
71
|
|
|
@@ -43,4 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
43
87
|
so rigged models no longer frame several times too small.
|
|
44
88
|
- Camera placement preferring an authored camera, with an eased flight that
|
|
45
89
|
becomes a jump cut under reduced motion.
|
|
90
|
+
- Lighting presets, including rigs measured from Sketchfab's viewer, applied
|
|
91
|
+
and removed as a unit.
|
|
92
|
+
- Background swatches, with custom colors resolved and their lightness
|
|
93
|
+
measured so overlays stay readable.
|
|
46
94
|
- A spoken model summary, a throttled announcer, and a Markdown transcript.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The parts of an EDU_modelhub viewer that should not be written twice.
|
|
4
4
|
|
|
5
|
-
This package holds the
|
|
5
|
+
This package holds the behavior two viewers of the same model must agree on:
|
|
6
6
|
how metadata is found, what happens when it is missing, how annotations are
|
|
7
7
|
numbered, where markers sit, which geometry a region owns, and where the
|
|
8
8
|
camera goes when someone selects an annotation.
|
|
@@ -72,7 +72,7 @@ used to mean the opposite, so this rule is enforced in one function.
|
|
|
72
72
|
|
|
73
73
|
**Regions bound to geometry.** Built from `primitive.extras.modelhubRegionId`,
|
|
74
74
|
which three.js copies onto the geometry's `userData` rather than the mesh's.
|
|
75
|
-
`applyVisibilityDefaults`
|
|
75
|
+
`applyVisibilityDefaults` honors a region authored as hidden.
|
|
76
76
|
|
|
77
77
|
**Markers.** Numbered from reading order, so the visual order and the
|
|
78
78
|
announced order cannot disagree. Use sprites in the scene, or project anchors
|
|
@@ -86,6 +86,6 @@ viewer prefers reduced motion.
|
|
|
86
86
|
**Text.** A spoken model summary, a throttled announcer so held keys do not
|
|
87
87
|
flood a live region, and a Markdown transcript of everything the model says.
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## License
|
|
90
90
|
|
|
91
91
|
Apache-2.0.
|
package/dist/backgrounds.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Viewer background
|
|
2
|
+
* Viewer background colors.
|
|
3
3
|
*
|
|
4
4
|
* Which background a model sits on changes how readable it is: a pale bone
|
|
5
|
-
* specimen disappears on white and reads clearly on dark
|
|
5
|
+
* specimen disappears on white and reads clearly on dark gray. Offering the
|
|
6
6
|
* same choices in every viewer means an instructor who picked one for a
|
|
7
7
|
* collection gets it wherever the model is shown.
|
|
8
8
|
*/
|
|
@@ -12,11 +12,11 @@ export interface BackgroundOption {
|
|
|
12
12
|
label: string;
|
|
13
13
|
/** Canonical `#rrggbb`. */
|
|
14
14
|
hex: string;
|
|
15
|
-
/** True when the
|
|
15
|
+
/** True when the color is light enough that dark overlays read better. */
|
|
16
16
|
light: boolean;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* The standard swatches. Dark
|
|
19
|
+
* The standard swatches. Dark gray is the reference background, and gallery
|
|
20
20
|
* thumbnails are captured against it.
|
|
21
21
|
*/
|
|
22
22
|
export declare const BACKGROUND_OPTIONS: readonly BackgroundOption[];
|
|
@@ -25,7 +25,7 @@ export declare const DEFAULT_BACKGROUND = "dark-grey";
|
|
|
25
25
|
/**
|
|
26
26
|
* Accept `#rgb` or `#rrggbb`, with or without the hash, and return canonical
|
|
27
27
|
* `#rrggbb`. Anything else returns an empty string, so a viewer falls back to
|
|
28
|
-
* a known swatch rather than handing an invalid
|
|
28
|
+
* a known swatch rather than handing an invalid color to the renderer.
|
|
29
29
|
*/
|
|
30
30
|
export declare function normalizeHexColor(value: unknown): string;
|
|
31
31
|
/** Perceived brightness, 0 to 1, using the usual luma weights. */
|
|
@@ -33,8 +33,8 @@ export declare function luminance(hex: string): number;
|
|
|
33
33
|
/**
|
|
34
34
|
* The background actually in force.
|
|
35
35
|
*
|
|
36
|
-
* A custom
|
|
36
|
+
* A custom color, typically chosen per collection or per model, wins over
|
|
37
37
|
* the named swatch, and its `light` flag is measured rather than declared so
|
|
38
|
-
* overlays stay readable on a
|
|
38
|
+
* overlays stay readable on a color nobody anticipated.
|
|
39
39
|
*/
|
|
40
40
|
export declare function resolveBackground(backgroundId?: string, custom?: string): BackgroundOption;
|
package/dist/camera.d.ts
CHANGED
|
@@ -24,9 +24,9 @@ export interface CameraPlacement {
|
|
|
24
24
|
/** Tuning for the computed fallback placement. */
|
|
25
25
|
export interface TourPlacementOptions {
|
|
26
26
|
/**
|
|
27
|
-
* `true` puts the camera between the model
|
|
27
|
+
* `true` puts the camera between the model center and the annotation,
|
|
28
28
|
* looking outward, which suits interior walkthrough scenes. `false` puts it
|
|
29
|
-
* beyond the annotation looking back at the
|
|
29
|
+
* beyond the annotation looking back at the center, which suits a single
|
|
30
30
|
* specimen and needs a real standoff or the camera lands inside the mesh.
|
|
31
31
|
*/
|
|
32
32
|
outward?: boolean;
|
|
@@ -60,7 +60,7 @@ export interface FlyOptions {
|
|
|
60
60
|
durationMs?: number;
|
|
61
61
|
/** Called after each step so the host can request a render. */
|
|
62
62
|
onFrame?: () => void;
|
|
63
|
-
/** Called once the move finishes or is
|
|
63
|
+
/** Called once the move finishes or is canceled. */
|
|
64
64
|
onDone?: () => void;
|
|
65
65
|
}
|
|
66
66
|
/** Handle for an in-flight camera move. */
|
package/dist/framing.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Fitting a model into view.
|
|
3
3
|
*
|
|
4
4
|
* Models arrive at wildly different scales, so every viewer normalizes them
|
|
5
|
-
* to a known size and
|
|
5
|
+
* to a known size and centers them at the origin. Doing it the same way in
|
|
6
6
|
* both apps means an authored camera means the same thing in each.
|
|
7
7
|
*/
|
|
8
8
|
import { Box3 } from "three";
|
|
@@ -19,7 +19,7 @@ export declare const NORMALIZED_SIZE = 2;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function computeDisplayBox(root: Object3D): Box3;
|
|
21
21
|
/**
|
|
22
|
-
* Scale a model so its longest axis is `size`, then
|
|
22
|
+
* Scale a model so its longest axis is `size`, then center it at the origin.
|
|
23
23
|
* Mutates the model's transform and returns the scale factor applied.
|
|
24
24
|
*/
|
|
25
25
|
export declare function frameModel(root: Object3D, size?: number): number;
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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,{maxLeafTris:8});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 grey",hex:"#d4d4d4",light:!0},{id:"medium-grey",label:"Medium grey",hex:"#808080",light:!1},{id:"dark-grey",label:"Dark grey",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 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}
|
|
2
2
|
${t}
|
|
3
3
|
|
|
4
4
|
`}function pt(e,t={}){const n=e.model??{},r=R(e),o=I(e);let i=`# ${n.title||"Untitled model"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* placing annotation markers, and moving the camera to them.
|
|
8
8
|
*
|
|
9
9
|
* Nothing here renders or owns a scene. A host brings its own three.js
|
|
10
|
-
* renderer and its own UI; this package supplies the
|
|
10
|
+
* renderer and its own UI; this package supplies the behavior underneath so
|
|
11
11
|
* two viewers of the same model agree about what it says.
|
|
12
12
|
*/
|
|
13
13
|
export { errorsOf, fetchBytes, loadModel, logFindings, isGlbParseError, readEmbedded, resourcePathFor, sidecarUrlFor, type LoadOptions, type LoadedModel, type MetadataResult, type MetadataSource } from "./metadata.js";
|
package/dist/index.js
CHANGED
|
@@ -558,7 +558,7 @@ function z(e) {
|
|
|
558
558
|
).map((n) => n.region);
|
|
559
559
|
}
|
|
560
560
|
const Be = "Auto-created from object ";
|
|
561
|
-
function
|
|
561
|
+
function De(e) {
|
|
562
562
|
var n;
|
|
563
563
|
const t = (n = e == null ? void 0 : e.description) == null ? void 0 : n.trim();
|
|
564
564
|
return t ? !t.startsWith(Be) : !1;
|
|
@@ -567,11 +567,11 @@ function tt(e) {
|
|
|
567
567
|
return Array.isArray(e) ? e.filter(
|
|
568
568
|
(t) => {
|
|
569
569
|
var n;
|
|
570
|
-
return
|
|
570
|
+
return De(t) || !((n = t.description) != null && n.trim());
|
|
571
571
|
}
|
|
572
572
|
) : [];
|
|
573
573
|
}
|
|
574
|
-
function
|
|
574
|
+
function D(e) {
|
|
575
575
|
var t;
|
|
576
576
|
return e ? ((t = e.label) == null ? void 0 : t.trim()) || e.id || "Unnamed region" : "Unknown region";
|
|
577
577
|
}
|
|
@@ -626,7 +626,7 @@ function ot(e) {
|
|
|
626
626
|
hiddenRegionIds: l
|
|
627
627
|
};
|
|
628
628
|
}
|
|
629
|
-
function
|
|
629
|
+
function Le(e) {
|
|
630
630
|
return e.isMesh === !0;
|
|
631
631
|
}
|
|
632
632
|
function rt(e, t, n = []) {
|
|
@@ -645,7 +645,7 @@ function rt(e, t, n = []) {
|
|
|
645
645
|
const c = new Set(n.map((a) => a.id));
|
|
646
646
|
return e.traverse((a) => {
|
|
647
647
|
var p, y;
|
|
648
|
-
if (!
|
|
648
|
+
if (!Le(a)) return;
|
|
649
649
|
const u = (y = (p = a.geometry) == null ? void 0 : p.userData) == null ? void 0 : y.modelhubRegionId, f = typeof u == "string" && u || i.get(a.name) || r.get(a.name) || (c.has(a.name) ? a.name : void 0);
|
|
650
650
|
if (!f) return;
|
|
651
651
|
const m = o.get(f);
|
|
@@ -738,7 +738,7 @@ async function mt(e) {
|
|
|
738
738
|
c != null && c.computeBoundsTree && (t.push(c), n.push(s));
|
|
739
739
|
}), !t.length) return Ue;
|
|
740
740
|
for (const s of t)
|
|
741
|
-
s.boundsTree || (i = s.computeBoundsTree) == null || i.call(s
|
|
741
|
+
s.boundsTree || (i = s.computeBoundsTree) == null || i.call(s);
|
|
742
742
|
const o = new q();
|
|
743
743
|
o.firstHitOnly = !0;
|
|
744
744
|
const r = new b();
|
|
@@ -846,9 +846,9 @@ function pt(e) {
|
|
|
846
846
|
}
|
|
847
847
|
const S = [
|
|
848
848
|
{ id: "white", label: "White", hex: "#ffffff", light: !0 },
|
|
849
|
-
{ id: "light-grey", label: "Light
|
|
850
|
-
{ id: "medium-grey", label: "Medium
|
|
851
|
-
{ id: "dark-grey", label: "Dark
|
|
849
|
+
{ id: "light-grey", label: "Light gray", hex: "#d4d4d4", light: !0 },
|
|
850
|
+
{ id: "medium-grey", label: "Medium gray", hex: "#808080", light: !1 },
|
|
851
|
+
{ id: "dark-grey", label: "Dark gray", hex: "#252525", light: !1 },
|
|
852
852
|
{ id: "black", label: "Black", hex: "#000000", light: !1 }
|
|
853
853
|
], Pe = "dark-grey";
|
|
854
854
|
function ze(e) {
|
|
@@ -870,7 +870,7 @@ function gt(e, t) {
|
|
|
870
870
|
} : S.find((o) => o.id === e) ?? S.find((o) => o.id === Pe) ?? S[0];
|
|
871
871
|
}
|
|
872
872
|
const Ve = 2;
|
|
873
|
-
function
|
|
873
|
+
function L(e) {
|
|
874
874
|
e.updateMatrixWorld(!0);
|
|
875
875
|
const t = new v(), n = new v();
|
|
876
876
|
return e.traverse((o) => {
|
|
@@ -879,9 +879,9 @@ function D(e) {
|
|
|
879
879
|
}), t.isEmpty() ? new v().setFromObject(e) : t;
|
|
880
880
|
}
|
|
881
881
|
function yt(e, t = Ve) {
|
|
882
|
-
const o =
|
|
882
|
+
const o = L(e).getSize(new b()), r = Math.max(o.x, o.y, o.z) || 1, i = t / r;
|
|
883
883
|
e.scale.multiplyScalar(i);
|
|
884
|
-
const s =
|
|
884
|
+
const s = L(e);
|
|
885
885
|
return e.position.sub(s.getCenter(new b())), i;
|
|
886
886
|
}
|
|
887
887
|
function bt(e, t = 1.3) {
|
|
@@ -985,7 +985,7 @@ function It(e, t = {}) {
|
|
|
985
985
|
|
|
986
986
|
`;
|
|
987
987
|
for (const s of o)
|
|
988
|
-
i += `### ${
|
|
988
|
+
i += `### ${D(s)}
|
|
989
989
|
${s.description || "No description."}
|
|
990
990
|
|
|
991
991
|
`;
|
|
@@ -993,7 +993,7 @@ ${s.description || "No description."}
|
|
|
993
993
|
return r.length && (i += `## Annotations (${r.length})
|
|
994
994
|
|
|
995
995
|
`, r.forEach((s, c) => {
|
|
996
|
-
const l = s.regionId ? ` (${
|
|
996
|
+
const l = s.regionId ? ` (${D(o.find((u) => u.id === s.regionId))})` : "", a = `${c + 1}. ${s.title || "Untitled"}${l}`;
|
|
997
997
|
i += `### ${a}
|
|
998
998
|
${s.body || "No description."}
|
|
999
999
|
|
|
@@ -1047,7 +1047,7 @@ export {
|
|
|
1047
1047
|
rt as buildRegionMeshMap,
|
|
1048
1048
|
It as buildTranscript,
|
|
1049
1049
|
ct as clearRegionHighlight,
|
|
1050
|
-
|
|
1050
|
+
L as computeDisplayBox,
|
|
1051
1051
|
$t as computeTourPlacement,
|
|
1052
1052
|
St as createAnnouncer,
|
|
1053
1053
|
Ce as createMarkerSprite,
|
|
@@ -1062,7 +1062,7 @@ export {
|
|
|
1062
1062
|
yt as frameModel,
|
|
1063
1063
|
et as fromViewerPosition,
|
|
1064
1064
|
at as highlightRegion,
|
|
1065
|
-
|
|
1065
|
+
De as isAuthoredRegion,
|
|
1066
1066
|
Je as isGlbParseError,
|
|
1067
1067
|
G as isVec3,
|
|
1068
1068
|
ht as lightingPresetNames,
|
|
@@ -1079,7 +1079,7 @@ export {
|
|
|
1079
1079
|
Ne as readEmbedded,
|
|
1080
1080
|
tt as readableRegions,
|
|
1081
1081
|
it as regionBindingCounts,
|
|
1082
|
-
|
|
1082
|
+
D as regionLabel,
|
|
1083
1083
|
z as regionsInOrder,
|
|
1084
1084
|
gt as resolveBackground,
|
|
1085
1085
|
Ke as resourcePathFor,
|
package/dist/lighting.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Scene lighting.
|
|
3
3
|
*
|
|
4
4
|
* Lighting is not decoration here. These rigs were measured from this
|
|
5
|
-
*
|
|
5
|
+
* organization's Sketchfab viewers through the Viewer API, so a model that
|
|
6
6
|
* was tuned on Sketchfab looks the same after migration. Keeping them in one
|
|
7
7
|
* place means a model lit correctly in one viewer is lit correctly in both.
|
|
8
8
|
*
|
package/dist/markers.d.ts
CHANGED
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
import { Sprite, Vector3 } from "three";
|
|
13
13
|
import type { Camera, Object3D } from "three";
|
|
14
14
|
import type { MarkerAnchor } from "./normalize.js";
|
|
15
|
-
/**
|
|
15
|
+
/** Colors for a marker's circle and number. */
|
|
16
16
|
export interface MarkerColors {
|
|
17
17
|
fill?: string;
|
|
18
18
|
text?: string;
|
|
19
19
|
textStroke?: string;
|
|
20
20
|
}
|
|
21
|
-
/** Default unselected marker
|
|
21
|
+
/** Default unselected marker color. */
|
|
22
22
|
export declare const MARKER_FILL = "#2196f3";
|
|
23
|
-
/** Default selected marker
|
|
23
|
+
/** Default selected marker color. */
|
|
24
24
|
export declare const MARKER_FILL_ACTIVE = "#ff5722";
|
|
25
25
|
/**
|
|
26
26
|
* Draw a numbered marker: a filled circle, a white ring, and the number.
|
|
@@ -35,7 +35,7 @@ export declare function drawMarkerCanvas(value: number | string, colors?: Marker
|
|
|
35
35
|
* `occlusion.ts` rather than by the depth buffer.
|
|
36
36
|
*/
|
|
37
37
|
export declare function createMarkerSprite(value: number | string, scale?: number, colors?: MarkerColors): Sprite;
|
|
38
|
-
/** Redraw a sprite's texture in a new
|
|
38
|
+
/** Redraw a sprite's texture in a new color, disposing the old one. */
|
|
39
39
|
export declare function setMarkerSpriteColor(sprite: Sprite, value: number | string, fill: string): void;
|
|
40
40
|
/**
|
|
41
41
|
* Add one sprite per marker anchor, parented to the model so they inherit
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harpern/edu-modelhub-viewer-core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
4
|
-
"description": "Framework-agnostic viewer core for EDU_modelhub models: metadata loading, normalization, region binding, annotation markers, and camera
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
|
+
"description": "Framework-agnostic viewer core for EDU_modelhub models: metadata loading, normalization, region binding, annotation markers, and camera behavior shared by every conforming viewer",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Oregon State University Ecampus",
|
|
7
7
|
"contributors": [
|