Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/hikkaku/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export * from './pen'
export * from './procedures'
export * from './sensing'
export * from './sound'
export * from './text2speech'
export * from './translate'
export * from './videosensing'
21 changes: 21 additions & 0 deletions packages/hikkaku/src/blocks/text2speech.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, test } from 'vite-plus/test'
import { block, createBlocks } from '../core/composer'
import { setLanguage, setVoice, speakAndWait } from './text2speech'

describe('blocks/text2speech', () => {
test('creates text to speech extension blocks', () => {
const blocks = createBlocks(() => {
block('event_whenflagclicked', { topLevel: true })
setVoice('TENOR')
setLanguage('ja')
speakAndWait('hello')
})

const opcodes = Object.values(blocks).map((b) => b.opcode)
expect(opcodes).toContain('text2speech_setVoice')
expect(opcodes).toContain('text2speech_menu_voices')
expect(opcodes).toContain('text2speech_setLanguage')
expect(opcodes).toContain('text2speech_menu_languages')
expect(opcodes).toContain('text2speech_speakAndWait')
})
})
103 changes: 103 additions & 0 deletions packages/hikkaku/src/blocks/text2speech.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { InputType } from 'sb3-types/enum'
import { fromPrimitiveSource, menuInput } from '../core/block-helper'
import { block, valueBlock } from '../core/composer'
import type { HikkakuString, PrimitiveSource } from '../core/types'

export type TextToSpeechVoice =
| 'ALTO'
| 'TENOR'
| 'SQUEAK'
| 'GIANT'
| 'KITTEN'
| 'GOOGLE'

/**
* Speaks text and waits until completion.
*
* Input: `words`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param words See function signature for accepted input values.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { speakAndWait } from 'hikkaku/blocks'
*
* speakAndWait('hello')
* ```
*/
export const speakAndWait = (words: PrimitiveSource<HikkakuString>) => {
return block('text2speech_speakAndWait', {
inputs: {
WORDS: fromPrimitiveSource(InputType.String, words, 'hello'),
},
})
}

/**
* Sets the text-to-speech voice.
*
* Input: `voice`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param voice See function signature for accepted input values.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { setVoice } from 'hikkaku/blocks'
*
* setVoice('TENOR')
* ```
*/
export function setVoice(voice: TextToSpeechVoice): ReturnType<typeof block>
export function setVoice(
voice: PrimitiveSource<HikkakuString>,
): ReturnType<typeof block>
export function setVoice(voice: PrimitiveSource<HikkakuString>) {
return block('text2speech_setVoice', {
inputs: {
VOICE: menuInput(voice, menuOfTextToSpeechVoice),
},
})
}

export const menuOfTextToSpeechVoice = (voice: TextToSpeechVoice = 'ALTO') => {
return valueBlock<HikkakuString>('text2speech_menu_voices', {
fields: {
voices: [voice, null],
},
isShadow: true,
})
}

/**
* Sets the text-to-speech language.
*
* Input: `language`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param language See function signature for accepted input values.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { setLanguage } from 'hikkaku/blocks'
*
* setLanguage('ja')
* ```
*/
export const setLanguage = (language: PrimitiveSource<HikkakuString>) => {
return block('text2speech_setLanguage', {
inputs: {
LANGUAGE: menuInput(language, menuOfTextToSpeechLanguage),
},
})
}

export const menuOfTextToSpeechLanguage = (language: string = 'en') => {
return valueBlock<HikkakuString>('text2speech_menu_languages', {
fields: {
languages: [language, null],
},
isShadow: true,
})
}
19 changes: 19 additions & 0 deletions packages/hikkaku/src/blocks/translate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, test } from 'vite-plus/test'
import { block, createBlocks } from '../core/composer'
import { say } from './looks'
import { getViewerLanguage, translate } from './translate'

describe('blocks/translate', () => {
test('creates translate extension blocks', () => {
const blocks = createBlocks(() => {
block('event_whenflagclicked', { topLevel: true })
say(translate('hello', 'ja'))
say(getViewerLanguage())
})

const opcodes = Object.values(blocks).map((b) => b.opcode)
expect(opcodes).toContain('translate_getTranslate')
expect(opcodes).toContain('translate_menu_languages')
expect(opcodes).toContain('translate_getViewerLanguage')
})
})
59 changes: 59 additions & 0 deletions packages/hikkaku/src/blocks/translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { InputType } from 'sb3-types/enum'
import { fromPrimitiveSource, menuInput } from '../core/block-helper'
import { valueBlock } from '../core/composer'
import type { HikkakuString, PrimitiveSource } from '../core/types'

/**
* Translates words to a target language.
*
* Input: `words`, `language`.
* Output: Scratch reporter block definition that can be used as an input value in other blocks.
*
* @param words See function signature for accepted input values.
* @param language See function signature for accepted input values.
* @returns Scratch reporter block definition that can be used as an input value in other blocks.
* @example
* ```ts
* import { translate } from 'hikkaku/blocks'
*
* translate('hello', 'ja')
* ```
*/
export const translate = (
words: PrimitiveSource<HikkakuString>,
language: PrimitiveSource<HikkakuString>,
) => {
return valueBlock<HikkakuString>('translate_getTranslate', {
inputs: {
WORDS: fromPrimitiveSource(InputType.String, words, 'hello'),
LANGUAGE: menuInput(language, menuOfTranslateLanguage),
},
})
}

export const menuOfTranslateLanguage = (language: string = 'ja') => {
return valueBlock<HikkakuString>('translate_menu_languages', {
fields: {
languages: [language, null],
},
isShadow: true,
})
}

/**
* Returns viewer language.
*
* Input: none.
* Output: Scratch reporter block definition that can be used as an input value in other blocks.
*
* @returns Scratch reporter block definition that can be used as an input value in other blocks.
* @example
* ```ts
* import { getViewerLanguage } from 'hikkaku/blocks'
*
* getViewerLanguage()
* ```
*/
export const getViewerLanguage = () => {
return valueBlock<HikkakuString>('translate_getViewerLanguage', {})
}
35 changes: 35 additions & 0 deletions packages/hikkaku/src/blocks/videosensing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from 'vite-plus/test'
import { createBlocks } from '../core/composer'
import { say } from './looks'
import {
setVideoTransparency,
videoOn,
videoToggle,
whenMotionGreaterThan,
} from './videosensing'

describe('blocks/videosensing', () => {
test('creates video sensing extension blocks', () => {
const blocks = createBlocks(() => {
whenMotionGreaterThan(10, () => {
setVideoTransparency(50)
})
say(videoOn('motion', 'this sprite'))
videoToggle('on')
})
Comment on lines +13 to +19

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, blocks created after calling whenMotionGreaterThan(…, () => { … }) are emitted in the root build scope; during createBlocks(), applyNextAndParent() will link those root blocks after the hat and can overwrite the hat’s next pointer (which attachStack() set to the substack). That can disconnect the intended stack from whenMotionGreaterThan, meaning the test isn’t actually validating the hat+stack wiring. Consider moving say(videoOn(...)) / videoToggle('on') into the hat callback (if they should be in that script), or start a separate top-level script before them (if they should be separate), and optionally assert the hat’s next points at the first stacked block.

Copilot uses AI. Check for mistakes.

const opcodes = Object.values(blocks).map((b) => b.opcode)
expect(opcodes).toContain('videoSensing_whenMotionGreaterThan')
expect(opcodes).toContain('videoSensing_videoOn')
expect(opcodes).toContain('videoSensing_menu_ATTRIBUTE')
expect(opcodes).toContain('videoSensing_menu_SUBJECT')
expect(opcodes).toContain('videoSensing_videoToggle')
expect(opcodes).toContain('videoSensing_menu_VIDEO_STATE')
expect(opcodes).toContain('videoSensing_setVideoTransparency')

const hat = Object.values(blocks).find(
(b) => b.opcode === 'videoSensing_whenMotionGreaterThan',
)
expect(hat?.topLevel).toBe(true)
})
})
149 changes: 149 additions & 0 deletions packages/hikkaku/src/blocks/videosensing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { InputType } from 'sb3-types/enum'
import { fromPrimitiveSource, menuInput } from '../core/block-helper'
import { attachStack, block, valueBlock } from '../core/composer'
import type {
HikkakuNumber,
HikkakuString,
PrimitiveSource,
} from '../core/types'

export type VideoSensingAttribute = 'motion' | 'direction'
export type VideoSensingSubject = 'this sprite' | 'stage'
export type VideoState = 'on' | 'off' | 'on-flipped'

/**
* Runs when motion is greater than a threshold.
*
* Input: `reference`, `stack`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param reference See function signature for accepted input values.
* @param stack See function signature for accepted input values. Optional.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { whenMotionGreaterThan } from 'hikkaku/blocks'
*
* whenMotionGreaterThan(10, () => {})
* ```
*/
export const whenMotionGreaterThan = (
reference: PrimitiveSource<HikkakuNumber>,
stack?: () => void,
) => {
const res = block('videoSensing_whenMotionGreaterThan', {
topLevel: true,
inputs: {
REFERENCE: fromPrimitiveSource(InputType.Number, reference, 10),
},
})
attachStack(res.id, stack)
return res
}

/**
* Returns motion or direction detected from a subject.
*
* Input: `attribute`, `subject`.
* Output: Scratch reporter block definition that can be used as an input value in other blocks.
*
* @param attribute See function signature for accepted input values.
* @param subject See function signature for accepted input values.
* @returns Scratch reporter block definition that can be used as an input value in other blocks.
* @example
* ```ts
* import { videoOn } from 'hikkaku/blocks'
*
* videoOn('motion', 'this sprite')
* ```
*/
export const videoOn = (
attribute: PrimitiveSource<HikkakuString>,
subject: PrimitiveSource<HikkakuString>,
) => {
return valueBlock<HikkakuNumber>('videoSensing_videoOn', {
inputs: {
ATTRIBUTE: menuInput(attribute, menuOfVideoSensingAttribute),
SUBJECT: menuInput(subject, menuOfVideoSensingSubject),
},
})
}

export const menuOfVideoSensingAttribute = (
attribute: VideoSensingAttribute = 'motion',
) => {
return valueBlock<HikkakuString>('videoSensing_menu_ATTRIBUTE', {
fields: {
ATTRIBUTE: [attribute, null],
},
isShadow: true,
})
}

export const menuOfVideoSensingSubject = (
subject: VideoSensingSubject = 'this sprite',
) => {
return valueBlock<HikkakuString>('videoSensing_menu_SUBJECT', {
fields: {
SUBJECT: [subject, null],
},
isShadow: true,
})
}

/**
* Turns video on/off/flipped.
*
* Input: `state`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param state See function signature for accepted input values.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { videoToggle } from 'hikkaku/blocks'
*
* videoToggle('on')
* ```
*/
export const videoToggle = (state: PrimitiveSource<HikkakuString>) => {
return block('videoSensing_videoToggle', {
inputs: {
VIDEO_STATE: menuInput(state, menuOfVideoSensingState),
},
})
}

export const menuOfVideoSensingState = (state: VideoState = 'on') => {
return valueBlock<HikkakuString>('videoSensing_menu_VIDEO_STATE', {
fields: {
VIDEO_STATE: [state, null],
},
isShadow: true,
})
}

/**
* Sets video transparency.
*
* Input: `transparency`.
* Output: Scratch statement block definition that is appended to the current script stack.
*
* @param transparency See function signature for accepted input values.
* @returns Scratch statement block definition that is appended to the current script stack.
* @example
* ```ts
* import { setVideoTransparency } from 'hikkaku/blocks'
*
* setVideoTransparency(50)
* ```
*/
export const setVideoTransparency = (
transparency: PrimitiveSource<HikkakuNumber>,
) => {
return block('videoSensing_setVideoTransparency', {
inputs: {
TRANSPARENCY: fromPrimitiveSource(InputType.Number, transparency, 50),
},
})
}
Loading