Files
dwg-dxf-viewer-sample/src/topProgress.ts
T
minsungandClaude Opus 5.5 aa27945d61 feat(viewer2d): 2.5D view, viewport fill transform and Z-aware fit box
- 2.5D view: a toolbar toggle (plus an options-panel checkbox) re-aims the
  orthographic camera at an SE-isometric angle and unlocks orbit, so the
  CAD elevation already in the vertex buffers (contours, TIN, 3D
  polylines) becomes visible. Nothing is re-tessellated; switching back
  restores the locked top view. A Z exaggeration slider (persisted in the
  options store) makes kilometre-wide sheets with metres of relief
  legible; picking and the X/Y readout intersect the Z=0 plane.
- Viewport fills: SOLID/HATCH fills and dimension arrowheads drawn
  through a paper-space VIEWPORT now get the model-to-paper transform
  (_fillXf / _bakeFillXf), as lines already did via pushSeg.
- The trimmed content box keeps the drawing's real Z range instead of
  collapsing to z=0, so the 2.5D fit and clip slab cover the relief.
- src/topProgress.ts: driver for the existing #progress loading bar. It
  is not imported from main.ts yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:48:19 +09:00

164 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Top loading bar (NProgress-style) for the DWG/DXF load pipeline.
*
* Why two layers:
* The expensive stages — `parse_dwg_json` on the WASM heap and
* `Viewer2D.load()` — are SYNCHRONOUS. While they run, the main thread is
* blocked: no rAF ticks, no `width` transitions, no repaint. A JS-driven bar
* simply freezes for the seconds that matter most.
*
* · `.bar` real progress, driven by `width` (accurate, main-thread bound).
* · `.sweep` liveness, a `transform` keyframe animation. Transform keyframes
* are handed to the compositor thread, so the stripe keeps moving
* across a blocked main thread. That is the only feedback the page
* can give while the parser is inside WASM.
*
* The markup and CSS live in index.html (`#progress`), same as the other
* panels; this module just drives them.
*/
/** Wait until the browser has actually painted the pending style change. */
function nextPaint(): Promise<void> {
return new Promise<void>((resolve) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
resolve();
};
// Two frames: the first callback still runs BEFORE paint, the second one
// only fires once the frame carrying our change has been composited.
requestAnimationFrame(() => requestAnimationFrame(finish));
// Backgrounded tabs never fire rAF — don't stall the load behind it.
setTimeout(finish, 150);
});
}
export class TopProgress {
private root: HTMLElement;
private bar: HTMLElement;
private value = 0;
private hideTimer = 0;
constructor(root: HTMLElement) {
this.root = root;
this.bar = root.querySelector<HTMLElement>('.bar') ?? root;
}
/** Show the bar and reset it to zero (no rewind animation). */
start(): void {
window.clearTimeout(this.hideTimer);
this.root.classList.remove('err');
this.value = 0;
this.jumpTo(0);
this.root.classList.add('on');
}
/**
* Move to `v` (0–1). Monotonic — a lower value is ignored, so out-of-order
* stage callbacks can never make the bar walk backwards.
*/
set(v: number): void {
const next = Math.max(this.value, Math.min(1, Math.max(0, v)));
if (next === this.value) return;
this.value = next;
this.bar.style.width = `${(next * 100).toFixed(2)}%`;
}
/** Toggle the compositor-driven sweep for a stage that blocks the main thread. */
busy(on: boolean): void {
this.root.classList.toggle('busy', on);
}
/** Resolve once the pending width/class change is on screen. */
paint(): Promise<void> {
return nextPaint();
}
/** Fill to 100% and fade out. */
done(): void {
this.finish();
}
/** Fill to 100% in the error color and fade out. */
fail(): void {
this.root.classList.add('err');
this.finish();
}
private finish(): void {
this.busy(false);
this.set(1);
window.clearTimeout(this.hideTimer);
this.hideTimer = window.setTimeout(() => {
this.root.classList.remove('on');
// Rewind only after the fade-out has finished, so the bar never visibly
// slides back to zero.
this.hideTimer = window.setTimeout(() => {
this.root.classList.remove('err');
this.value = 0;
this.jumpTo(0);
}, 320);
}, 260);
}
/** Set the width with the transition suppressed for one frame. */
private jumpTo(v: number): void {
const prev = this.bar.style.transition;
this.bar.style.transition = 'none';
this.bar.style.width = `${(v * 100).toFixed(2)}%`;
void this.bar.offsetWidth; // force reflow so the next change animates again
this.bar.style.transition = prev;
}
}
/** 1.4 MB / 812 kB — short byte label for the download readout. */
export function formatBytes(n: number): string {
if (!Number.isFinite(n) || n < 0) return '?';
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} kB`;
return `${(n / 1024 / 1024).toFixed(1)} MB`;
}
/**
* fetch() that reports download progress.
*
* Falls back to a plain `arrayBuffer()` when the response has no body stream
* or no Content-Length (then `total` is 0 and the caller should show an
* indeterminate state). `loaded` is decompressed bytes while Content-Length is
* the compressed size, so the ratio is clamped to 1.
*/
export async function fetchWithProgress(
url: string,
onProgress: (loaded: number, total: number) => void,
): Promise<ArrayBuffer> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const total = Number(res.headers.get('content-length')) || 0;
if (!res.body || !total) {
const buf = await res.arrayBuffer();
onProgress(buf.byteLength, buf.byteLength);
return buf;
}
const reader = res.body.getReader();
const chunks: Uint8Array[] = [];
let loaded = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
chunks.push(value);
loaded += value.length;
onProgress(loaded, total);
}
const out = new Uint8Array(loaded);
let offset = 0;
for (const c of chunks) {
out.set(c, offset);
offset += c.length;
}
return out.buffer;
}