Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
TypeScript
at mathquill
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341import { buildScene3dPreview, cancelScene3dBuilds, refineScene3dAdaptive, refineScene3d, refineScene3dQ3} from './scene3d';import { mergeScene3dPart, mergeScene3dSettledCreaseAdaptiveGroup, mergeScene3dSettledCreaseDeepAdaptiveGroup, mergeScene3dSettledCreaseGroup, type Scene3dPartResult} from './scene3d-merge';import { adaptImplicitCellTargets, explicitSurfaceAdaptiveChildTargets, settledContinuousCreaseAdaptiveChildTargets, settledContinuousCreaseDeepAdaptiveChildTargets, settledContinuousCreaseAdaptiveTargets, selectSettledContinuousCreaseAdaptiveTargets, selectExplicitSurfaceAdaptiveTargets, selectQ3AdaptiveTargets} from './scene3d-targets';import { isScene3dSupersededError } from './scene3d-worker-client';import { cloneScene3dViewSnapshot, isValidScene3dViewSnapshot, sameScene3dViewSnapshot, type Scene3dViewSnapshot} from './scene3d-view';import type { GraphDocument3d, GpuSurfaceResource3d, ImplicitAdaptiveTarget3d, RenderScene3d, Scene3dRefinementTarget} from './types';
export const SCENE3D_REFINE_DELAY_MS = 200;const MAX_SCENE3D_PRESENTATION_MEASURES = 120;
interface Scene3dPresentationControllerOptions { publish: (scene: RenderScene3d, document: GraphDocument3d) => void; setBusy: (busy: boolean) => void; setError: (error: string | null) => void; setTimer?: (callback: () => void, delayMs: number) => number; clearTimer?: (handle: number) => void; requestAnimationFrame?: (callback: (timestamp: number) => void) => number; cancelAnimationFrame?: (handle: number) => void; buildPreview?: (document: GraphDocument3d, revision: number, view?: Scene3dViewSnapshot) => Promise<RenderScene3d>; refine?: ( document: GraphDocument3d, revision: number, targets: Scene3dRefinementTarget[], onpart: (part: Scene3dPartResult) => void, onstage?: (stage: 'q2') => void, view?: Scene3dViewSnapshot ) => Promise<void>; refineQ3?: ( document: GraphDocument3d, revision: number, candidates: ImplicitAdaptiveTarget3d[], onpart: (part: Scene3dPartResult) => void, view?: Scene3dViewSnapshot ) => Promise<void>; refineAdaptive?: ( document: GraphDocument3d, revision: number, targets: Scene3dRefinementTarget[], onpart: (part: Scene3dPartResult) => void, view?: Scene3dViewSnapshot ) => Promise<void>; cancel?: () => void; now?: () => number; setPhase?: (phase: 'idle' | 'preview' | 'refining') => void;}
/** Coordinates retained coarse scenes and cancellable, frame-batched refinement. */export class Scene3dPresentationController { readonly #publish: Scene3dPresentationControllerOptions['publish']; readonly #setBusy: (busy: boolean) => void; readonly #setError: (error: string | null) => void; readonly #setTimer: (callback: () => void, delayMs: number) => number; readonly #clearTimer: (handle: number) => void; readonly #requestAnimationFrame: (callback: (timestamp: number) => void) => number; readonly #cancelAnimationFrame: (handle: number) => void; readonly #buildPreview: NonNullable<Scene3dPresentationControllerOptions['buildPreview']>; readonly #refine: NonNullable<Scene3dPresentationControllerOptions['refine']>; readonly #refineQ3: NonNullable<Scene3dPresentationControllerOptions['refineQ3']>; readonly #refineAdaptive: NonNullable<Scene3dPresentationControllerOptions['refineAdaptive']>; readonly #cancel: () => void; readonly #now: () => number; readonly #setPhase: (phase: 'idle' | 'preview' | 'refining') => void; #revision = 0; #scene: RenderScene3d | null = null; #document: GraphDocument3d | null = null; /** Document that produced the geometry in the currently displayed scene. */ #displayedGeometryDocument: GraphDocument3d | null = null; #refineTimer: number | undefined; #frameHandle: number | undefined; #pendingParts: Scene3dPartResult[] = []; #pendingAtomicParts: Scene3dPartResult[] = []; #pendingDeepParts: Scene3dPartResult[] = []; #pendingSettledCreaseParts: Scene3dPartResult[] = []; #pendingSettledCreaseDeepParts: Scene3dPartResult[] = []; /** q2 parents successfully published for later bounded q3 admission. */ #settledQ2Targets: Array<Extract<Scene3dRefinementTarget, { kind: 'settled-continuous-crease-adaptive-child' }>> = []; #interactiveGpuCache = new Map<string, GpuSurfaceResource3d>(); #stagingScene: RenderScene3d | null = null; #retainedBoundsTransition = false; #stagingQ1TargetCount = 0; #stagingQ1PartCount = 0; #stagingMergedPartCount = 0; #stagingMergedQualityLevel: 1 | 2 | 3 = 1; #frameStartedAt = 0; #refineDueAt = 0; #q2SettledRevision: number | undefined; #viewSnapshot: Scene3dViewSnapshot | undefined; #cameraRefineTimer: number | undefined; #viewEpoch = 0; #lastViewChangedAt = 0; #refinementGeneration = 0;
constructor(options: Scene3dPresentationControllerOptions) { this.#publish = options.publish; this.#setBusy = options.setBusy; this.#setError = options.setError; this.#setTimer = options.setTimer ?? ((callback, delay) => window.setTimeout(callback, delay)); this.#clearTimer = options.clearTimer ?? ((handle) => window.clearTimeout(handle)); this.#requestAnimationFrame = options.requestAnimationFrame ?? ((callback) => { if (typeof window !== 'undefined' && window.requestAnimationFrame) { return window.requestAnimationFrame(callback); }
return this.#setTimer(() => callback(this.#now()), 0); }); this.#cancelAnimationFrame = options.cancelAnimationFrame ?? ((handle) => { if (typeof window !== 'undefined' && window.cancelAnimationFrame) { window.cancelAnimationFrame(handle); } else { this.#clearTimer(handle); } }); this.#buildPreview = options.buildPreview ?? buildScene3dPreview; this.#refine = options.refine ?? refineScene3d; this.#refineQ3 = options.refineQ3 ?? refineScene3dQ3; this.#refineAdaptive = options.refineAdaptive ?? refineScene3dAdaptive; this.#cancel = options.cancel ?? cancelScene3dBuilds; this.#now = options.now ?? (() => performance.now()); this.#setPhase = options.setPhase ?? (() => undefined); }
/** Update presentation state without changing the document revision or demand. */ setViewSnapshot(snapshot: Scene3dViewSnapshot | undefined) { if (sameScene3dViewSnapshot(this.#viewSnapshot, snapshot)) return; this.#viewSnapshot = cloneScene3dViewSnapshot(snapshot); this.#viewEpoch += 1; this.#lastViewChangedAt = this.#now();
if (this.#q2SettledRevision === this.#revision && this.#scene) { this.#armCameraRefinement(); } }
setDocument(document: GraphDocument3d) { const previousDocument = this.#document;
const overrideOnly = Boolean( previousDocument && onlyVariableOverridesChanged(previousDocument, document) );
const colorOnly = Boolean( previousDocument && this.#scene && onlyColorChanges(previousDocument, document) );
if (colorOnly && this.#scene) { this.#document = document; this.#cancel(); this.#refinementGeneration += 1; this.#revision += 1; this.#q2SettledRevision = undefined; this.#settledQ2Targets = []; this.#clearRefineTimer(); this.#clearCameraRefineTimer(); this.#discardPendingFrame(); this.#retainedBoundsTransition = false; this.#scene = applyDocumentColors(this.#scene, document);
for (const [expressionId, resource] of this.#interactiveGpuCache) { this.#interactiveGpuCache.set(expressionId, applyDocumentColorsToResource(resource, document)); }
this.#displayedGeometryDocument = document; this.#setError(null); this.#setBusy(false); this.#setPhase('idle'); this.#publish(this.#scene, document);
return; }
const retainDisplayedScene = Boolean( this.#scene && this.#scene.resources.length > 0 && this.#displayedGeometryDocument && onlyBoundsChanged(this.#displayedGeometryDocument, document) );
if (previousDocument && !sameInteractiveGeometryDocument(previousDocument, document)) { this.#interactiveGpuCache.clear(); }
this.#document = document;
if (this.#revision > 0) this.#cancel(); this.#refinementGeneration += 1; const revision = ++this.#revision; this.#q2SettledRevision = undefined; this.#settledQ2Targets = []; this.#clearRefineTimer(); this.#clearCameraRefineTimer(); this.#discardPendingFrame(); this.#retainedBoundsTransition = retainDisplayedScene; this.#refineDueAt = this.#now() + SCENE3D_REFINE_DELAY_MS; this.#setBusy(true); this.#setPhase('preview');
if (overrideOnly && this.#scene) { this.#scene = restoreCachedGpuOverrides( optimisticGpuOverrides(this.#scene, document.variable_overrides ?? {}), document.variable_overrides ?? {}, this.#interactiveGpuCache ); this.#displayedGeometryDocument = document; this.#publish(this.#scene, document); }
const build = this.#buildPreview(document, revision, this.#viewSnapshot); void build .then((scene) => { if (revision !== this.#revision) return; this.#syncInteractiveGpuCache(scene); this.#setError(null); const targets = adaptImplicitCellTargets(scene);
if (this.#retainedBoundsTransition) { // Keep the last useful geometry visible while the replacement // preview/refinement is staged. GraphCanvas3d applies the new // bounds independently, so clipping and guides stay responsive. this.#stagingScene = scene; this.#stagingQ1TargetCount = targets.filter( (target) => target.qualityLevel < 2 && target.kind !== 'settled-continuous-crease-patch' ).length; this.#stagingQ1PartCount = 0; this.#stagingMergedPartCount = 0; this.#stagingMergedQualityLevel = 1;
if (targets.length === 0) { // A bounds-only preview with no refinement work is already // settled, so it is safe to publish it immediately. If q2 // work remains, keep even this preview staged until closure. this.#publishStagingScene(document); } } else { this.#scene = scene; this.#displayedGeometryDocument = document; this.#publish(scene, document); }
if (targets.length === 0) { this.#setBusy(false); this.#setPhase('idle');
return; }
this.#scheduleRefinement(document, revision, targets); }) .catch((error) => { if (revision !== this.#revision) return;
if (isScene3dSupersededError(error)) { this.#setBusy(false); this.#setPhase('idle');
return; }
this.#setError(error instanceof Error ? error.message : String(error)); this.#setBusy(false); this.#setPhase('idle'); }); }
dispose() { this.#revision += 1; this.#refinementGeneration += 1; this.#clearRefineTimer(); this.#clearCameraRefineTimer(); this.#discardPendingFrame(); this.#cancel(); this.#interactiveGpuCache.clear(); this.#q2SettledRevision = undefined; this.#settledQ2Targets = []; this.#setPhase('idle'); }
#scheduleRefinement(document: GraphDocument3d, revision: number, targets: Scene3dRefinementTarget[]) { const delay = Math.max(0, this.#refineDueAt - this.#now()); this.#refineTimer = this.#setTimer(() => { this.#refineTimer = undefined;
if (revision !== this.#revision) return; const generation = ++this.#refinementGeneration; const requestViewEpoch = this.#viewEpoch;
const optionalSettledCreaseOnly = targets.length > 0 && targets.every( (target) => target.kind === 'settled-continuous-crease-patch' || target.kind === 'settled-continuous-crease-adaptive-child' || target.kind === 'settled-continuous-crease-deep-adaptive-child' );
this.#setPhase('refining'); this.#frameStartedAt = this.#now(); void this.#refine(document, revision, targets, (part) => { if (revision !== this.#revision || generation !== this.#refinementGeneration || !this.#scene) return;
if ( 'kind' in part.part && (part.part.kind === 'settled-continuous-crease-patch' || part.part.kind === 'settled-continuous-crease-adaptive-child') ) { this.#pendingSettledCreaseParts.push(part);
return; }
if ( 'kind' in part.part && part.part.kind === 'settled-continuous-crease-deep-adaptive-child' ) { this.#pendingSettledCreaseDeepParts.push(part);
return; }
if (part.part.qualityLevel >= 3) { this.#pendingDeepParts.push(part);
return; }
if (part.part.qualityLevel >= 2) { this.#pendingAtomicParts.push(part);
return; }
if (this.#retainedBoundsTransition) { // Worker refinement emits exactly one q1 part for every q1 // target, including empty parts. Keep all of them off-screen // until the complete q1 replacement is ready. this.#pendingParts.push(part); this.#stagingQ1PartCount += 1;
if (this.#stagingQ1PartCount >= this.#stagingQ1TargetCount) { this.#mergeStagedQuality1(revision); }
return; }
this.#pendingParts.push(part); this.#scheduleFrame(revision, document); }, (stage) => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return;
if (stage === 'q2') { this.#q2SettledRevision = revision;
if (this.#retainedBoundsTransition) { this.#mergeStagedQuality1(revision); this.#mergeStagedQuality2(revision); this.#mergeStagedSettledCrease(revision); // q2 is the settled bounds-transition result. Publish it // now, while optional q3 work remains buffered as a later // atomic upgrade. this.#publishStagingScene(document, revision); this.#retainedBoundsTransition = false; } else { // All q1 messages precede the stage marker, but a pending // animation frame may not have flushed them yet. Settle q1 // before publishing q2 so a late q1 frame cannot reintroduce // lower-quality resources beside the atomic replacement. this.#flushPendingParts(revision, document); this.#flushAtomicParts(revision, document, 2); this.#flushSettledCreaseParts(revision, document); }
this.#setBusy(false); this.#setPhase('idle');
if (this.#viewEpoch !== requestViewEpoch) this.#armCameraRefinement(); } }, this.#viewSnapshot) .then(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return;
if (this.#retainedBoundsTransition) { // Keep the old displayed scene visible until the refinement // transaction closes. Merge any late q1 callbacks and all q2 // replacements into staging, then publish exactly once. this.#mergeStagedQuality1(revision); this.#mergeStagedQuality2(revision); this.#mergeStagedDeep(revision); this.#mergeStagedSettledCrease(revision); this.#mergeStagedSettledCreaseDeep(revision); this.#publishStagingScene(document, revision); } else { this.#flushPendingParts(revision, document); this.#flushAtomicParts(revision, document, 2); this.#flushAtomicParts(revision, document, 3); this.#flushSettledCreaseParts(revision, document); this.#flushSettledCreaseDeepParts(revision, document); }
this.#retainedBoundsTransition = false; this.#setBusy(false); this.#setPhase('idle'); }) .catch((error) => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return;
if (isScene3dSupersededError(error)) { this.#discardPendingFrame(); this.#retainedBoundsTransition = false; this.#setBusy(false); this.#setPhase('idle');
return; }
this.#discardPendingFrame(); this.#retainedBoundsTransition = false;
if (this.#q2SettledRevision === revision) { // q3 is an optional upgrade. A worker failure after q2 // settled must preserve q2 without becoming a user-facing // diagnostic. this.#setBusy(false); this.#setPhase('idle');
return; }
if (optionalSettledCreaseOnly) { // Settled crease work is an optional replacement. If its worker // request fails before producing a complete group, preserve the // interpolated GPU scene without surfacing a red diagnostic. this.#setBusy(false); this.#setPhase('idle');
return; }
this.#setError(error instanceof Error ? error.message : String(error)); this.#setBusy(false); this.#setPhase('idle'); }); }, delay); }
#armCameraRefinement() { this.#clearCameraRefineTimer();
if ( this.#q2SettledRevision !== this.#revision || !this.#scene || !this.#document || !isValidScene3dViewSnapshot(this.#viewSnapshot) || this.#lastViewChangedAt <= 0 ) return; const dueAt = this.#lastViewChangedAt + SCENE3D_REFINE_DELAY_MS; const delay = Math.max(0, dueAt - this.#now()); this.#cameraRefineTimer = this.#setTimer(() => { this.#cameraRefineTimer = undefined; this.#startCameraRefinement(); }, delay); }
#startCameraRefinement() { const document = this.#document; const revision = this.#revision; const view = cloneScene3dViewSnapshot(this.#viewSnapshot);
if ( !document || this.#q2SettledRevision !== revision || !this.#scene || !isValidScene3dViewSnapshot(view) ) return; const scene = this.#scene;
const candidates = (this.#scene.implicit_adaptive_targets ?? []).filter((candidate) => candidate.quality_level === 3 && this.#scene?.model_revisions[candidate.expression_id] === candidate.model_revision && (this.#scene?.coverage_revisions?.[candidate.expression_id] ?? 0) === (candidate.coverage_revision ?? 0) );
const q3Selection = selectQ3AdaptiveTargets(candidates, view);
const explicitCandidates = (this.#scene.explicit_surface_adaptive_targets ?? []).filter((candidate) => this.#scene?.model_revisions[candidate.expression_id] === candidate.model_revision && (this.#scene?.coverage_revisions?.[candidate.expression_id] ?? 0) === (candidate.coverage_revision ?? 0) );
const explicitSelection = selectExplicitSurfaceAdaptiveTargets( explicitCandidates, this.#scene.bounds, view );
const explicitChildren = explicitSurfaceAdaptiveChildTargets( explicitSelection.selected, this.#scene.bounds, view );
const settledCandidates = settledContinuousCreaseAdaptiveTargets( scene.settled_continuous_crease_adaptive_targets ?? [] ).filter((candidate) => scene.model_revisions[candidate.expressionId] === candidate.modelRevision && (scene.coverage_revisions?.[candidate.expressionId] ?? 0) === (candidate.coverageRevision ?? 0) );
const settledSelection = selectSettledContinuousCreaseAdaptiveTargets( settledCandidates, scene.bounds, view, 'camera-upgrade' );
const settledChildren = settledContinuousCreaseAdaptiveChildTargets( settledSelection, scene.bounds );
const settledDeepParents = this.#settledQ2Targets.filter((candidate) => scene.resources.some((resource) => resource.type === 'mesh' && resource.key.expression_id === candidate.expressionId && resource.key.region === candidate.region && resource.key.quality_level === 2 && resource.model_revision === candidate.modelRevision && (resource.coverage_revision ?? 0) === (candidate.coverageRevision ?? 0) ) && !scene.resources.some((resource) => resource.type === 'mesh' && resource.key.expression_id === candidate.expressionId && resource.key.region.startsWith('patch-d2-') && resource.model_revision === candidate.modelRevision && (resource.coverage_revision ?? 0) === (candidate.coverageRevision ?? 0) && resource.key.region === `patch-d2-${candidate.index[0] * 2}-${candidate.index[1] * 2}` ) );
const settledDeepChildren = settledContinuousCreaseDeepAdaptiveChildTargets( settledDeepParents, scene.bounds, view );
const adaptiveTargets = [...explicitChildren, ...settledChildren, ...settledDeepChildren];
if (q3Selection.selected.length === 0 && adaptiveTargets.length === 0) return;
const generation = ++this.#refinementGeneration; this.#cancel(); this.#discardPendingFrame();
if (adaptiveTargets.length === 0) { // Preserve the existing synchronous admission point for q3-only // camera work; callers and tests use this to observe a request as // soon as the settled-view timer fires. void this.#refineQ3(document, revision, candidates, (part) => { if (revision !== this.#revision || generation !== this.#refinementGeneration || !this.#scene) return;
if (part.part.qualityLevel >= 3) this.#pendingDeepParts.push(part); }, view) .then(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return; this.#flushAtomicParts(revision, document, 3); }) .catch(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return; this.#discardPendingFrame(); });
return; }
const refineAdaptive = adaptiveTargets.length > 0 ? this.#refineAdaptive(document, revision, adaptiveTargets, (part) => { if (revision !== this.#revision || generation !== this.#refinementGeneration || !this.#scene) return;
if ('kind' in part.part && part.part.kind === 'settled-continuous-crease-adaptive-child') { this.#pendingSettledCreaseParts.push(part); } else if ( 'kind' in part.part && part.part.kind === 'settled-continuous-crease-deep-adaptive-child' ) { this.#pendingSettledCreaseDeepParts.push(part); } else if (part.part.qualityLevel >= 2) { this.#pendingAtomicParts.push(part); } }, view) : Promise.resolve();
void refineAdaptive .then(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return undefined; this.#flushAtomicParts(revision, document, 2); this.#flushSettledCreaseParts(revision, document); this.#flushSettledCreaseDeepParts(revision, document);
if (q3Selection.selected.length === 0) return undefined;
return this.#refineQ3(document, revision, candidates, (part) => { if (revision !== this.#revision || generation !== this.#refinementGeneration || !this.#scene) return;
if (part.part.qualityLevel >= 3) this.#pendingDeepParts.push(part); }, view); }) .then(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return; this.#flushAtomicParts(revision, document, 3); }) .catch(() => { if (revision !== this.#revision || generation !== this.#refinementGeneration) return; // Camera-only q3 is opportunistic. Preserve the published q2 scene // and leave the next settled view free to retry. this.#discardPendingFrame(); }); }
#scheduleFrame(revision: number, document: GraphDocument3d) { if (this.#frameHandle !== undefined) return; this.#frameStartedAt = this.#now(); this.#frameHandle = this.#requestAnimationFrame(() => { this.#frameHandle = undefined;
if (revision !== this.#revision) { this.#pendingParts = [];
return; }
this.#flushPendingParts(revision, document); }); }
#flushPendingParts(revision: number, document: GraphDocument3d) { if (revision !== this.#revision || !this.#scene || this.#pendingParts.length === 0) return;
if (this.#frameHandle !== undefined) { this.#cancelAnimationFrame(this.#frameHandle); this.#frameHandle = undefined; }
const parts = this.#pendingParts.splice(0); const mergeStartedAt = this.#now();
for (const part of parts) this.#scene = mergeScene3dPart(this.#scene, part); const mergedAt = this.#now(); this.#publish(this.#scene, document); const publishedAt = this.#now(); const qualityLevel: 1 | 2 = parts.some((part) => part.part.qualityLevel >= 2) ? 2 : 1; recordScene3dPresentation({ revision, partCount: parts.length, qualityLevel, strategy: 'normal', mergeMs: Math.max(0, mergedAt - mergeStartedAt), publishMs: Math.max(0, publishedAt - mergedAt), frameMs: Math.max(0, publishedAt - this.#frameStartedAt) }); }
#mergeStagedQuality1(revision: number) { if ( revision !== this.#revision || !this.#stagingScene ) return; const parts = this.#pendingParts.splice(0);
for (const part of parts) this.#stagingScene = mergeScene3dPart(this.#stagingScene, part); this.#stagingMergedPartCount += parts.length; }
#mergeStagedQuality2(revision: number) { if (revision !== this.#revision || !this.#stagingScene || this.#pendingAtomicParts.length === 0) return; const parts = completeExplicitAdaptiveGroups(this.#pendingAtomicParts.splice(0));
if (parts.length === 0) return;
for (const part of parts) this.#stagingScene = mergeScene3dPart(this.#stagingScene, part); this.#stagingMergedPartCount += parts.length; this.#stagingMergedQualityLevel = 2; }
#mergeStagedDeep(revision: number) { if (revision !== this.#revision || !this.#stagingScene || this.#pendingDeepParts.length === 0) return; const parts = this.#pendingDeepParts.splice(0);
for (const part of parts) this.#stagingScene = mergeScene3dPart(this.#stagingScene, part); this.#stagingMergedPartCount += parts.length; this.#stagingMergedQualityLevel = 3; }
#mergeStagedSettledCrease(revision: number) { if (revision !== this.#revision || !this.#stagingScene || this.#pendingSettledCreaseParts.length === 0) return; let stagingScene = this.#stagingScene; const pending = this.#pendingSettledCreaseParts.splice(0); const groups = completeSettledCreaseGroups(pending); const consumed = new Set<Scene3dPartResult>(groups.flat());
const adaptiveGroups = completeSettledCreaseAdaptiveGroups( pending.filter((part) => !consumed.has(part)) );
for (const group of [...groups, ...adaptiveGroups]) { const merged = group.some((part) => 'kind' in part.part && part.part.kind === 'settled-continuous-crease-adaptive-child' ) ? mergeScene3dSettledCreaseAdaptiveGroup(stagingScene, group) : mergeScene3dSettledCreaseGroup(stagingScene, group);
if (merged !== stagingScene) { this.#rememberSettledQ2Targets(group); stagingScene = merged; this.#stagingMergedPartCount += group.length; this.#stagingMergedQualityLevel = group.some((part) => part.part.qualityLevel >= 2) ? 2 : 1; } }
this.#stagingScene = stagingScene; }
#mergeStagedSettledCreaseDeep(revision: number) { if (revision !== this.#revision || !this.#stagingScene || this.#pendingSettledCreaseDeepParts.length === 0) return; let stagingScene = this.#stagingScene; const pending = this.#pendingSettledCreaseDeepParts.splice(0);
for (const group of completeSettledCreaseDeepGroups(pending)) { const merged = mergeScene3dSettledCreaseDeepAdaptiveGroup(stagingScene, group);
if (merged === stagingScene) continue; stagingScene = merged; this.#stagingMergedPartCount += group.length; this.#stagingMergedQualityLevel = 3; }
this.#stagingScene = stagingScene; }
#publishStagingScene(document: GraphDocument3d, revision?: number) { if (!this.#stagingScene) return; this.#scene = this.#stagingScene; this.#displayedGeometryDocument = document; this.#publish(this.#scene, document);
if (revision !== undefined && this.#stagingMergedPartCount > 0) { recordScene3dPresentation({ revision, partCount: this.#stagingMergedPartCount, qualityLevel: this.#stagingMergedQualityLevel, strategy: 'bounds-retained', mergeMs: 0, publishMs: 0, frameMs: Math.max(0, this.#now() - this.#frameStartedAt) }); }
this.#stagingScene = null; }
#syncInteractiveGpuCache(scene: RenderScene3d) { const candidateIds = new Set( (scene.settled_continuous_crease_patch_targets ?? []).map((target) => target.expression_id) );
for (const expressionId of this.#interactiveGpuCache.keys()) { if (!candidateIds.has(expressionId)) this.#interactiveGpuCache.delete(expressionId); }
for (const expressionId of candidateIds) { const resource = scene.resources.find((candidate) => candidate.type === 'gpu_surface' && candidate.key.expression_id === expressionId && candidate.key.representation === 'surface_gpu' && candidate.key.region === 'whole' );
if (!resource || resource.type !== 'gpu_surface') { this.#interactiveGpuCache.delete(expressionId); continue; }
const cached = this.#interactiveGpuCache.get(expressionId);
if (cached && !sameGpuSurfaceRoute(cached, resource)) { this.#interactiveGpuCache.delete(expressionId); }
this.#interactiveGpuCache.set(expressionId, resource); } }
#flushAtomicParts(revision: number, document: GraphDocument3d, qualityLevel: 2 | 3) { const pending = qualityLevel === 3 ? this.#pendingDeepParts : this.#pendingAtomicParts;
if (revision !== this.#revision || !this.#scene || pending.length === 0) return;
const parts = qualityLevel === 2 ? completeExplicitAdaptiveGroups(pending.splice(0)) : pending.splice(0);
if (parts.length === 0) return; const mergeStartedAt = this.#now();
for (const part of parts) this.#scene = mergeScene3dPart(this.#scene, part); const mergedAt = this.#now(); this.#displayedGeometryDocument = document; this.#publish(this.#scene, document); const publishedAt = this.#now(); recordScene3dPresentation({ revision, partCount: parts.length, qualityLevel, strategy: this.#retainedBoundsTransition ? 'bounds-retained' : 'normal', mergeMs: Math.max(0, mergedAt - mergeStartedAt), publishMs: Math.max(0, publishedAt - mergedAt), frameMs: Math.max(0, publishedAt - this.#frameStartedAt) }); }
#flushSettledCreaseParts(revision: number, document: GraphDocument3d) { if (revision !== this.#revision || !this.#scene || this.#pendingSettledCreaseParts.length === 0) return; const pending = this.#pendingSettledCreaseParts.splice(0); const groups = completeSettledCreaseGroups(pending); const consumed = new Set<Scene3dPartResult>(groups.flat());
const adaptiveGroups = completeSettledCreaseAdaptiveGroups( pending.filter((part) => !consumed.has(part)) );
if (groups.length === 0 && adaptiveGroups.length === 0) return; const mergeStartedAt = this.#now(); let mergedParts = 0;
for (const group of groups) { const merged = mergeScene3dSettledCreaseGroup(this.#scene, group);
if (merged === this.#scene) continue; this.#rememberSettledQ2Targets(group); this.#scene = merged; mergedParts += group.length; }
for (const group of adaptiveGroups) { const merged = mergeScene3dSettledCreaseAdaptiveGroup(this.#scene, group);
if (merged === this.#scene) continue; this.#rememberSettledQ2Targets(group); this.#scene = merged; mergedParts += group.length; }
if (mergedParts === 0) return; const mergedAt = this.#now(); this.#displayedGeometryDocument = document; this.#publish(this.#scene, document); const publishedAt = this.#now(); recordScene3dPresentation({ revision, partCount: mergedParts, qualityLevel: adaptiveGroups.length > 0 ? 2 : 1, strategy: this.#retainedBoundsTransition ? 'bounds-retained' : 'normal', mergeMs: Math.max(0, mergedAt - mergeStartedAt), publishMs: Math.max(0, publishedAt - mergedAt), frameMs: Math.max(0, publishedAt - this.#frameStartedAt) }); }
#flushSettledCreaseDeepParts(revision: number, document: GraphDocument3d) { if (revision !== this.#revision || !this.#scene || this.#pendingSettledCreaseDeepParts.length === 0) return; const pending = this.#pendingSettledCreaseDeepParts.splice(0); const groups = completeSettledCreaseDeepGroups(pending);
if (groups.length === 0) return; const mergeStartedAt = this.#now(); let mergedParts = 0;
for (const group of groups) { const merged = mergeScene3dSettledCreaseDeepAdaptiveGroup(this.#scene, group);
if (merged === this.#scene) continue; this.#scene = merged; mergedParts += group.length; }
if (mergedParts === 0) return; const mergedAt = this.#now(); this.#displayedGeometryDocument = document; this.#publish(this.#scene, document); const publishedAt = this.#now(); recordScene3dPresentation({ revision, partCount: mergedParts, qualityLevel: 3, strategy: this.#retainedBoundsTransition ? 'bounds-retained' : 'normal', mergeMs: Math.max(0, mergedAt - mergeStartedAt), publishMs: Math.max(0, publishedAt - mergedAt), frameMs: Math.max(0, publishedAt - this.#frameStartedAt) }); }
#rememberSettledQ2Targets(parts: Scene3dPartResult[]) { for (const result of parts) { if (!('kind' in result.part) || result.part.kind !== 'settled-continuous-crease-adaptive-child') continue; const key = `${result.part.expressionId}|${result.part.index.join(',')}|${result.part.modelRevision}|${result.part.coverageRevision ?? 0}`;
if (!this.#settledQ2Targets.some((target) => `${target.expressionId}|${target.index.join(',')}|${target.modelRevision}|${target.coverageRevision ?? 0}` === key )) { this.#settledQ2Targets.push(result.part); } } }
#discardPendingFrame() { if (this.#frameHandle !== undefined) { this.#cancelAnimationFrame(this.#frameHandle); this.#frameHandle = undefined; }
this.#pendingParts = []; this.#pendingAtomicParts = []; this.#pendingDeepParts = []; this.#pendingSettledCreaseParts = []; this.#pendingSettledCreaseDeepParts = []; this.#stagingScene = null; this.#stagingQ1TargetCount = 0; this.#stagingQ1PartCount = 0; this.#stagingMergedPartCount = 0; this.#stagingMergedQualityLevel = 1; }
#clearRefineTimer() { if (this.#refineTimer === undefined) return; this.#clearTimer(this.#refineTimer); this.#refineTimer = undefined; }
#clearCameraRefineTimer() { if (this.#cameraRefineTimer === undefined) return; this.#clearTimer(this.#cameraRefineTimer); this.#cameraRefineTimer = undefined; }}
function completeExplicitAdaptiveGroups(parts: Scene3dPartResult[]) { type ExplicitAdaptivePart = Scene3dPartResult & { part: Extract<Scene3dRefinementTarget, { kind: 'explicit-surface-adaptive-child' }>; };
const isExplicitAdaptivePart = (part: Scene3dPartResult): part is ExplicitAdaptivePart => 'kind' in part.part && part.part.kind === 'explicit-surface-adaptive-child';
const isSettledAdaptivePart = (part: Scene3dPartResult) => 'kind' in part.part && part.part.kind === 'settled-continuous-crease-adaptive-child';
const complete = parts.filter((part) => !isExplicitAdaptivePart(part) && !isSettledAdaptivePart(part)); const groups = new Map<string, ExplicitAdaptivePart[]>();
for (const part of parts) { if (!isExplicitAdaptivePart(part)) continue; const group = groups.get(part.part.atomicGroup) ?? []; group.push(part); groups.set(part.part.atomicGroup, group); }
for (const group of groups.values()) { const childBits = new Set(group.map((part) => part.part.childBits.join(',')));
if (group.length === 4 && childBits.size === 4) complete.push(...group); }
return complete;}
function completeSettledCreaseGroups(parts: Scene3dPartResult[]) { type SettledCreasePart = Scene3dPartResult & { part: Extract<Scene3dRefinementTarget, { kind: 'settled-continuous-crease-patch' }>; };
type SettledAdaptivePart = Scene3dPartResult & { part: Extract<Scene3dRefinementTarget, { kind: 'settled-continuous-crease-adaptive-child' }>; };
const grouped = new Map<string, SettledCreasePart[]>(); const adaptiveByRootGroup = new Map<string, SettledAdaptivePart[]>();
for (const result of parts) { if (!('kind' in result.part)) continue;
if (result.part.kind === 'settled-continuous-crease-patch') { const group = grouped.get(result.part.atomicGroup) ?? []; group.push(result as SettledCreasePart); grouped.set(result.part.atomicGroup, group); } else if (result.part.kind === 'settled-continuous-crease-adaptive-child') { const suffix = `|patch-${result.part.parentPatch[0]}-${result.part.parentPatch[1]}`;
const rootGroup = result.part.atomicGroup.endsWith(suffix) ? result.part.atomicGroup.slice(0, -suffix.length) : result.part.atomicGroup;
const adaptive = adaptiveByRootGroup.get(rootGroup) ?? []; adaptive.push(result as SettledAdaptivePart); adaptiveByRootGroup.set(rootGroup, adaptive); } }
return [...grouped.values()].filter((group) => { if (group.length !== 16) return false;
if (new Set(group.map((result) => result.part.patch.join(','))).size !== 16) return false; const rootGroup = group[0].part.atomicGroup; const adaptive = adaptiveByRootGroup.get(rootGroup) ?? []; const adaptiveGroups = new Map<string, SettledAdaptivePart[]>();
for (const result of adaptive) { const childGroup = adaptiveGroups.get(result.part.atomicGroup) ?? []; childGroup.push(result); adaptiveGroups.set(result.part.atomicGroup, childGroup); }
return [...adaptiveGroups.values()].every((childGroup) => { if (childGroup.length !== 4) return false;
return new Set(childGroup.map((result) => result.part.childBits.join(','))).size === 4; }); });}
function completeSettledCreaseAdaptiveGroups(parts: Scene3dPartResult[]) { type SettledAdaptivePart = Scene3dPartResult & { part: Extract<Scene3dRefinementTarget, { kind: 'settled-continuous-crease-adaptive-child' }>; };
const grouped = new Map<string, SettledAdaptivePart[]>();
for (const result of parts) { if (!('kind' in result.part) || result.part.kind !== 'settled-continuous-crease-adaptive-child') continue; const group = grouped.get(result.part.atomicGroup) ?? []; group.push(result as SettledAdaptivePart); grouped.set(result.part.atomicGroup, group); }
return [...grouped.values()].filter((group) => { if (group.length !== 4) return false; const bits = new Set(group.map((result) => result.part.childBits.join(',')));
return bits.size === 4 && [...bits].every((value) => ['0,0', '0,1', '1,0', '1,1'].includes(value)); });}
function completeSettledCreaseDeepGroups(parts: Scene3dPartResult[]) { type SettledDeepPart = Scene3dPartResult & { part: Extract<Scene3dRefinementTarget, { kind: 'settled-continuous-crease-deep-adaptive-child' }>; };
const grouped = new Map<string, SettledDeepPart[]>();
for (const result of parts) { if (!('kind' in result.part) || result.part.kind !== 'settled-continuous-crease-deep-adaptive-child') continue; const group = grouped.get(result.part.atomicGroup) ?? []; group.push(result as SettledDeepPart); grouped.set(result.part.atomicGroup, group); }
return [...grouped.values()].filter((group) => { if (group.length !== 4) return false; const bits = new Set(group.map((result) => result.part.childBits.join(',')));
return bits.size === 4 && [...bits].every((value) => ['0,0', '0,1', '1,0', '1,1'].includes(value)); });}
function interactiveGeometryFingerprint(document: GraphDocument3d) { return { bounds: document.bounds, // Complex mode changes expression semantics, so it must invalidate every // retained geometry/cache path just like a source edit. complex_mode: document.complex_mode ?? false, expressions: document.expressions.map(({ id, input_mode, source, enabled }) => ({ id, input_mode, source, enabled })) };}
function sameInteractiveGeometryDocument(left: GraphDocument3d, right: GraphDocument3d) { return JSON.stringify(interactiveGeometryFingerprint(left)) === JSON.stringify(interactiveGeometryFingerprint(right));}
function onlyColorChanges(left: GraphDocument3d, right: GraphDocument3d) { if (!sameInteractiveGeometryDocument(left, right)) return false;
if (JSON.stringify(left.variable_overrides ?? {}) !== JSON.stringify(right.variable_overrides ?? {})) { return false; }
if (left.expressions.length !== right.expressions.length) return false;
return left.expressions.some((expression, index) => !Object.is(expression.color, right.expressions[index]?.color) );}
function applyDocumentColors(scene: RenderScene3d, document: GraphDocument3d): RenderScene3d { return { ...scene, resources: scene.resources.map((resource) => applyDocumentColorToResource(resource, document) ) };}
function applyDocumentColorToResource<T extends { key: { expression_id: string }; color: string }>( resource: T, document: GraphDocument3d): T { const color = document.expressions.find((expression) => expression.id === resource.key.expression_id)?.color;
return color === null || color === undefined ? resource : { ...resource, color };}
function applyDocumentColorsToResource( resource: GpuSurfaceResource3d, document: GraphDocument3d) { return applyDocumentColorToResource(resource, document);}
function restoreCachedGpuOverrides( scene: RenderScene3d, overrides: Record<string, number>, cache: Map<string, GpuSurfaceResource3d>): RenderScene3d { if (cache.size === 0) return scene; const resources: RenderScene3d['resources'] = []; const restored = new Set<string>();
for (const resource of scene.resources) { const cached = cache.get(resource.key.expression_id);
const sameRevision = cached && resource.model_revision === cached.model_revision && (resource.coverage_revision ?? 0) === (cached.coverage_revision ?? 0);
const settledMesh = Boolean( cached && sameRevision && resource.type === 'mesh' && resource.key.representation === 'surface_mesh' && resource.key.region.startsWith('patch-') );
const cachedGpu = Boolean( cached && sameRevision && resource.type === 'gpu_surface' && resource.key.representation === 'surface_gpu' && resource.key.region === 'whole' );
if (settledMesh || cachedGpu) { if (cached && !restored.has(resource.key.expression_id)) { resources.push(gpuWithOverrides(cached, overrides)); restored.add(resource.key.expression_id); }
continue; }
resources.push(resource); }
for (const [expressionId, cached] of cache) { if (!restored.has(expressionId)) resources.push(gpuWithOverrides(cached, overrides)); }
return { ...scene, resources };}
function gpuWithOverrides( resource: GpuSurfaceResource3d, overrides: Record<string, number>): GpuSurfaceResource3d { return { ...resource, depth_mode: 'interpolated', uniforms: resource.uniforms.map((uniform) => ({ ...uniform, value: overrides[uniform.variable] ?? uniform.value })) };}
function sameGpuSurfaceRoute(left: GpuSurfaceResource3d, right: GpuSurfaceResource3d) { return JSON.stringify({ key: left.key, grid_size: left.grid_size, depth_mode: left.depth_mode, value_glsl: left.value_glsl, cusp_preview: left.cusp_preview ?? null, dx_glsl: left.dx_glsl, dy_glsl: left.dy_glsl, uniforms: left.uniforms.map(({ name, variable }) => ({ name, variable })) }) === JSON.stringify({ key: right.key, grid_size: right.grid_size, depth_mode: right.depth_mode, value_glsl: right.value_glsl, cusp_preview: right.cusp_preview ?? null, dx_glsl: right.dx_glsl, dy_glsl: right.dy_glsl, uniforms: right.uniforms.map(({ name, variable }) => ({ name, variable })) });}
function onlyVariableOverridesChanged(left: GraphDocument3d, right: GraphDocument3d): boolean { if (JSON.stringify({ ...left, variable_overrides: undefined }) !== JSON.stringify({ ...right, variable_overrides: undefined })) { return false; }
const leftOverrides = left.variable_overrides ?? {}; const rightOverrides = right.variable_overrides ?? {}; const names = new Set([...Object.keys(leftOverrides), ...Object.keys(rightOverrides)]);
return [...names].some((name) => !Object.is(leftOverrides[name], rightOverrides[name]));}
function onlyBoundsChanged(left: GraphDocument3d, right: GraphDocument3d): boolean { const boundsDiffer = (Object.keys(left.bounds) as Array<keyof GraphDocument3d['bounds']>) .some((key) => !Object.is(left.bounds[key], right.bounds[key]));
if (!boundsDiffer) return false;
return JSON.stringify({ name: left.name, complex_mode: left.complex_mode ?? false, expressions: left.expressions, variable_overrides: left.variable_overrides ?? {} }) === JSON.stringify({ name: right.name, complex_mode: right.complex_mode ?? false, expressions: right.expressions, variable_overrides: right.variable_overrides ?? {} });}
function optimisticGpuOverrides( scene: RenderScene3d, overrides: Record<string, number>): RenderScene3d { return { ...scene, variables: scene.variables.map((variable) => ({ ...variable, value: overrides[variable.name] ?? variable.value })), resources: scene.resources.map((resource) => { if (resource.type !== 'gpu_surface') return resource;
const updated = { ...resource, uniforms: resource.uniforms.map((uniform) => ({ ...uniform, value: overrides[uniform.variable] ?? uniform.value })) };
// A slider override is not yet covered by the worker's semantic // admission decision. Temporarily use ordinary raster depth for // exact resources so a stale exact-depth correction cannot project // the new value through the old coverage. if (resource.depth_mode === 'exact') { updated.depth_mode = 'interpolated'; }
return updated; }) };}
function recordScene3dPresentation(detail: { revision: number; partCount: number; qualityLevel: 1 | 2 | 3; strategy?: 'normal' | 'bounds-retained'; mergeMs: number; publishMs: number; frameMs: number;}) { if (typeof performance === 'undefined' || typeof performance.measure !== 'function') return;
if (performance.getEntriesByName('cartesium:scene3d-presentation').length >= MAX_SCENE3D_PRESENTATION_MEASURES) { performance.clearMeasures('cartesium:scene3d-presentation'); }
performance.measure('cartesium:scene3d-presentation', { start: Math.max(0, performance.now() - detail.frameMs), duration: detail.frameMs, detail });}