moonscratch 0.1.1 → 0.1.2
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/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/index.d.mts +1173 -0
- package/dist/index.mjs +27135 -0
- package/package.json +6 -1
- package/.agents/skills/moonbit-agent-guide/LICENSE +0 -202
- package/.agents/skills/moonbit-agent-guide/SKILL.mbt.md +0 -1126
- package/.agents/skills/moonbit-agent-guide/SKILL.md +0 -1126
- package/.agents/skills/moonbit-agent-guide/ide.md +0 -116
- package/.agents/skills/moonbit-agent-guide/references/advanced-moonbit-build.md +0 -106
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.mbt.md +0 -422
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.md +0 -422
- package/.agents/skills/moonbit-practice/SKILL.md +0 -258
- package/.agents/skills/moonbit-practice/assets/ci.yaml +0 -25
- package/.agents/skills/moonbit-practice/reference/agents.md +0 -1469
- package/.agents/skills/moonbit-practice/reference/configuration.md +0 -228
- package/.agents/skills/moonbit-practice/reference/ffi.md +0 -229
- package/.agents/skills/moonbit-practice/reference/ide.md +0 -189
- package/.agents/skills/moonbit-practice/reference/performance.md +0 -217
- package/.agents/skills/moonbit-practice/reference/refactor.md +0 -154
- package/.agents/skills/moonbit-practice/reference/stdlib.md +0 -351
- package/.agents/skills/moonbit-practice/reference/testing.md +0 -228
- package/.agents/skills/moonbit-refactoring/LICENSE +0 -21
- package/.agents/skills/moonbit-refactoring/SKILL.md +0 -323
- package/.githooks/README.md +0 -23
- package/.githooks/pre-commit +0 -3
- package/.github/workflows/copilot-setup-steps.yml +0 -40
- package/AGENTS.md +0 -91
- package/PLAN.md +0 -64
- package/TODO.md +0 -120
- package/benchmarks/calc.bench.ts +0 -144
- package/benchmarks/draw.bench.ts +0 -215
- package/benchmarks/load.bench.ts +0 -28
- package/benchmarks/render.bench.ts +0 -53
- package/benchmarks/run.bench.ts +0 -8
- package/benchmarks/types.d.ts +0 -15
- package/docs/scratch-vm-specs/eventloop.md +0 -103
- package/docs/scratch-vm-specs/moonscratch-time-separation.md +0 -50
- package/index.html +0 -91
- package/js/AGENTS.md +0 -5
- package/js/a.ts +0 -52
- package/js/assets/AGENTS.md +0 -5
- package/js/assets/base64.test.ts +0 -14
- package/js/assets/base64.ts +0 -21
- package/js/assets/build-asset.test.ts +0 -26
- package/js/assets/build-asset.ts +0 -28
- package/js/assets/create.test.ts +0 -142
- package/js/assets/create.ts +0 -122
- package/js/assets/index.test.ts +0 -15
- package/js/assets/index.ts +0 -2
- package/js/assets/types.ts +0 -26
- package/js/assets/validation.test.ts +0 -34
- package/js/assets/validation.ts +0 -25
- package/js/assets.test.ts +0 -14
- package/js/assets.ts +0 -1
- package/js/index.test.ts +0 -26
- package/js/index.ts +0 -3
- package/js/render/index.test.ts +0 -65
- package/js/render/index.ts +0 -13
- package/js/render/sharp.ts +0 -87
- package/js/render/svg.ts +0 -68
- package/js/render/types.ts +0 -35
- package/js/render/utils.ts +0 -108
- package/js/render/webgl.ts +0 -274
- package/js/sharp-optional.d.ts +0 -16
- package/js/test/helpers.ts +0 -116
- package/js/test/hikkaku-sample.test.ts +0 -37
- package/js/test/rubik-components.input-motion.test.ts +0 -60
- package/js/test/rubik-components.lists.test.ts +0 -49
- package/js/test/rubik-components.operators.test.ts +0 -104
- package/js/test/rubik-components.pen.test.ts +0 -112
- package/js/test/rubik-components.procedures-loops.test.ts +0 -72
- package/js/test/rubik-components.variables-branches.test.ts +0 -57
- package/js/test/rubik-components.visibility-entry.test.ts +0 -31
- package/js/test/test-projects.ts +0 -598
- package/js/test/variable.ts +0 -200
- package/js/test/warp.test.ts +0 -59
- package/js/vm/AGENTS.md +0 -6
- package/js/vm/README.md +0 -183
- package/js/vm/bindings.test.ts +0 -13
- package/js/vm/bindings.ts +0 -5
- package/js/vm/compare-operators.test.ts +0 -145
- package/js/vm/constants.test.ts +0 -11
- package/js/vm/constants.ts +0 -4
- package/js/vm/effect-guards.test.ts +0 -68
- package/js/vm/effect-guards.ts +0 -44
- package/js/vm/factory.test.ts +0 -486
- package/js/vm/factory.ts +0 -615
- package/js/vm/headless-vm.test.ts +0 -131
- package/js/vm/headless-vm.ts +0 -342
- package/js/vm/index.test.ts +0 -28
- package/js/vm/index.ts +0 -5
- package/js/vm/internal-types.ts +0 -32
- package/js/vm/json.test.ts +0 -40
- package/js/vm/json.ts +0 -273
- package/js/vm/normalize.test.ts +0 -48
- package/js/vm/normalize.ts +0 -65
- package/js/vm/options.test.ts +0 -30
- package/js/vm/options.ts +0 -55
- package/js/vm/pen-transparency.test.ts +0 -115
- package/js/vm/program-wasm.ts +0 -322
- package/js/vm/scheduler-render.test.ts +0 -401
- package/js/vm/scratch-assets.test.ts +0 -136
- package/js/vm/scratch-assets.ts +0 -202
- package/js/vm/types.ts +0 -358
- package/js/vm/value-guards.test.ts +0 -25
- package/js/vm/value-guards.ts +0 -18
- package/moon.mod.json +0 -10
- package/scripts/preinstall.ts +0 -4
- package/src/AGENTS.md +0 -6
- package/src/api.mbt +0 -161
- package/src/api_aot_commands.mbt +0 -184
- package/src/api_effects_json.mbt +0 -72
- package/src/api_options.mbt +0 -60
- package/src/api_program_wasm.mbt +0 -1647
- package/src/api_program_wat.mbt +0 -2206
- package/src/api_snapshot_json.mbt +0 -44
- package/src/cmd/AGENTS.md +0 -5
- package/src/cmd/main/AGENTS.md +0 -5
- package/src/cmd/main/main.mbt +0 -29
- package/src/cmd/main/moon.pkg +0 -7
- package/src/cmd/main/pkg.generated.mbti +0 -13
- package/src/json_helpers.mbt +0 -176
- package/src/moon.pkg +0 -65
- package/src/moonscratch.mbt +0 -3
- package/src/moonscratch_wbtest.mbt +0 -40
- package/src/parser_sb3.mbt +0 -890
- package/src/pkg.generated.mbti +0 -479
- package/src/runtime_eval.mbt +0 -2844
- package/src/runtime_exec.mbt +0 -3850
- package/src/runtime_render.mbt +0 -2550
- package/src/runtime_state.mbt +0 -870
- package/src/test/AGENTS.md +0 -3
- package/src/test/projects/AGENTS.md +0 -6
- package/src/test/projects/moon.pkg +0 -4
- package/src/test/projects/moonscratch_compat_test.mbt +0 -642
- package/src/test/projects/moonscratch_core_test.mbt +0 -1332
- package/src/test/projects/moonscratch_runtime_test.mbt +0 -1087
- package/src/test/projects/pkg.generated.mbti +0 -13
- package/src/test/projects/test_support.mbt +0 -35
- package/src/types_effects.mbt +0 -20
- package/src/types_error.mbt +0 -4
- package/src/types_options.mbt +0 -31
- package/src/types_runtime_structs.mbt +0 -254
- package/src/types_vm.mbt +0 -109
- package/tsconfig.json +0 -29
- package/viewer/index.ts +0 -399
- package/viewer/vite.d.ts +0 -1
- package/viewer/worker.ts +0 -161
- package/vite.config.ts +0 -61
package/js/vm/json.ts
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import type { MoonResult } from './internal-types.ts'
|
|
2
|
-
import type { JsonValue, ProjectJson } from './types.ts'
|
|
3
|
-
|
|
4
|
-
const isRecord = (input: unknown): input is Record<string, unknown> => {
|
|
5
|
-
return input !== null && typeof input === 'object' && !Array.isArray(input)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const hasAnyKey = (input: Record<string, unknown>): boolean => {
|
|
9
|
-
for (const _key in input) {
|
|
10
|
-
return true
|
|
11
|
-
}
|
|
12
|
-
return false
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const compactCostumes = (input: unknown): unknown[] => {
|
|
16
|
-
if (!Array.isArray(input)) {
|
|
17
|
-
return []
|
|
18
|
-
}
|
|
19
|
-
const costumes: unknown[] = []
|
|
20
|
-
for (const raw of input) {
|
|
21
|
-
if (!isRecord(raw)) {
|
|
22
|
-
costumes.push({})
|
|
23
|
-
continue
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const costume: Record<string, unknown> = {}
|
|
27
|
-
if (typeof raw.name === 'string') {
|
|
28
|
-
costume.name = raw.name
|
|
29
|
-
}
|
|
30
|
-
if (typeof raw.assetId === 'string' && raw.assetId.length > 0) {
|
|
31
|
-
costume.assetId = raw.assetId
|
|
32
|
-
}
|
|
33
|
-
if (typeof raw.md5ext === 'string' && raw.md5ext.length > 0) {
|
|
34
|
-
costume.md5ext = raw.md5ext
|
|
35
|
-
}
|
|
36
|
-
if (
|
|
37
|
-
typeof raw.bitmapResolution === 'number' &&
|
|
38
|
-
Number.isFinite(raw.bitmapResolution) &&
|
|
39
|
-
raw.bitmapResolution !== 1
|
|
40
|
-
) {
|
|
41
|
-
costume.bitmapResolution = raw.bitmapResolution
|
|
42
|
-
}
|
|
43
|
-
if (
|
|
44
|
-
typeof raw.rotationCenterX === 'number' &&
|
|
45
|
-
Number.isFinite(raw.rotationCenterX) &&
|
|
46
|
-
raw.rotationCenterX !== 0
|
|
47
|
-
) {
|
|
48
|
-
costume.rotationCenterX = raw.rotationCenterX
|
|
49
|
-
}
|
|
50
|
-
if (
|
|
51
|
-
typeof raw.rotationCenterY === 'number' &&
|
|
52
|
-
Number.isFinite(raw.rotationCenterY) &&
|
|
53
|
-
raw.rotationCenterY !== 0
|
|
54
|
-
) {
|
|
55
|
-
costume.rotationCenterY = raw.rotationCenterY
|
|
56
|
-
}
|
|
57
|
-
if (
|
|
58
|
-
typeof raw.width === 'number' &&
|
|
59
|
-
Number.isFinite(raw.width) &&
|
|
60
|
-
raw.width > 0
|
|
61
|
-
) {
|
|
62
|
-
costume.width = raw.width
|
|
63
|
-
}
|
|
64
|
-
if (
|
|
65
|
-
typeof raw.height === 'number' &&
|
|
66
|
-
Number.isFinite(raw.height) &&
|
|
67
|
-
raw.height > 0
|
|
68
|
-
) {
|
|
69
|
-
costume.height = raw.height
|
|
70
|
-
}
|
|
71
|
-
costumes.push(costume)
|
|
72
|
-
}
|
|
73
|
-
return costumes
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const compactBlocks = (input: unknown): Record<string, unknown> => {
|
|
77
|
-
if (!isRecord(input)) {
|
|
78
|
-
return {}
|
|
79
|
-
}
|
|
80
|
-
const blocks: Record<string, unknown> = {}
|
|
81
|
-
for (const id in input) {
|
|
82
|
-
const raw = input[id]
|
|
83
|
-
if (!isRecord(raw)) {
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const block: Record<string, unknown> = {}
|
|
88
|
-
if (typeof raw.opcode === 'string') {
|
|
89
|
-
block.opcode = raw.opcode
|
|
90
|
-
}
|
|
91
|
-
if (typeof raw.next === 'string') {
|
|
92
|
-
block.next = raw.next
|
|
93
|
-
}
|
|
94
|
-
if (typeof raw.parent === 'string') {
|
|
95
|
-
block.parent = raw.parent
|
|
96
|
-
}
|
|
97
|
-
if (isRecord(raw.inputs) && hasAnyKey(raw.inputs)) {
|
|
98
|
-
block.inputs = raw.inputs
|
|
99
|
-
}
|
|
100
|
-
if (isRecord(raw.fields) && hasAnyKey(raw.fields)) {
|
|
101
|
-
block.fields = raw.fields
|
|
102
|
-
}
|
|
103
|
-
if (isRecord(raw.mutation) && hasAnyKey(raw.mutation)) {
|
|
104
|
-
block.mutation = raw.mutation
|
|
105
|
-
}
|
|
106
|
-
if (raw.topLevel === true) {
|
|
107
|
-
block.topLevel = true
|
|
108
|
-
}
|
|
109
|
-
blocks[id] = block
|
|
110
|
-
}
|
|
111
|
-
return blocks
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const compactTargets = (input: unknown): unknown[] => {
|
|
115
|
-
if (!Array.isArray(input)) {
|
|
116
|
-
return []
|
|
117
|
-
}
|
|
118
|
-
const targets: unknown[] = []
|
|
119
|
-
for (const raw of input) {
|
|
120
|
-
if (!isRecord(raw)) {
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const target: Record<string, unknown> = {}
|
|
125
|
-
if (typeof raw.name === 'string') {
|
|
126
|
-
target.name = raw.name
|
|
127
|
-
}
|
|
128
|
-
if (raw.isStage === true) {
|
|
129
|
-
target.isStage = true
|
|
130
|
-
}
|
|
131
|
-
if (typeof raw.x === 'number' && Number.isFinite(raw.x) && raw.x !== 0) {
|
|
132
|
-
target.x = raw.x
|
|
133
|
-
}
|
|
134
|
-
if (typeof raw.y === 'number' && Number.isFinite(raw.y) && raw.y !== 0) {
|
|
135
|
-
target.y = raw.y
|
|
136
|
-
}
|
|
137
|
-
if (
|
|
138
|
-
typeof raw.direction === 'number' &&
|
|
139
|
-
Number.isFinite(raw.direction) &&
|
|
140
|
-
raw.direction !== 90
|
|
141
|
-
) {
|
|
142
|
-
target.direction = raw.direction
|
|
143
|
-
}
|
|
144
|
-
if (
|
|
145
|
-
typeof raw.size === 'number' &&
|
|
146
|
-
Number.isFinite(raw.size) &&
|
|
147
|
-
raw.size !== 100
|
|
148
|
-
) {
|
|
149
|
-
target.size = raw.size
|
|
150
|
-
}
|
|
151
|
-
if (
|
|
152
|
-
typeof raw.volume === 'number' &&
|
|
153
|
-
Number.isFinite(raw.volume) &&
|
|
154
|
-
raw.volume !== 100
|
|
155
|
-
) {
|
|
156
|
-
target.volume = raw.volume
|
|
157
|
-
}
|
|
158
|
-
if (
|
|
159
|
-
typeof raw.musicInstrument === 'number' &&
|
|
160
|
-
Number.isFinite(raw.musicInstrument) &&
|
|
161
|
-
raw.musicInstrument !== 1
|
|
162
|
-
) {
|
|
163
|
-
target.musicInstrument = raw.musicInstrument
|
|
164
|
-
}
|
|
165
|
-
if (
|
|
166
|
-
typeof raw.textToSpeechVoice === 'string' &&
|
|
167
|
-
raw.textToSpeechVoice.length > 0 &&
|
|
168
|
-
raw.textToSpeechVoice !== 'ALTO'
|
|
169
|
-
) {
|
|
170
|
-
target.textToSpeechVoice = raw.textToSpeechVoice
|
|
171
|
-
}
|
|
172
|
-
if (raw.visible === false) {
|
|
173
|
-
target.visible = false
|
|
174
|
-
}
|
|
175
|
-
if (
|
|
176
|
-
typeof raw.currentCostume === 'number' &&
|
|
177
|
-
Number.isFinite(raw.currentCostume) &&
|
|
178
|
-
raw.currentCostume !== 0
|
|
179
|
-
) {
|
|
180
|
-
target.currentCostume = raw.currentCostume
|
|
181
|
-
}
|
|
182
|
-
target.variables = isRecord(raw.variables) ? raw.variables : {}
|
|
183
|
-
target.lists = isRecord(raw.lists) ? raw.lists : {}
|
|
184
|
-
target.blocks = compactBlocks(raw.blocks)
|
|
185
|
-
target.costumes = compactCostumes(raw.costumes)
|
|
186
|
-
targets.push(target)
|
|
187
|
-
}
|
|
188
|
-
return targets
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export const toProjectJsonString = (input: string | ProjectJson): string => {
|
|
192
|
-
if (typeof input === 'string') {
|
|
193
|
-
if (input.trim().length === 0) {
|
|
194
|
-
throw new Error('projectJson must be a non-empty JSON string or object')
|
|
195
|
-
}
|
|
196
|
-
return input
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
if (!isRecord(input) || !Array.isArray(input.targets)) {
|
|
201
|
-
return JSON.stringify(input)
|
|
202
|
-
}
|
|
203
|
-
return JSON.stringify({
|
|
204
|
-
targets: compactTargets(input.targets),
|
|
205
|
-
})
|
|
206
|
-
} catch (error) {
|
|
207
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
208
|
-
throw new Error(`projectJson could not be serialized as JSON: ${message}`)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export const toJsonString = (
|
|
213
|
-
input: string | ProjectJson,
|
|
214
|
-
inputName: string,
|
|
215
|
-
requireNonEmpty: boolean,
|
|
216
|
-
): string => {
|
|
217
|
-
if (typeof input === 'string') {
|
|
218
|
-
if (requireNonEmpty && input.trim().length === 0) {
|
|
219
|
-
throw new Error(`${inputName} must be a non-empty JSON string or object`)
|
|
220
|
-
}
|
|
221
|
-
return input
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
return JSON.stringify(input)
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
228
|
-
throw new Error(`${inputName} could not be serialized as JSON: ${message}`)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export const toOptionalJsonString = (
|
|
233
|
-
input: string | JsonValue | undefined,
|
|
234
|
-
inputName: string,
|
|
235
|
-
): string | undefined => {
|
|
236
|
-
if (input === undefined) {
|
|
237
|
-
return undefined
|
|
238
|
-
}
|
|
239
|
-
return toJsonString(input, inputName, false)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const formatVmError = (error: unknown): string => {
|
|
243
|
-
if (error && typeof error === 'object') {
|
|
244
|
-
const candidate = (error as { _0?: unknown })._0
|
|
245
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
246
|
-
return candidate
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
return JSON.stringify(error)
|
|
251
|
-
} catch {
|
|
252
|
-
return String(error)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export const unwrapResult = <T>(
|
|
257
|
-
result: MoonResult<T, unknown>,
|
|
258
|
-
context: string,
|
|
259
|
-
): T => {
|
|
260
|
-
if (result.$tag === 1) {
|
|
261
|
-
return result._0
|
|
262
|
-
}
|
|
263
|
-
throw new Error(`${context}: ${formatVmError(result._0)}`)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export const parseJson = <T>(text: string, context: string): T => {
|
|
267
|
-
try {
|
|
268
|
-
return JSON.parse(text) as T
|
|
269
|
-
} catch (error) {
|
|
270
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
271
|
-
throw new Error(`${context}: failed to parse JSON (${message})`)
|
|
272
|
-
}
|
|
273
|
-
}
|
package/js/vm/normalize.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
cloneTranslateCache,
|
|
5
|
-
normalizeLanguage,
|
|
6
|
-
normalizeMaxFrames,
|
|
7
|
-
normalizeNowMs,
|
|
8
|
-
toFrameReport,
|
|
9
|
-
} from './normalize.ts'
|
|
10
|
-
|
|
11
|
-
describe('moonscratch/js/vm/normalize.ts', () => {
|
|
12
|
-
test('normalizes language code', () => {
|
|
13
|
-
expect(normalizeLanguage(' JA ')).toBe('ja')
|
|
14
|
-
expect(normalizeLanguage('')).toBe('en')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('clones and normalizes translate cache', () => {
|
|
18
|
-
const cache = cloneTranslateCache({ JA: { hello: 'こんにちは' } })
|
|
19
|
-
expect(cache).toEqual({ ja: { hello: 'こんにちは' } })
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('normalizes frame inputs', () => {
|
|
23
|
-
expect(normalizeNowMs(16.9)).toBe(16)
|
|
24
|
-
expect(normalizeMaxFrames(10.7)).toBe(10)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('maps raw frame report fields', () => {
|
|
28
|
-
expect(
|
|
29
|
-
toFrameReport({
|
|
30
|
-
active_threads: 2,
|
|
31
|
-
tick_count: 4,
|
|
32
|
-
op_count: 100,
|
|
33
|
-
emitted_effects: 3,
|
|
34
|
-
stop_reason: 'timeout',
|
|
35
|
-
should_render: true,
|
|
36
|
-
is_in_warp: false,
|
|
37
|
-
}),
|
|
38
|
-
).toEqual({
|
|
39
|
-
activeThreads: 2,
|
|
40
|
-
ticks: 4,
|
|
41
|
-
ops: 100,
|
|
42
|
-
emittedEffects: 3,
|
|
43
|
-
stopReason: 'timeout',
|
|
44
|
-
shouldRender: true,
|
|
45
|
-
isInWarp: false,
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
})
|
package/js/vm/normalize.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_LANGUAGE } from './constants.ts'
|
|
2
|
-
import type { RawFrameReport } from './internal-types.ts'
|
|
3
|
-
import type { FrameReport, FrameStopReason, TranslateCache } from './types.ts'
|
|
4
|
-
|
|
5
|
-
export const normalizeLanguage = (language: unknown): string =>
|
|
6
|
-
String(language ?? '')
|
|
7
|
-
.trim()
|
|
8
|
-
.toLowerCase() || DEFAULT_LANGUAGE
|
|
9
|
-
|
|
10
|
-
export const cloneTranslateCache = (
|
|
11
|
-
cache: TranslateCache | undefined,
|
|
12
|
-
): TranslateCache => {
|
|
13
|
-
const out: TranslateCache = {}
|
|
14
|
-
for (const [language, bucket] of Object.entries(cache ?? {})) {
|
|
15
|
-
if (!bucket || typeof bucket !== 'object' || Array.isArray(bucket)) {
|
|
16
|
-
continue
|
|
17
|
-
}
|
|
18
|
-
const normalizedLanguage = normalizeLanguage(language)
|
|
19
|
-
out[normalizedLanguage] = {}
|
|
20
|
-
for (const [words, translated] of Object.entries(bucket)) {
|
|
21
|
-
out[normalizedLanguage][String(words)] = String(translated)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return out
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const normalizeNowMs = (nowMs: number): number => {
|
|
28
|
-
if (!Number.isFinite(nowMs)) {
|
|
29
|
-
throw new Error('nowMs must be a finite number')
|
|
30
|
-
}
|
|
31
|
-
return Math.trunc(nowMs)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const normalizeMaxFrames = (maxFrames: number): number => {
|
|
35
|
-
if (!Number.isFinite(maxFrames)) {
|
|
36
|
-
throw new Error('maxFrames must be a finite number')
|
|
37
|
-
}
|
|
38
|
-
const out = Math.trunc(maxFrames)
|
|
39
|
-
if (out <= 0) {
|
|
40
|
-
throw new Error('maxFrames must be greater than 0')
|
|
41
|
-
}
|
|
42
|
-
return out
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const toFrameStopReason = (reason: string): FrameStopReason => {
|
|
46
|
-
if (
|
|
47
|
-
reason === 'finished' ||
|
|
48
|
-
reason === 'timeout' ||
|
|
49
|
-
reason === 'rerender' ||
|
|
50
|
-
reason === 'warp-exit'
|
|
51
|
-
) {
|
|
52
|
-
return reason
|
|
53
|
-
}
|
|
54
|
-
return 'timeout'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const toFrameReport = (report: RawFrameReport): FrameReport => ({
|
|
58
|
-
activeThreads: report.active_threads,
|
|
59
|
-
ticks: report.tick_count,
|
|
60
|
-
ops: report.op_count,
|
|
61
|
-
emittedEffects: report.emitted_effects,
|
|
62
|
-
stopReason: toFrameStopReason(report.stop_reason),
|
|
63
|
-
shouldRender: report.should_render,
|
|
64
|
-
isInWarp: report.is_in_warp,
|
|
65
|
-
})
|
package/js/vm/options.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import { toOptionsJson } from './options.ts'
|
|
4
|
-
|
|
5
|
-
describe('moonscratch/js/vm/options.ts', () => {
|
|
6
|
-
test('returns undefined when options are omitted', () => {
|
|
7
|
-
expect(toOptionsJson(undefined)).toBeUndefined()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
test('passes through raw options JSON strings', () => {
|
|
11
|
-
expect(toOptionsJson('{"turbo":true}')).toBe('{"turbo":true}')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('maps camelCase and snake_case fields to raw options', () => {
|
|
15
|
-
const json = toOptionsJson({
|
|
16
|
-
turbo: true,
|
|
17
|
-
compatibility30tps: true,
|
|
18
|
-
max_clones: 123,
|
|
19
|
-
deterministic: true,
|
|
20
|
-
seed: 42,
|
|
21
|
-
penWidth: 480,
|
|
22
|
-
pen_height: 360,
|
|
23
|
-
stepTimeoutTicks: 2048,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
expect(json).toBe(
|
|
27
|
-
'{"turbo":true,"compatibility_30tps":true,"max_clones":123,"deterministic":true,"seed":42,"pen_width":480,"pen_height":360,"step_timeout_ticks":2048}',
|
|
28
|
-
)
|
|
29
|
-
})
|
|
30
|
-
})
|
package/js/vm/options.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { RawVMOptions } from './internal-types.ts'
|
|
2
|
-
import type { VMOptionsInput } from './types.ts'
|
|
3
|
-
|
|
4
|
-
export const toOptionsJson = (
|
|
5
|
-
options: string | VMOptionsInput | undefined,
|
|
6
|
-
): string | undefined => {
|
|
7
|
-
if (options === undefined) {
|
|
8
|
-
return undefined
|
|
9
|
-
}
|
|
10
|
-
if (typeof options === 'string') {
|
|
11
|
-
return options
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const raw: RawVMOptions = {}
|
|
15
|
-
if (options.turbo !== undefined) {
|
|
16
|
-
raw.turbo = options.turbo
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const compatibility30tps =
|
|
20
|
-
options.compatibility30tps ?? options.compatibility_30tps
|
|
21
|
-
if (compatibility30tps !== undefined) {
|
|
22
|
-
raw.compatibility_30tps = compatibility30tps
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const maxClones = options.maxClones ?? options.max_clones
|
|
26
|
-
if (maxClones !== undefined) {
|
|
27
|
-
raw.max_clones = maxClones
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (options.deterministic !== undefined) {
|
|
31
|
-
raw.deterministic = options.deterministic
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (options.seed !== undefined) {
|
|
35
|
-
raw.seed = options.seed
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const penWidth = options.penWidth ?? options.pen_width
|
|
39
|
-
if (penWidth !== undefined) {
|
|
40
|
-
raw.pen_width = penWidth
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const penHeight = options.penHeight ?? options.pen_height
|
|
44
|
-
if (penHeight !== undefined) {
|
|
45
|
-
raw.pen_height = penHeight
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const stepTimeoutTicks =
|
|
49
|
-
options.stepTimeoutTicks ?? options.step_timeout_ticks
|
|
50
|
-
if (stepTimeoutTicks !== undefined) {
|
|
51
|
-
raw.step_timeout_ticks = stepTimeoutTicks
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return JSON.stringify(raw)
|
|
55
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { Project } from 'hikkaku'
|
|
2
|
-
import {
|
|
3
|
-
clear,
|
|
4
|
-
gotoXY,
|
|
5
|
-
penDown,
|
|
6
|
-
penUp,
|
|
7
|
-
setPenColorParamTo,
|
|
8
|
-
setPenColorToColor,
|
|
9
|
-
setPenSizeTo,
|
|
10
|
-
whenFlagClicked,
|
|
11
|
-
} from 'hikkaku/blocks'
|
|
12
|
-
import { describe, expect, test } from 'vite-plus/test'
|
|
13
|
-
import { stepMany } from '../test/test-projects.ts'
|
|
14
|
-
import { createHeadlessVM, createProgramModuleFromProject } from './factory.ts'
|
|
15
|
-
|
|
16
|
-
describe('moonscratch/js/vm pen transparency', () => {
|
|
17
|
-
test('does not overdraw alpha by repeatedly blending the same stroke area', () => {
|
|
18
|
-
const project = new Project()
|
|
19
|
-
const sprite = project.createSprite('pen-sprite')
|
|
20
|
-
|
|
21
|
-
sprite.run(() => {
|
|
22
|
-
whenFlagClicked(() => {
|
|
23
|
-
clear()
|
|
24
|
-
setPenSizeTo(6)
|
|
25
|
-
|
|
26
|
-
setPenColorToColor('#ff0000')
|
|
27
|
-
setPenColorParamTo('transparency', 0)
|
|
28
|
-
penDown()
|
|
29
|
-
gotoXY(-100, 0)
|
|
30
|
-
gotoXY(100, 0)
|
|
31
|
-
penUp()
|
|
32
|
-
|
|
33
|
-
setPenColorToColor('#0000ff')
|
|
34
|
-
setPenColorParamTo('transparency', 70)
|
|
35
|
-
penDown()
|
|
36
|
-
gotoXY(-100, 0)
|
|
37
|
-
gotoXY(100, 0)
|
|
38
|
-
penUp()
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const program = createProgramModuleFromProject({
|
|
43
|
-
projectJson: project.toScratch(),
|
|
44
|
-
})
|
|
45
|
-
const vm = createHeadlessVM({
|
|
46
|
-
program,
|
|
47
|
-
initialNowMs: 0,
|
|
48
|
-
})
|
|
49
|
-
vm.greenFlag()
|
|
50
|
-
stepMany(vm, 8)
|
|
51
|
-
|
|
52
|
-
const frame = vm.renderFrame()
|
|
53
|
-
const centerX = Math.floor(frame.width / 2)
|
|
54
|
-
const centerY = Math.floor(frame.height / 2)
|
|
55
|
-
const base = (centerY * frame.width + centerX) * 4
|
|
56
|
-
const r = frame.pixels[base] ?? 0
|
|
57
|
-
const g = frame.pixels[base + 1] ?? 0
|
|
58
|
-
const b = frame.pixels[base + 2] ?? 0
|
|
59
|
-
const a = frame.pixels[base + 3] ?? 0
|
|
60
|
-
|
|
61
|
-
// Blue over red with transparency should stay mixed, not collapse to near-solid blue.
|
|
62
|
-
expect(a).toBe(255)
|
|
63
|
-
expect(g).toBeLessThan(16)
|
|
64
|
-
expect(r).toBeGreaterThan(70)
|
|
65
|
-
expect(b).toBeGreaterThan(70)
|
|
66
|
-
expect(r).toBeLessThan(220)
|
|
67
|
-
expect(b).toBeLessThan(220)
|
|
68
|
-
expect(Math.abs(r - b)).toBeLessThan(90)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('keeps subpixel coverage on diagonal size-1 pen lines', () => {
|
|
72
|
-
const project = new Project()
|
|
73
|
-
const sprite = project.createSprite('pen-sprite')
|
|
74
|
-
|
|
75
|
-
sprite.run(() => {
|
|
76
|
-
whenFlagClicked(() => {
|
|
77
|
-
clear()
|
|
78
|
-
setPenSizeTo(1)
|
|
79
|
-
setPenColorToColor('#00ff00')
|
|
80
|
-
setPenColorParamTo('transparency', 0)
|
|
81
|
-
penDown()
|
|
82
|
-
gotoXY(-120, -80)
|
|
83
|
-
gotoXY(120, 80)
|
|
84
|
-
penUp()
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const program = createProgramModuleFromProject({
|
|
89
|
-
projectJson: project.toScratch(),
|
|
90
|
-
})
|
|
91
|
-
const vm = createHeadlessVM({
|
|
92
|
-
program,
|
|
93
|
-
initialNowMs: 0,
|
|
94
|
-
})
|
|
95
|
-
vm.greenFlag()
|
|
96
|
-
stepMany(vm, 8)
|
|
97
|
-
|
|
98
|
-
const frame = vm.renderFrame()
|
|
99
|
-
let pureGreen = 0
|
|
100
|
-
let blendedGreen = 0
|
|
101
|
-
for (let index = 0; index < frame.pixels.length; index += 4) {
|
|
102
|
-
const r = frame.pixels[index] ?? 0
|
|
103
|
-
const g = frame.pixels[index + 1] ?? 0
|
|
104
|
-
const b = frame.pixels[index + 2] ?? 0
|
|
105
|
-
if (r === 0 && g === 255 && b === 0) {
|
|
106
|
-
pureGreen += 1
|
|
107
|
-
} else if (g === 255 && r === b && r > 0 && r < 255) {
|
|
108
|
-
blendedGreen += 1
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
expect(pureGreen).toBeGreaterThan(0)
|
|
113
|
-
expect(blendedGreen).toBeGreaterThan(0)
|
|
114
|
-
})
|
|
115
|
-
})
|