hikkaku 0.1.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 +25 -0
- package/src/blocks/control.ts +169 -0
- package/src/blocks/data.ts +203 -0
- package/src/blocks/events.ts +116 -0
- package/src/blocks/index.ts +9 -0
- package/src/blocks/looks.ts +189 -0
- package/src/blocks/motion.ts +146 -0
- package/src/blocks/operator.ts +219 -0
- package/src/blocks/procedures.ts +208 -0
- package/src/blocks/sensing.ts +129 -0
- package/src/blocks/sound.ts +77 -0
- package/src/client/fiber.ts +110 -0
- package/src/client/index.ts +26 -0
- package/src/client/types.ts +7 -0
- package/src/compiler/block-helper.ts +49 -0
- package/src/compiler/composer.ts +177 -0
- package/src/compiler/index.ts +6 -0
- package/src/compiler/project.ts +138 -0
- package/src/compiler/types.ts +37 -0
- package/src/index.ts +5 -0
- package/src/utils/assets.ts +4 -0
- package/src/vite/env.ts +46 -0
- package/src/vite/index.ts +77 -0
- package/tsconfig.json +3 -0
- package/tsdown.config.ts +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { fromPrimitiveSource, fromSoundSource } from '../compiler/block-helper'
|
|
2
|
+
import { block, valueBlock } from '../compiler/composer'
|
|
3
|
+
import type { PrimitiveSource, SoundSource } from '../compiler/types'
|
|
4
|
+
|
|
5
|
+
export type SoundEffect = 'pitch' | 'pan'
|
|
6
|
+
|
|
7
|
+
export const playSound = (sound: SoundSource) => {
|
|
8
|
+
return block('sound_play', {
|
|
9
|
+
inputs: {
|
|
10
|
+
SOUND_MENU: fromSoundSource(sound),
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const playSoundUntilDone = (sound: SoundSource) => {
|
|
16
|
+
return block('sound_playuntildone', {
|
|
17
|
+
inputs: {
|
|
18
|
+
SOUND_MENU: fromSoundSource(sound),
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const stopAllSounds = () => {
|
|
24
|
+
return block('sound_stopallsounds', {})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const setSoundEffectTo = (
|
|
28
|
+
effect: SoundEffect,
|
|
29
|
+
value: PrimitiveSource<number>,
|
|
30
|
+
) => {
|
|
31
|
+
return block('sound_seteffectto', {
|
|
32
|
+
inputs: {
|
|
33
|
+
VALUE: fromPrimitiveSource(value),
|
|
34
|
+
},
|
|
35
|
+
fields: {
|
|
36
|
+
EFFECT: [effect, null],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const changeSoundEffectBy = (
|
|
42
|
+
effect: SoundEffect,
|
|
43
|
+
value: PrimitiveSource<number>,
|
|
44
|
+
) => {
|
|
45
|
+
return block('sound_changeeffectby', {
|
|
46
|
+
inputs: {
|
|
47
|
+
VALUE: fromPrimitiveSource(value),
|
|
48
|
+
},
|
|
49
|
+
fields: {
|
|
50
|
+
EFFECT: [effect, null],
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const clearEffects = () => {
|
|
56
|
+
return block('sound_cleareffects', {})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const setVolumeTo = (value: PrimitiveSource<number>) => {
|
|
60
|
+
return block('sound_setvolumeto', {
|
|
61
|
+
inputs: {
|
|
62
|
+
VOLUME: fromPrimitiveSource(value),
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const changeVolumeBy = (value: PrimitiveSource<number>) => {
|
|
68
|
+
return block('sound_changevolumeby', {
|
|
69
|
+
inputs: {
|
|
70
|
+
VOLUME: fromPrimitiveSource(value),
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const getVolume = () => {
|
|
76
|
+
return valueBlock('sound_volume', {})
|
|
77
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ScratchVM } from './types'
|
|
2
|
+
|
|
3
|
+
export const findDOMAppRoot = () => {
|
|
4
|
+
const probably = [
|
|
5
|
+
document.getElementById('app'), // Scratch WWW: #app is the root element
|
|
6
|
+
document.querySelector('[class^=index_app]'), // Playground code of Scratch GUI
|
|
7
|
+
]
|
|
8
|
+
for (const el of probably) {
|
|
9
|
+
if (el && '_reactRootContainer' in el) {
|
|
10
|
+
return el as ScratchRoot
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
throw new Error(
|
|
15
|
+
'Could not find root DOM node. Make sure you are running this in a Scratch environment.',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FiberNode {
|
|
20
|
+
child: ScratchAppFiberNode | FiberNode | null
|
|
21
|
+
sibling: ScratchAppFiberNode | FiberNode | null
|
|
22
|
+
type: string | null
|
|
23
|
+
elementType?: {
|
|
24
|
+
propTypes?: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
interface ScratchGUIReduxStoreType {
|
|
28
|
+
vm: ScratchVM
|
|
29
|
+
}
|
|
30
|
+
interface ScratchInternalReduxStoreType {
|
|
31
|
+
scratchGui: ScratchGUIReduxStoreType
|
|
32
|
+
}
|
|
33
|
+
interface ScratchAppFiberNode extends FiberNode {
|
|
34
|
+
memoizedProps: {
|
|
35
|
+
store: {
|
|
36
|
+
getState: () => ScratchInternalReduxStoreType
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
type ScratchRoot = {
|
|
41
|
+
_reactRootContainer: {
|
|
42
|
+
_internalRoot: {
|
|
43
|
+
current: FiberNode
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} & Element
|
|
47
|
+
|
|
48
|
+
export function getSpecifiedFiber(
|
|
49
|
+
root: FiberNode,
|
|
50
|
+
cond: (fiber: FiberNode) => boolean,
|
|
51
|
+
) {
|
|
52
|
+
const stack = [root]
|
|
53
|
+
while (true) {
|
|
54
|
+
const fiber = stack.pop()
|
|
55
|
+
if (!fiber) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cond(fiber)) {
|
|
60
|
+
return fiber
|
|
61
|
+
}
|
|
62
|
+
if (fiber.child) {
|
|
63
|
+
stack.push(fiber.child)
|
|
64
|
+
}
|
|
65
|
+
if (fiber.sibling) {
|
|
66
|
+
stack.push(fiber.sibling)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const getAppFiberNode = (root: FiberNode): ScratchAppFiberNode => {
|
|
72
|
+
let cur = root.child
|
|
73
|
+
while (cur) {
|
|
74
|
+
if ('memoizedProps' in cur && 'store' in cur.memoizedProps) {
|
|
75
|
+
return cur
|
|
76
|
+
}
|
|
77
|
+
cur = cur.child
|
|
78
|
+
}
|
|
79
|
+
throw new Error('Could not find app fiber node.')
|
|
80
|
+
}
|
|
81
|
+
export const getScratchInternalStates = (root: ScratchRoot) => {
|
|
82
|
+
const rootContainer = root._reactRootContainer
|
|
83
|
+
const rootFiberNode = rootContainer._internalRoot.current
|
|
84
|
+
const appFiberNode = getAppFiberNode(rootFiberNode)
|
|
85
|
+
|
|
86
|
+
const reduxState = appFiberNode.memoizedProps.store.getState()
|
|
87
|
+
const vm = reduxState.scratchGui.vm
|
|
88
|
+
|
|
89
|
+
const scratchBlocksFiber = getSpecifiedFiber(rootFiberNode, (fiber) => {
|
|
90
|
+
if (typeof fiber.type === 'function') {
|
|
91
|
+
const propTypes = fiber.elementType?.propTypes
|
|
92
|
+
if (propTypes && 'toolboxXML' in propTypes) {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
97
|
+
})
|
|
98
|
+
// @ts-expect-error scratchBlocksFiber existence checked
|
|
99
|
+
const scratchBlocks = scratchBlocksFiber.stateNode.ScratchBlocks as {
|
|
100
|
+
getMainWorkspace: () => {
|
|
101
|
+
cleanUp: () => void
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
reduxState,
|
|
107
|
+
vm,
|
|
108
|
+
scratchBlocks,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
// find root DOM node
|
|
3
|
+
|
|
4
|
+
import type * as sb3 from '@pnsk-lab/sb3-types'
|
|
5
|
+
import { findDOMAppRoot, getScratchInternalStates } from './fiber'
|
|
6
|
+
import type { getModeForResolutionAtIndex } from 'typescript'
|
|
7
|
+
|
|
8
|
+
const root = findDOMAppRoot()
|
|
9
|
+
const state = getScratchInternalStates(root)
|
|
10
|
+
|
|
11
|
+
console.log('Scratch root element:', state)
|
|
12
|
+
|
|
13
|
+
// @ts-expect-error helpers for devtools
|
|
14
|
+
globalThis.hk = {
|
|
15
|
+
root,
|
|
16
|
+
vm: state.vm,
|
|
17
|
+
getModeForResolutionAtIndex: state.reduxState,
|
|
18
|
+
getJSON: () => state.vm.toJSON(),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import.meta.hot?.on('hikkaku:project', (project: sb3.ScratchProject) => {
|
|
22
|
+
state.vm.loadProject(project)
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
state.scratchBlocks.getMainWorkspace().cleanUp()
|
|
25
|
+
}, 1000)
|
|
26
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type * as sb3 from '@pnsk-lab/sb3-types'
|
|
2
|
+
import type {
|
|
3
|
+
CostumeSource,
|
|
4
|
+
PrimitiveAvailableOnScratch,
|
|
5
|
+
PrimitiveSource,
|
|
6
|
+
SoundSource,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
export const fromPrimitiveSource = <T extends PrimitiveAvailableOnScratch>(
|
|
10
|
+
source: PrimitiveSource<T>,
|
|
11
|
+
): sb3.Input => {
|
|
12
|
+
if (typeof source === 'number') {
|
|
13
|
+
return [1, [4, source]]
|
|
14
|
+
}
|
|
15
|
+
if (typeof source === 'boolean') {
|
|
16
|
+
return [1, [6, source ? 1 : 0]]
|
|
17
|
+
}
|
|
18
|
+
if (typeof source === 'string') {
|
|
19
|
+
return [1, [10, source]]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [1, source.id]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const fromCostumeSource = (source: CostumeSource): sb3.Input => {
|
|
26
|
+
if (
|
|
27
|
+
typeof source === 'object' &&
|
|
28
|
+
source !== null &&
|
|
29
|
+
'type' in source &&
|
|
30
|
+
source.type === 'costume'
|
|
31
|
+
) {
|
|
32
|
+
return fromPrimitiveSource(source.name)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return fromPrimitiveSource(source as PrimitiveSource<string>)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const fromSoundSource = (source: SoundSource): sb3.Input => {
|
|
39
|
+
if (
|
|
40
|
+
typeof source === 'object' &&
|
|
41
|
+
source !== null &&
|
|
42
|
+
'type' in source &&
|
|
43
|
+
source.type === 'sound'
|
|
44
|
+
) {
|
|
45
|
+
return fromPrimitiveSource(source.name)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return fromPrimitiveSource(source as PrimitiveSource<string>)
|
|
49
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type * as sb3 from '@pnsk-lab/sb3-types'
|
|
2
|
+
import type { HikkakuBlock } from './types'
|
|
3
|
+
|
|
4
|
+
export type Handler = () => void
|
|
5
|
+
|
|
6
|
+
let id = 0
|
|
7
|
+
const nextId = () => (++id).toString(16)
|
|
8
|
+
|
|
9
|
+
interface RootContext {
|
|
10
|
+
blocks: Record<string, sb3.Block>
|
|
11
|
+
adder?: (id: string, block: sb3.Block) => void
|
|
12
|
+
usedAsValueSet: WeakSet<sb3.Block>
|
|
13
|
+
valueBlockSet: WeakSet<sb3.Block>
|
|
14
|
+
blockToId: WeakMap<sb3.Block, string>
|
|
15
|
+
}
|
|
16
|
+
let rootContext: RootContext | null = null
|
|
17
|
+
const getRootContext = () => {
|
|
18
|
+
if (!rootContext) {
|
|
19
|
+
throw new Error('Root context is not initialized. Call createBlocks first.')
|
|
20
|
+
}
|
|
21
|
+
return rootContext
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BlockInit {
|
|
25
|
+
inputs?: Record<string, sb3.Input>
|
|
26
|
+
fields?: Record<string, sb3.Fields>
|
|
27
|
+
topLevel?: boolean
|
|
28
|
+
mutation?: sb3.Mutation
|
|
29
|
+
isShadow?: boolean
|
|
30
|
+
isValue?: boolean
|
|
31
|
+
}
|
|
32
|
+
export const block = (opcode: string, init: BlockInit): HikkakuBlock => {
|
|
33
|
+
const ctx = getRootContext()
|
|
34
|
+
const id = nextId()
|
|
35
|
+
const block = {
|
|
36
|
+
opcode,
|
|
37
|
+
inputs: init.inputs ?? {},
|
|
38
|
+
fields: init.fields ?? {},
|
|
39
|
+
mutation: init.mutation,
|
|
40
|
+
shadow: init.isShadow ?? false,
|
|
41
|
+
topLevel: init.topLevel ?? false,
|
|
42
|
+
x: 0,
|
|
43
|
+
y: 0,
|
|
44
|
+
next: null,
|
|
45
|
+
parent: null,
|
|
46
|
+
}
|
|
47
|
+
ctx.blocks[id] = block
|
|
48
|
+
ctx.blockToId.set(block, id)
|
|
49
|
+
if (init.isValue) {
|
|
50
|
+
ctx.valueBlockSet.add(block)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (init.inputs) {
|
|
54
|
+
for (const [_key, value] of Object.entries(init.inputs)) {
|
|
55
|
+
if (typeof value[1] === 'string') {
|
|
56
|
+
const valueBlockId = value[1]
|
|
57
|
+
const valueBlock = ctx.blocks[valueBlockId]
|
|
58
|
+
if (valueBlock) {
|
|
59
|
+
valueBlock.parent = id
|
|
60
|
+
ctx.usedAsValueSet.add(valueBlock)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ctx.adder) {
|
|
67
|
+
ctx.adder(id, block)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
isBlock: true,
|
|
72
|
+
id,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const valueBlock = (opcode: string, init: BlockInit): HikkakuBlock => {
|
|
77
|
+
return block(opcode, { ...init, isValue: true })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const applyNextAndParent = (blocks: sb3.Block[]) => {
|
|
81
|
+
const ctx = getRootContext()
|
|
82
|
+
let lastBlock: [string, sb3.Block] | null = null
|
|
83
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
84
|
+
const block = blocks[i]
|
|
85
|
+
if (!block || ctx.usedAsValueSet.has(block)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const blockId = ctx.blockToId.get(block)
|
|
89
|
+
if (!blockId) {
|
|
90
|
+
throw new Error(`Block ${JSON.stringify(block)} does not have an ID`)
|
|
91
|
+
}
|
|
92
|
+
if (block.topLevel) {
|
|
93
|
+
lastBlock = null
|
|
94
|
+
block.parent = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (lastBlock) {
|
|
98
|
+
lastBlock[1].next = blockId
|
|
99
|
+
block.parent ??= lastBlock[0]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lastBlock = [blockId, block]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const catchNewBlocks = (
|
|
106
|
+
handler: Handler,
|
|
107
|
+
catched: (id: string, block: sb3.Block) => void,
|
|
108
|
+
) => {
|
|
109
|
+
const ctx = getRootContext()
|
|
110
|
+
const oldAdder = ctx.adder
|
|
111
|
+
ctx.adder = (id: string, block: sb3.Block) => {
|
|
112
|
+
catched(id, block)
|
|
113
|
+
}
|
|
114
|
+
handler()
|
|
115
|
+
ctx.adder = oldAdder
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const substack = (handler: Handler) => {
|
|
119
|
+
const ctx = getRootContext()
|
|
120
|
+
const blocks: sb3.Block[] = []
|
|
121
|
+
|
|
122
|
+
catchNewBlocks(handler, (id, block) => {
|
|
123
|
+
ctx.blocks[id] = block
|
|
124
|
+
blocks.push(block)
|
|
125
|
+
})
|
|
126
|
+
applyNextAndParent(blocks)
|
|
127
|
+
|
|
128
|
+
// Pick the first executable block (skip value blocks used as inputs).
|
|
129
|
+
for (const block of blocks) {
|
|
130
|
+
if (!block || ctx.usedAsValueSet.has(block)) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
const blockId = ctx.blockToId.get(block)
|
|
134
|
+
if (blockId) {
|
|
135
|
+
return blockId
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const createBlocks = (handler: Handler) => {
|
|
143
|
+
const blocks: Record<string, sb3.Block> = {}
|
|
144
|
+
rootContext = {
|
|
145
|
+
blocks: blocks,
|
|
146
|
+
usedAsValueSet: new WeakSet(),
|
|
147
|
+
valueBlockSet: new WeakSet(),
|
|
148
|
+
blockToId: new WeakMap(),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const blocksForAddingNext: sb3.Block[] = []
|
|
152
|
+
catchNewBlocks(handler, (id, block) => {
|
|
153
|
+
blocks[id] = block
|
|
154
|
+
blocksForAddingNext.push(block)
|
|
155
|
+
})
|
|
156
|
+
applyNextAndParent(blocksForAddingNext)
|
|
157
|
+
|
|
158
|
+
const unconnectedValueBlocks: Array<{ id: string; opcode: string }> = []
|
|
159
|
+
for (const [blockId, block] of Object.entries(blocks)) {
|
|
160
|
+
if (
|
|
161
|
+
rootContext.valueBlockSet.has(block) &&
|
|
162
|
+
!rootContext.usedAsValueSet.has(block)
|
|
163
|
+
) {
|
|
164
|
+
unconnectedValueBlocks.push({ id: blockId, opcode: block.opcode })
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (unconnectedValueBlocks.length > 0) {
|
|
168
|
+
const formatted = unconnectedValueBlocks
|
|
169
|
+
.map(({ id, opcode }) => `${opcode} (${id})`)
|
|
170
|
+
.join(', ')
|
|
171
|
+
rootContext = null
|
|
172
|
+
throw new Error(`Unconnected value block(s): ${formatted}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
rootContext = null
|
|
176
|
+
return blocks
|
|
177
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type * as sb3 from '@pnsk-lab/sb3-types'
|
|
2
|
+
import { createBlocks } from './composer'
|
|
3
|
+
import type {
|
|
4
|
+
CostumeReference,
|
|
5
|
+
ListReference,
|
|
6
|
+
SoundReference,
|
|
7
|
+
VariableReference,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
let nextAssetId = 0
|
|
11
|
+
const createAssetId = () => `asset-${(++nextAssetId).toString(16)}`
|
|
12
|
+
|
|
13
|
+
export class Target<IsStage extends boolean = boolean> {
|
|
14
|
+
readonly isStage: IsStage
|
|
15
|
+
readonly name: IsStage extends true ? 'Stage' : string
|
|
16
|
+
currentCostume = 0
|
|
17
|
+
|
|
18
|
+
#blocks: Record<string, sb3.Block> = {}
|
|
19
|
+
#variables: Record<string, sb3.ScalarVariable> = {}
|
|
20
|
+
#lists: Record<string, sb3.List> = {}
|
|
21
|
+
#costumes: sb3.Costume[] = []
|
|
22
|
+
#sounds: sb3.Sound[] = []
|
|
23
|
+
constructor(isStage: IsStage, name: IsStage extends true ? 'Stage' : string) {
|
|
24
|
+
this.isStage = isStage
|
|
25
|
+
this.name = name
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
run(handler: (target: Target<IsStage>) => void): void {
|
|
29
|
+
const blocks = createBlocks(() => {
|
|
30
|
+
handler(this)
|
|
31
|
+
})
|
|
32
|
+
this.#blocks = {
|
|
33
|
+
...this.#blocks,
|
|
34
|
+
...blocks,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
createVariable(
|
|
39
|
+
name: string,
|
|
40
|
+
defaultValue: sb3.ScalarVal = 0,
|
|
41
|
+
isCloudVariable?: boolean,
|
|
42
|
+
): VariableReference {
|
|
43
|
+
const id = createAssetId()
|
|
44
|
+
this.#variables[id] = isCloudVariable
|
|
45
|
+
? [name, defaultValue, true]
|
|
46
|
+
: [name, defaultValue]
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
name,
|
|
50
|
+
type: 'variable',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createList(name: string, defaultValue: sb3.ScalarVal[] = []): ListReference {
|
|
55
|
+
const id = createAssetId()
|
|
56
|
+
this.#lists[id] = [name, defaultValue]
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
name,
|
|
60
|
+
type: 'list',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addCostume(costume: sb3.Costume): CostumeReference {
|
|
65
|
+
this.#costumes.push(costume)
|
|
66
|
+
return {
|
|
67
|
+
name: costume.name,
|
|
68
|
+
type: 'costume' as const,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
addSound(sound: sb3.Sound): SoundReference {
|
|
73
|
+
this.#sounds.push(sound)
|
|
74
|
+
return {
|
|
75
|
+
name: sound.name,
|
|
76
|
+
type: 'sound' as const,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
toScratch(): IsStage extends true ? sb3.Stage : sb3.Sprite {
|
|
81
|
+
const costumes =
|
|
82
|
+
this.#costumes.length > 0
|
|
83
|
+
? this.#costumes
|
|
84
|
+
: [
|
|
85
|
+
{
|
|
86
|
+
name: this.name,
|
|
87
|
+
assetId: 'cd21514d0531fdffb22204e0ec5ed84a',
|
|
88
|
+
dataFormat: 'svg' as const,
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
const target: sb3.Target = {
|
|
92
|
+
blocks: this.#blocks,
|
|
93
|
+
broadcasts: {},
|
|
94
|
+
variables: this.#variables,
|
|
95
|
+
lists: this.#lists,
|
|
96
|
+
sounds: this.#sounds,
|
|
97
|
+
currentCostume: this.currentCostume,
|
|
98
|
+
costumes,
|
|
99
|
+
}
|
|
100
|
+
if (this.isStage) {
|
|
101
|
+
return {
|
|
102
|
+
...target,
|
|
103
|
+
isStage: true,
|
|
104
|
+
name: 'Stage',
|
|
105
|
+
} satisfies sb3.Stage as IsStage extends true ? sb3.Stage : sb3.Sprite
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
...target,
|
|
109
|
+
isStage: false,
|
|
110
|
+
name: this.name,
|
|
111
|
+
visible: true,
|
|
112
|
+
} satisfies sb3.Sprite as IsStage extends true ? sb3.Stage : sb3.Sprite
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class Project {
|
|
117
|
+
readonly stage: Target<true>
|
|
118
|
+
#targets: Target[] = []
|
|
119
|
+
constructor() {
|
|
120
|
+
const target = new Target(true, 'Stage')
|
|
121
|
+
this.stage = target
|
|
122
|
+
this.#targets.push(target)
|
|
123
|
+
}
|
|
124
|
+
createSprite(name: string): Target<false> {
|
|
125
|
+
const sprite = new Target(false, name)
|
|
126
|
+
this.#targets.push(sprite)
|
|
127
|
+
return sprite
|
|
128
|
+
}
|
|
129
|
+
toScratch(): sb3.ScratchProject {
|
|
130
|
+
return {
|
|
131
|
+
targets: this.#targets.map((target) => target.toScratch()),
|
|
132
|
+
meta: {
|
|
133
|
+
semver: '3.0.0',
|
|
134
|
+
agent: `Hikkaku | ${globalThis.navigator ? navigator.userAgent : 'unknown'}`,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PrimitiveAvailableOnScratch = number | boolean | string
|
|
2
|
+
|
|
3
|
+
export type PrimitiveSource<T extends PrimitiveAvailableOnScratch> =
|
|
4
|
+
| T
|
|
5
|
+
| HikkakuBlock
|
|
6
|
+
|
|
7
|
+
export interface VariableBase {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface VariableReference extends VariableBase {
|
|
13
|
+
type: 'variable'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ListReference extends VariableBase {
|
|
17
|
+
type: 'list'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CostumeReference {
|
|
21
|
+
name: string
|
|
22
|
+
type: 'costume'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CostumeSource = PrimitiveSource<string> | CostumeReference
|
|
26
|
+
|
|
27
|
+
export interface SoundReference {
|
|
28
|
+
name: string
|
|
29
|
+
type: 'sound'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SoundSource = PrimitiveSource<string> | SoundReference
|
|
33
|
+
|
|
34
|
+
export interface HikkakuBlock {
|
|
35
|
+
isBlock: true
|
|
36
|
+
id: string
|
|
37
|
+
}
|
package/src/index.ts
ADDED
package/src/vite/env.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DevEnvironment,
|
|
3
|
+
type HotChannel,
|
|
4
|
+
type ResolvedConfig,
|
|
5
|
+
type WebSocketServer,
|
|
6
|
+
} from 'vite'
|
|
7
|
+
import {
|
|
8
|
+
ESModulesEvaluator,
|
|
9
|
+
ModuleRunner,
|
|
10
|
+
type ModuleRunnerTransport,
|
|
11
|
+
} from 'vite/module-runner'
|
|
12
|
+
|
|
13
|
+
export function createHikkakuEnvironment(
|
|
14
|
+
name: string,
|
|
15
|
+
config: ResolvedConfig,
|
|
16
|
+
_context: {
|
|
17
|
+
ws: WebSocketServer
|
|
18
|
+
},
|
|
19
|
+
): DevEnvironment {
|
|
20
|
+
const transport: ModuleRunnerTransport = {
|
|
21
|
+
send: (data) => {
|
|
22
|
+
console.log('sent', data)
|
|
23
|
+
},
|
|
24
|
+
connect(_handlers) {},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const env = new (class extends DevEnvironment {
|
|
28
|
+
runner: ModuleRunner
|
|
29
|
+
constructor(
|
|
30
|
+
name: string,
|
|
31
|
+
config: ResolvedConfig,
|
|
32
|
+
context: { hot: boolean; transport?: HotChannel },
|
|
33
|
+
) {
|
|
34
|
+
super(name, config, context)
|
|
35
|
+
this.runner = new ModuleRunner(
|
|
36
|
+
{ transport: this.hot },
|
|
37
|
+
new ESModulesEvaluator(),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
})(name, config, {
|
|
41
|
+
transport,
|
|
42
|
+
hot: true,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return env
|
|
46
|
+
}
|