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.
- package/package.json +22 -0
- package/src/assets/Roboto-OFL.txt +93 -0
- package/src/assets/Roboto-Variable.woff2 +0 -0
- package/src/canvas/createCanvasHost.ts +103 -0
- package/src/canvas/gen/drawlist.ts +138 -0
- package/src/canvas/gen/painter.ts +176 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/sourcemapError.ts +178 -0
- package/src/ui/nativeView.ts +12 -0
- package/src/ui/safeAreaSpec.ts +30 -0
- package/src/ui/viewScale.ts +19 -0
- package/src/utils/fetch.ts +162 -0
- package/src/utils/fonts.ts +88 -0
- package/src/utils/mediaplayer.ts +128 -0
- package/src/utils/svg.ts +61 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -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
|
+
}
|