lecodes-web-shared 2.0.0

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.
@@ -0,0 +1,201 @@
1
+ // GENERATED by scripts/vendor-canvas.ts from engines/canvas/spec/gen/spec.ts @ ae6bc30b952b — do not edit here.
2
+ // Bump the submodule and run `bun run vendor:canvas`; `bun run check:canvas-ops` fails when this copy is stale.
3
+ // GENERATED by spec/generate.ts from spec/*.h — do not edit.
4
+
5
+ export const SPEC_VERSION = 1
6
+
7
+ export const LineJoin = {
8
+ MITER: 0,
9
+ ROUND: 1,
10
+ BEVEL: 2,
11
+ } as const
12
+ export type LineJoin = (typeof LineJoin)[keyof typeof LineJoin]
13
+ export const LineJoinName: readonly string[] = ["miter", "round", "bevel"]
14
+
15
+ export const LineCap = {
16
+ BUTT: 0,
17
+ ROUND: 1,
18
+ SQUARE: 2,
19
+ } as const
20
+ export type LineCap = (typeof LineCap)[keyof typeof LineCap]
21
+ export const LineCapName: readonly string[] = ["butt", "round", "square"]
22
+
23
+ export const TextAlign = {
24
+ LEFT: 0,
25
+ CENTER: 1,
26
+ RIGHT: 2,
27
+ START: 3,
28
+ END: 4,
29
+ } as const
30
+ export type TextAlign = (typeof TextAlign)[keyof typeof TextAlign]
31
+ export const TextAlignName: readonly string[] = ["left", "center", "right", "start", "end"]
32
+
33
+ export const TextBaseline = {
34
+ ALPHABETIC: 0,
35
+ TOP: 1,
36
+ MIDDLE: 2,
37
+ BOTTOM: 3,
38
+ HANGING: 4,
39
+ IDEOGRAPHIC: 5,
40
+ } as const
41
+ export type TextBaseline = (typeof TextBaseline)[keyof typeof TextBaseline]
42
+ export const TextBaselineName: readonly string[] = ["alphabetic", "top", "middle", "bottom", "hanging", "ideographic"]
43
+
44
+ export const FillRule = {
45
+ NONZERO: 0,
46
+ EVENODD: 1,
47
+ } as const
48
+ export type FillRule = (typeof FillRule)[keyof typeof FillRule]
49
+ export const FillRuleName: readonly string[] = ["nonzero", "evenodd"]
50
+
51
+ export const Spread = {
52
+ PAD: 0,
53
+ REFLECT: 1,
54
+ REPEAT: 2,
55
+ } as const
56
+ export type Spread = (typeof Spread)[keyof typeof Spread]
57
+ export const SpreadName: readonly string[] = ["pad", "reflect", "repeat"]
58
+
59
+ export const Gradient = {
60
+ LINEAR: 0,
61
+ RADIAL: 1,
62
+ } as const
63
+ export type Gradient = (typeof Gradient)[keyof typeof Gradient]
64
+ export const GradientName: readonly string[] = ["linear", "radial"]
65
+
66
+ export const Paint = {
67
+ COLOR: 0,
68
+ LINEAR: 1,
69
+ RADIAL: 2,
70
+ } as const
71
+ export type Paint = (typeof Paint)[keyof typeof Paint]
72
+ export const PaintName: readonly string[] = ["color", "linear", "radial"]
73
+
74
+ export const PathVerb = {
75
+ MOVE: 0,
76
+ LINE: 1,
77
+ QUAD: 2,
78
+ CUBIC: 3,
79
+ CLOSE: 4,
80
+ } as const
81
+ export type PathVerb = (typeof PathVerb)[keyof typeof PathVerb]
82
+ export const PathVerbName: readonly string[] = ["move", "line", "quad", "cubic", "close"]
83
+
84
+ export const ImageFormat = {
85
+ PNG: 0,
86
+ JPEG: 1,
87
+ } as const
88
+ export type ImageFormat = (typeof ImageFormat)[keyof typeof ImageFormat]
89
+ export const ImageFormatName: readonly string[] = ["png", "jpeg"]
90
+
91
+ /** The opcode stream: op id → the word after it. */
92
+ export const OP = {
93
+ SAVE: 0,
94
+ RESTORE: 1,
95
+ TRANSLATE: 2,
96
+ SCALE: 3,
97
+ ROTATE: 4,
98
+ TRANSFORM: 5,
99
+ SET_TRANSFORM: 6,
100
+ RESET_TRANSFORM: 7,
101
+ GLOBAL_ALPHA: 8,
102
+ FILL_STYLE: 9,
103
+ STROKE_STYLE: 10,
104
+ FILL_GRADIENT: 11,
105
+ STROKE_GRADIENT: 12,
106
+ LINE_WIDTH: 13,
107
+ LINE_JOIN: 14,
108
+ LINE_CAP: 15,
109
+ MITER_LIMIT: 16,
110
+ LINE_DASH: 17,
111
+ LINE_DASH_OFFSET: 18,
112
+ FONT: 19,
113
+ TEXT_ALIGN: 20,
114
+ TEXT_BASELINE: 21,
115
+ LETTER_SPACING: 22,
116
+ PATH_BEGIN: 23,
117
+ PATH_CLOSE: 24,
118
+ MOVE_TO: 25,
119
+ LINE_TO: 26,
120
+ QUADRATIC_TO: 27,
121
+ BEZIER_TO: 28,
122
+ ARC: 29,
123
+ ARC_TO: 30,
124
+ ELLIPSE: 31,
125
+ RECT: 32,
126
+ ROUND_RECT: 33,
127
+ FILL: 34,
128
+ STROKE: 35,
129
+ CLIP: 36,
130
+ FILL_RECT: 37,
131
+ STROKE_RECT: 38,
132
+ CLEAR_RECT: 39,
133
+ FILL_TEXT: 40,
134
+ STROKE_TEXT: 41,
135
+ DRAW_IMAGE: 42,
136
+ } as const
137
+ export type OP = (typeof OP)[keyof typeof OP]
138
+ /** Operand layout per op: `args` is one letter per fixed word (f float, i int, r string ref);
139
+ * `repeat` is the group that follows the trailing count. */
140
+ export const OP_ARGS: Record<number, { args: string, repeat: string }> = {
141
+ 0: { args: "", repeat: "" },
142
+ 1: { args: "", repeat: "" },
143
+ 2: { args: "ff", repeat: "" },
144
+ 3: { args: "ff", repeat: "" },
145
+ 4: { args: "f", repeat: "" },
146
+ 5: { args: "ffffff", repeat: "" },
147
+ 6: { args: "ffffff", repeat: "" },
148
+ 7: { args: "", repeat: "" },
149
+ 8: { args: "f", repeat: "" },
150
+ 9: { args: "r", repeat: "" },
151
+ 10: { args: "r", repeat: "" },
152
+ 11: { args: "iffffffi", repeat: "fr" },
153
+ 12: { args: "iffffffi", repeat: "fr" },
154
+ 13: { args: "f", repeat: "" },
155
+ 14: { args: "i", repeat: "" },
156
+ 15: { args: "i", repeat: "" },
157
+ 16: { args: "f", repeat: "" },
158
+ 17: { args: "i", repeat: "f" },
159
+ 18: { args: "f", repeat: "" },
160
+ 19: { args: "r", repeat: "" },
161
+ 20: { args: "i", repeat: "" },
162
+ 21: { args: "i", repeat: "" },
163
+ 22: { args: "f", repeat: "" },
164
+ 23: { args: "", repeat: "" },
165
+ 24: { args: "", repeat: "" },
166
+ 25: { args: "ff", repeat: "" },
167
+ 26: { args: "ff", repeat: "" },
168
+ 27: { args: "ffff", repeat: "" },
169
+ 28: { args: "ffffff", repeat: "" },
170
+ 29: { args: "fffffi", repeat: "" },
171
+ 30: { args: "fffff", repeat: "" },
172
+ 31: { args: "fffffffi", repeat: "" },
173
+ 32: { args: "ffff", repeat: "" },
174
+ 33: { args: "fffff", repeat: "" },
175
+ 34: { args: "i", repeat: "" },
176
+ 35: { args: "", repeat: "" },
177
+ 36: { args: "i", repeat: "" },
178
+ 37: { args: "ffff", repeat: "" },
179
+ 38: { args: "ffff", repeat: "" },
180
+ 39: { args: "ffff", repeat: "" },
181
+ 40: { args: "rfff", repeat: "" },
182
+ 41: { args: "rfff", repeat: "" },
183
+ 42: { args: "iffffffff", repeat: "" },
184
+ }
185
+ export const OP_NAME: readonly string[] = ["save", "restore", "translate", "scale", "rotate", "transform", "setTransform", "resetTransform", "globalAlpha", "fillStyle", "strokeStyle", "fillGradient", "strokeGradient", "lineWidth", "lineJoin", "lineCap", "miterLimit", "lineDash", "lineDashOffset", "font", "textAlign", "textBaseline", "letterSpacing", "pathBegin", "pathClose", "moveTo", "lineTo", "quadraticTo", "bezierTo", "arc", "arcTo", "ellipse", "rect", "roundRect", "fill", "stroke", "clip", "fillRect", "strokeRect", "clearRect", "fillText", "strokeText", "drawImage"]
186
+
187
+ /** The draw list: command id → the word after it. */
188
+ export const DRAW = {
189
+ SET_TRANSFORM: 0,
190
+ SAVE: 1,
191
+ RESTORE: 2,
192
+ CLIP: 3,
193
+ FILL_PATH: 4,
194
+ STROKE_PATH: 5,
195
+ FILL_TEXT: 6,
196
+ STROKE_TEXT: 7,
197
+ DRAW_IMAGE: 8,
198
+ CLEAR_RECT: 9,
199
+ } as const
200
+ export type DRAW = (typeof DRAW)[keyof typeof DRAW]
201
+ export const DRAW_NAME: readonly string[] = ["setTransform", "save", "restore", "clip", "fillPath", "strokePath", "fillText", "strokeText", "drawImage", "clearRect"]
@@ -0,0 +1,178 @@
1
+ // Map runtime errors from a compiled bundle back to original source locations.
2
+ //
3
+ // The preview runs the bundle via `new Function(code)`, so the browser won't apply the bundle's
4
+ // inline source map to stack frames automatically — we do it here. Two offsets are involved and both
5
+ // are handled: the bundle carries an inline source map whose generated positions already include the
6
+ // compile header (the backend shifts it), and `new Function` adds a constant wrapper offset that we
7
+ // calibrate at runtime. Self-contained (a tiny VLQ decoder), so there's no extra dependency.
8
+
9
+ type RawSourceMap = { version: number, sources: string[], mappings: string, sourcesContent?: (string | null)[] }
10
+ type OrigPos = { source: string, line: number, column: number }
11
+ export type MappedFrame = { fn: string | null, source: string, line: number, column: number }
12
+ /** A runtime error from the preview. `native` errors come from the WASM engine (no JS source to map).
13
+ * `nativeFrames` are the crash's decoded engine (C++) function names, top-first — filled in by a
14
+ * follow-up emit once the wasm host has fetched its symbol map (see viewer's wasmCrashSymbolizer). */
15
+ export type PreviewError = { message: string, frames: MappedFrame[], stack?: string, native?: boolean, detail?: string, nativeFrames?: string[] }
16
+
17
+ /**
18
+ * Is this a native (WASM engine) error rather than a JS error from the user's bundle? Emscripten
19
+ * surfaces C++ exceptions/aborts as a bare number (a pointer), and WebAssembly traps as
20
+ * `WebAssembly.RuntimeError`; either way there's no JS source position to map. We deliberately do
21
+ * *not* treat an arbitrary frameless JS `Error` as native (that would hijack unrelated editor errors).
22
+ */
23
+ export const isNativeError = (e: unknown): boolean => {
24
+ if (typeof e === "number" || typeof e === "bigint") return true
25
+ if (typeof WebAssembly !== "undefined" && e instanceof WebAssembly.RuntimeError) return true
26
+ const stack = e instanceof Error ? (e.stack ?? "") : ""
27
+ return /\bwasm\b|wasm-function|\.wasm/i.test(stack)
28
+ }
29
+
30
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
31
+ const B64_INDEX: Record<string, number> = {}
32
+ for (let i = 0; i < B64.length; i++) B64_INDEX[B64[i]!] = i
33
+
34
+ /** Decode one comma-segment of base64-VLQ into signed ints. */
35
+ const decodeVlq = (segment: string): number[] => {
36
+ const out: number[] = []
37
+ let shift = 0, value = 0
38
+ for (const ch of segment) {
39
+ const digit = B64_INDEX[ch]
40
+ if (digit === undefined) break
41
+ const more = digit & 32
42
+ value += (digit & 31) << shift
43
+ if (more) {
44
+ shift += 5
45
+ } else {
46
+ const negative = value & 1
47
+ value >>= 1
48
+ out.push(negative ? -value : value)
49
+ value = 0
50
+ shift = 0
51
+ }
52
+ }
53
+ return out
54
+ }
55
+
56
+ /** A minimal source-map consumer: original position for a generated (1-based line, 0-based column). */
57
+ export class SourceMap {
58
+ private sources: string[]
59
+ // Per generated line: sorted segments [genCol, srcIdx, srcLine, srcCol].
60
+ private lines: number[][][]
61
+
62
+ constructor(raw: RawSourceMap) {
63
+ this.sources = raw.sources
64
+ this.lines = []
65
+ let srcIdx = 0, srcLine = 0, srcCol = 0
66
+ for (const lineStr of raw.mappings.split(";")) {
67
+ const segs: number[][] = []
68
+ let genCol = 0
69
+ if (lineStr) {
70
+ for (const segStr of lineStr.split(",")) {
71
+ const v = decodeVlq(segStr)
72
+ if (v.length === 0) continue
73
+ genCol += v[0]!
74
+ if (v.length >= 4) {
75
+ srcIdx += v[1]!
76
+ srcLine += v[2]!
77
+ srcCol += v[3]!
78
+ segs.push([ genCol, srcIdx, srcLine, srcCol ])
79
+ }
80
+ }
81
+ }
82
+ this.lines.push(segs)
83
+ }
84
+ }
85
+
86
+ originalPositionFor(line: number, column: number): OrigPos | null {
87
+ const segs = this.lines[line - 1]
88
+ if (!segs || segs.length === 0) return null
89
+ // Largest segment whose generated column is <= the queried column.
90
+ let best: number[] | null = null
91
+ for (const seg of segs) {
92
+ if (seg[0]! <= column) best = seg
93
+ else break
94
+ }
95
+ best ??= segs[0]!
96
+ const source = this.sources[best[1]!]
97
+ if (source === undefined) return null
98
+ return { source, line: best[2]! + 1, column: best[3]! }
99
+ }
100
+ }
101
+
102
+ /** Pull the inline `//# lecodesMap=data:...base64,…` map out of a bundle string (a non-standard
103
+ * directive the browser ignores — see inlineSourceMapComment in the SDK for why). */
104
+ export const extractInlineSourceMap = (code: string): SourceMap | null => {
105
+ const m = code.match(/\/\/# lecodesMap=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)\s*$/)
106
+ if (!m) return null
107
+ try {
108
+ const json = typeof atob === "function" ? atob(m[1]!) : Buffer.from(m[1]!, "base64").toString("utf8")
109
+ return new SourceMap(JSON.parse(json) as RawSourceMap)
110
+ } catch {
111
+ return null
112
+ }
113
+ }
114
+
115
+ // `new Function(body)` wraps the body in `function anonymous(args\n) {\n<body>\n}`, so the body starts
116
+ // a constant number of lines down. Calibrate once per engine by throwing from a one-line body.
117
+ let functionOffset: number | null = null
118
+ const newFunctionLineOffset = (): number => {
119
+ if (functionOffset !== null) return functionOffset
120
+ try {
121
+ new Function("throw new Error('chisel-calibrate')")()
122
+ } catch (e) {
123
+ const stack = (e as Error).stack ?? ""
124
+ // The body coordinate is the `<anonymous>:line:col` one (V8/Chrome). A one-line body throws at
125
+ // body line 1, so its reported line minus 1 is the wrapper offset.
126
+ const m = stack.match(/<anonymous>:(\d+):\d+/)
127
+ functionOffset = m ? parseInt(m[1]!, 10) - 1 : 2
128
+ return functionOffset
129
+ }
130
+ functionOffset = 2
131
+ return functionOffset
132
+ }
133
+
134
+ /** Parse `error.stack`, mapping each `new Function` frame back to original source via `map`. */
135
+ export const mapErrorStack = (error: unknown, map: SourceMap): MappedFrame[] => {
136
+ const stack = (error instanceof Error ? error.stack : "") ?? ""
137
+ const offset = newFunctionLineOffset()
138
+ const frames: MappedFrame[] = []
139
+ for (const raw of stack.split("\n")) {
140
+ // The user-bundle position is the `<anonymous>:line:col` coordinate (V8 gives body-relative
141
+ // lines here; an `eval at … (file:line)` prefix, if present, is the call site — not it).
142
+ const m = raw.match(/<anonymous>:(\d+):(\d+)/)
143
+ if (!m) continue
144
+ const genLine = parseInt(m[1]!, 10) - offset
145
+ const genCol = parseInt(m[2]!, 10) - 1
146
+ if (genLine < 1) continue
147
+ const orig = map.originalPositionFor(genLine, genCol < 0 ? 0 : genCol)
148
+ if (!orig) continue
149
+ const fnMatch = raw.match(/at\s+([^\s(]+)\s*\(/)
150
+ frames.push({ fn: fnMatch ? fnMatch[1]! : null, source: orig.source, line: orig.line, column: orig.column })
151
+ }
152
+ return frames
153
+ }
154
+
155
+ /** Rewrite every `<filename>:line:col` frame in a block of text to its original position.
156
+ * This is the shape a host that EVALUATES the bundle as one named file produces — quickjs on the
157
+ * native hosts (`module.js:5:51631`, see creatorUtilsHandler) and `vm.runInContext` in the headless
158
+ * renderer (`project.js:…`) — as opposed to the preview's `new Function` frames above. Both report
159
+ * 1-based columns. Frames naming other files (a host's own launcher, `native`) pass through. */
160
+ export const mapNamedFileStack = (text: string, map: SourceMap, filename: string): string => {
161
+ const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
162
+ return text.replace(new RegExp(`\\b${escaped}:(\\d+):(\\d+)`, "g"), (whole, l: string, c: string) => {
163
+ const orig = map.originalPositionFor(parseInt(l, 10), Math.max(0, parseInt(c, 10) - 1))
164
+ return orig ? `${orig.source.replace(/^(\.\/|\/)/, "")}:${orig.line}:${orig.column}` : whole
165
+ })
166
+ }
167
+
168
+ /** A one-line, human-readable original location for an error, or null if it can't be mapped. */
169
+ export const formatErrorLocation = (error: unknown, code: string): { message: string, frames: MappedFrame[] } | null => {
170
+ const map = extractInlineSourceMap(code)
171
+ if (!map) return null
172
+ const frames = mapErrorStack(error, map)
173
+ if (frames.length === 0) return null
174
+ const top = frames[0]!
175
+ const msg = error instanceof Error ? error.message : String(error)
176
+ const where = `${top.source}:${top.line}:${top.column}${top.fn ? ` (${top.fn})` : ""}`
177
+ return { message: `${msg}\n at ${where}`, frames }
178
+ }
@@ -0,0 +1,12 @@
1
+ // NativeView on the web hosts (docs/navigation-presentable-plan.md): a factory makes the live DOM
2
+ // instance of a view (`NativeView("name", params)`), the channel carries its events to the SDK
3
+ // (`_creatorTree`'s view events), `call` is the method channel. Factories come from the web halves
4
+ // of the plugins (a host is handed them: `host.registerPlugin`). `doc` is the document the app's DOM lives in, so
5
+ // factories create elements in the right realm.
6
+ export type WebViewChannel = { emit(event: string, data?: any): void }
7
+ export type WebViewInstanceHandle = {
8
+ el: HTMLElement
9
+ call?(method: string, args: any[]): any | Promise<any>
10
+ destroy?(): void
11
+ }
12
+ export type WebViewFactory = (params: any, channel: WebViewChannel, doc: Document) => WebViewInstanceHandle
@@ -0,0 +1,30 @@
1
+ // Device-emulation safe-area override — the editor Simulator's iOS/Android shells. A host without
2
+ // a spec keeps the neutral web defaults it has always used; a spec pins the insets per orientation
3
+ // and declares which edges are exact-height system BARS (Android status/nav bar) — `comfort-*`
4
+ // ADDS its knob past a bar edge instead of flooring against it (creator-ui setSafeAreaBars
5
+ // semantics). Shell inset values live with the embedder (the frontend mirrors lecodes-design's
6
+ // SHELL_SAFE_AREA / `lecodes render --device`).
7
+
8
+ export type SafeAreaEdge = "top" | "right" | "bottom" | "left"
9
+
10
+ export type SafeAreaSpec = {
11
+ /** [top, right, bottom, left] px */
12
+ portrait: [number, number, number, number]
13
+ landscape: [number, number, number, number]
14
+ portraitBars?: SafeAreaEdge[]
15
+ landscapeBars?: SafeAreaEdge[]
16
+ }
17
+
18
+ // The neutral defaults both web hosts have always simulated (portrait: top 20 / bottom 8).
19
+ export const DEFAULT_SAFE_AREA: SafeAreaSpec = {
20
+ portrait: [20, 0, 8, 0],
21
+ landscape: [0, 8, 0, 20],
22
+ }
23
+
24
+ export const orientedSafeArea = (spec: SafeAreaSpec, landscape: boolean): {
25
+ insets: [number, number, number, number]
26
+ bars: SafeAreaEdge[]
27
+ } => ({
28
+ insets: landscape ? spec.landscape : spec.portrait,
29
+ bars: (landscape ? spec.landscapeBars : spec.portraitBars) ?? [],
30
+ })
@@ -0,0 +1,19 @@
1
+ // The effective on-screen scale of a host root, recovered from the DOM.
2
+ //
3
+ // The simulator displays the app at a fixed CSS `zoom` (default 0.75) so the preview matches a real
4
+ // device while the app keeps its true logical viewport. Under `zoom` — and under a legacy
5
+ // `transform: scale()` — an element's getBoundingClientRect() reports VISUAL px (post-scale) while
6
+ // clientWidth/clientHeight stay LAYOUT px (the element's own coordinate space). Their ratio is the
7
+ // effective scale a visual coordinate (e.g. a pointer's `clientX - rect.left`) must be divided by to
8
+ // reach the app's logical space.
9
+ //
10
+ // This is the same computation createUI's getBoundingClientRect bridge already does
11
+ // (appRect.width / displayWidth); factored out so every pointer path shares one definition.
12
+
13
+ /** Visual-to-layout scale of `el` (guards a zero layout size → 1). */
14
+ export const viewScale = (el: HTMLElement): number => {
15
+ const layout = el.clientWidth
16
+ if (!layout) return 1
17
+ const visual = el.getBoundingClientRect().width
18
+ return visual > 0 ? visual / layout : 1
19
+ }
@@ -0,0 +1,162 @@
1
+ // Slot 0 is reserved: 0 means "no buffer" in the host contract (HostFetch.putBuffer, a form's bufferId > 0).
2
+ const arrayBuffers: ArrayBuffer[] = [null as unknown as ArrayBuffer]
3
+
4
+ const decoder = new TextDecoder('utf-8');
5
+
6
+ export const _fetch = async (url: string, fetchOptions: any, onComplete: (systemId: number, statusCode: number) => void, onReject: (err: Error) => void) => {
7
+
8
+ let body: any = undefined
9
+ if (fetchOptions.body) {
10
+ if (typeof fetchOptions.body === "object" && Array.isArray(fetchOptions.body._entries)) {
11
+ const formData = new FormData()
12
+ for (let [key,value,filename] of fetchOptions.body._entries) {
13
+ if (typeof value === "object") {
14
+ formData.append(key, new Blob([ getBuffer(value._id) ]), filename)
15
+ } else {
16
+ formData.append(key, value)
17
+ }
18
+ }
19
+ body = formData
20
+ } else if (typeof fetchOptions.body === "string") {
21
+ body = fetchOptions.body
22
+ } else {
23
+ onReject(new Error("Invalid body"))
24
+ }
25
+ }
26
+
27
+ const resp = await fetch(url, {
28
+ method: fetchOptions.method,
29
+ body,
30
+ headers: fetchOptions.headers,
31
+ mode: "cors",
32
+ credentials: "omit"
33
+ })
34
+
35
+ const onProgress =
36
+ typeof fetchOptions.onProgress === 'function'
37
+ ? fetchOptions.onProgress
38
+ : null
39
+
40
+ // FAST PATH (без стрима)
41
+ if (!onProgress) {
42
+ const buffer = await resp.arrayBuffer()
43
+ arrayBuffers.push(buffer)
44
+ onComplete(arrayBuffers.length - 1, resp.status)
45
+ return
46
+ }
47
+
48
+ // STREAMING PATH
49
+ const reader = resp.body?.getReader()
50
+
51
+ // fallback если стрима нет
52
+ if (!reader) {
53
+ const buffer = await resp.arrayBuffer()
54
+ arrayBuffers.push(buffer)
55
+ onComplete(arrayBuffers.length - 1, resp.status)
56
+ return
57
+ }
58
+
59
+ const contentLength = Number(resp.headers.get("content-length")) || undefined
60
+
61
+ let loaded = 0
62
+ const chunks: Uint8Array[] = []
63
+
64
+ while (true) {
65
+ const { done, value } = await reader.read()
66
+ if (done) break
67
+
68
+ if (value) {
69
+ chunks.push(value)
70
+ loaded += value.length
71
+
72
+ onProgress({ loaded, total: contentLength })
73
+ }
74
+ }
75
+
76
+ // сборка
77
+ const result = new Uint8Array(loaded)
78
+ let offset = 0
79
+ for (const chunk of chunks) {
80
+ result.set(chunk, offset)
81
+ offset += chunk.length
82
+ }
83
+
84
+ arrayBuffers.push(result.buffer)
85
+ onComplete(arrayBuffers.length - 1, resp.status)
86
+ }
87
+
88
+ export const _fetchToJson = (systemId: number) => {
89
+ return JSON.parse(decoder.decode(getBuffer(systemId)))
90
+ }
91
+
92
+ export const _fetchToText = (systemId: number) => {
93
+ return decoder.decode(getBuffer(systemId))
94
+ }
95
+
96
+ /** The fetched bytes as a Uint8Array copy (Terrain.load and other SDK-parsed binaries). */
97
+ export const _fetchToBytes = (systemId: number): Uint8Array => {
98
+ return new Uint8Array(getBuffer(systemId)).slice()
99
+ }
100
+
101
+ export const _fetchSlice = (systemId: number, start: number, end: number) => {
102
+ return decoder.decode(getBuffer(systemId).slice(start, end))
103
+ }
104
+
105
+ export const getUrl = (systemId: number) => {
106
+ const blob = new Blob([ arrayBuffers[systemId] ])
107
+ return URL.createObjectURL(blob)
108
+ }
109
+
110
+ export const getBlob = (systemId: number) => {
111
+ return new Blob([ arrayBuffers[systemId] ])
112
+ }
113
+
114
+ export const getBuffer = (systemId: number) => {
115
+ return arrayBuffers[systemId]
116
+ }
117
+
118
+ export const _disposeFetch = (id: number) => {
119
+ arrayBuffers[id] = null as any
120
+ }
121
+
122
+ export const putBuffer = (buffer: ArrayBuffer) => {
123
+ arrayBuffers.push(buffer)
124
+ return arrayBuffers.length - 1
125
+ }
126
+
127
+ const map = new Map<string, ArrayBuffer>()
128
+ export const putLocalBuffer = (str: string, buf: ArrayBuffer) => {
129
+ map.set(str, buf)
130
+ // A name may already be materialized as a systemId (localResources caches forever). Refresh the
131
+ // buffer behind it, or a later engine sees the stale payload — e.g. the lite engine's empty
132
+ // built-in registrations must not shadow the real filamat the Filament engine loads on a
133
+ // lite → filament host switch.
134
+ const systemId = localResources.get(str)
135
+ if (systemId !== undefined) arrayBuffers[systemId] = buf
136
+ }
137
+
138
+ export const hasLocalBuffer = (str: string) => map.has(str)
139
+
140
+ const localResources = new Map<string, number>()
141
+
142
+ /** Reverse lookup: the local-resource name a systemId was materialized from (viewer-lite resolves
143
+ * material shaders — "lit.filamat" etc — by name instead of compiling the filamat package). */
144
+ export const getLocalName = (systemId: number): string | undefined => {
145
+ for (const [name, id] of localResources) if (id === systemId) return name
146
+ return undefined
147
+ }
148
+
149
+ export const _fetchLocal = (path: string): number => {
150
+ const existsSystemId = localResources.get(path)
151
+ if (existsSystemId !== undefined) {
152
+ return existsSystemId
153
+ }
154
+ const buf = map.get(path)
155
+ if (!buf) return -1
156
+
157
+ arrayBuffers.push(buf)
158
+ const systemId = arrayBuffers.length-1
159
+
160
+ localResources.set(path, systemId)
161
+ return systemId
162
+ }