hikkaku 0.3.3 → 0.3.4
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/README.md +2 -1
- package/{composer-bPcsVhIg.mjs → block-helper-DaOyXkRZ.mjs} +164 -90
- package/blocks/index.d.mts +163 -150
- package/blocks/index.mjs +144 -135
- package/{project-CpV5Dm-X.d.mts → index-kUDvI5sE.d.mts} +59 -16
- package/index.d.mts +2 -2
- package/index.mjs +17 -6
- package/package.json +5 -2
- package/types.d.mts +7 -0
- package/vite/index.mjs +22 -17
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<img src="https://raw.githubusercontent.com/pnsk-lab/hikkaku/refs/heads/main/docs/assets/logo.svg" alt="Hikkaku Logo" width="128" height="128" align="right" />
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
# Hikkaku
|
|
4
5
|
|
|
5
|
-
[](https://www.npmjs.com/package/hikkaku)
|
|
6
|
+
[](https://www.npmjs.com/package/hikkaku) [](https://codecov.io/gh/pnsk-lab/hikkaku)
|
|
6
7
|
|
|
7
8
|
[Docs](https://pnsk-lab.github.io/hikkaku/) | [Playground](https://pnsk-lab.github.io/playground/)
|
|
8
9
|
|
|
@@ -1,73 +1,5 @@
|
|
|
1
1
|
import { InputType, Shadow } from "sb3-types/enum";
|
|
2
2
|
|
|
3
|
-
//#region src/core/sb3-enum.ts
|
|
4
|
-
const toNumericEnum = (value, fallback) => {
|
|
5
|
-
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
6
|
-
if (typeof value === "string") {
|
|
7
|
-
const normalized = value.trim();
|
|
8
|
-
if (normalized.length === 0) return fallback;
|
|
9
|
-
const parsed = Number(normalized);
|
|
10
|
-
if (Number.isInteger(parsed)) return parsed;
|
|
11
|
-
}
|
|
12
|
-
return fallback;
|
|
13
|
-
};
|
|
14
|
-
const shadow = Shadow;
|
|
15
|
-
const inputType = InputType;
|
|
16
|
-
const Shadow$1 = {
|
|
17
|
-
SameBlockShadow: toNumericEnum(shadow.SameBlockShadow ?? shadow.UnObscured, 1),
|
|
18
|
-
NoShadow: toNumericEnum(shadow.NoShadow ?? shadow.No, 2),
|
|
19
|
-
DiffBlockShadow: toNumericEnum(shadow.DiffBlockShadow ?? shadow.Obscured, 3)
|
|
20
|
-
};
|
|
21
|
-
const InputType$1 = {
|
|
22
|
-
Number: toNumericEnum(inputType.Number, 4),
|
|
23
|
-
PositiveInteger: toNumericEnum(inputType.PositiveInteger ?? inputType.PossiveInteger, 6),
|
|
24
|
-
String: toNumericEnum(inputType.String, 10),
|
|
25
|
-
Broadcast: toNumericEnum(inputType.Broadcast, 11),
|
|
26
|
-
Color: toNumericEnum(inputType.Color, 9)
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
//#endregion
|
|
30
|
-
//#region src/core/block-helper.ts
|
|
31
|
-
const fromPrimitiveSource = (source) => {
|
|
32
|
-
if (typeof source === "number") return [Shadow$1.SameBlockShadow, [InputType$1.Number, source]];
|
|
33
|
-
if (typeof source === "boolean") return [Shadow$1.SameBlockShadow, [InputType$1.PositiveInteger, source ? 1 : 0]];
|
|
34
|
-
if (typeof source === "string") return [Shadow$1.SameBlockShadow, [InputType$1.String, source]];
|
|
35
|
-
return [Shadow$1.SameBlockShadow, source.id];
|
|
36
|
-
};
|
|
37
|
-
const fromPrimitiveSourceColor = (color) => {
|
|
38
|
-
if (typeof color === "string") return [Shadow$1.SameBlockShadow, [InputType$1.Color, color]];
|
|
39
|
-
return fromPrimitiveSource(color);
|
|
40
|
-
};
|
|
41
|
-
const unwrapCostumeSource = (source) => {
|
|
42
|
-
if (isCostumeReference(source)) return source.name;
|
|
43
|
-
return source;
|
|
44
|
-
};
|
|
45
|
-
const unwrapSoundSource = (source) => {
|
|
46
|
-
if (isSoundReference(source)) return source.name;
|
|
47
|
-
return source;
|
|
48
|
-
};
|
|
49
|
-
const isHikkakuBlock = (block) => {
|
|
50
|
-
return typeof block === "object" && block !== null && "isBlock" in block && block.isBlock === true && "id" in block && typeof block.id === "string";
|
|
51
|
-
};
|
|
52
|
-
const menuInput = (source, createMenu) => {
|
|
53
|
-
if (isHikkakuBlock(source)) {
|
|
54
|
-
const shadow = createMenu();
|
|
55
|
-
return [
|
|
56
|
-
Shadow$1.DiffBlockShadow,
|
|
57
|
-
source.id,
|
|
58
|
-
shadow.id
|
|
59
|
-
];
|
|
60
|
-
}
|
|
61
|
-
return fromPrimitiveSource(createMenu(source));
|
|
62
|
-
};
|
|
63
|
-
const isCostumeReference = (source) => {
|
|
64
|
-
return typeof source === "object" && source !== null && "type" in source && source.type === "costume";
|
|
65
|
-
};
|
|
66
|
-
const isSoundReference = (source) => {
|
|
67
|
-
return typeof source === "object" && source !== null && "type" in source && source.type === "sound";
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
//#endregion
|
|
71
3
|
//#region src/core/composer.ts
|
|
72
4
|
let id = 0;
|
|
73
5
|
const nextId = () => (++id).toString(16);
|
|
@@ -76,8 +8,52 @@ const getRootContext = () => {
|
|
|
76
8
|
if (!rootContext) throw new Error("Root context is not initialized. Call createBlocks first.");
|
|
77
9
|
return rootContext;
|
|
78
10
|
};
|
|
11
|
+
const getCurrentScope = (ctx) => {
|
|
12
|
+
return ctx.scopeStack[ctx.scopeStack.length - 1] ?? null;
|
|
13
|
+
};
|
|
14
|
+
const __unstable_getBuildScopeFrame = () => {
|
|
15
|
+
if (!rootContext) return null;
|
|
16
|
+
const frame = getCurrentScope(rootContext);
|
|
17
|
+
if (!frame) return null;
|
|
18
|
+
return {
|
|
19
|
+
id: frame.id,
|
|
20
|
+
kind: frame.kind,
|
|
21
|
+
depth: rootContext.scopeStack.length,
|
|
22
|
+
forbidStop: frame.forbidStop
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const __unstable_onBuildScopeExit = (callback) => {
|
|
26
|
+
const frame = getCurrentScope(getRootContext());
|
|
27
|
+
if (!frame) throw new Error("Build scope is not initialized.");
|
|
28
|
+
frame.onExit.add(callback);
|
|
29
|
+
};
|
|
30
|
+
const __unstable_forbidStopInCurrentScope = () => {
|
|
31
|
+
const frame = getCurrentScope(getRootContext());
|
|
32
|
+
if (!frame) throw new Error("Build scope is not initialized.");
|
|
33
|
+
frame.forbidStop = true;
|
|
34
|
+
};
|
|
35
|
+
const withScope = (kind, handler) => {
|
|
36
|
+
const ctx = getRootContext();
|
|
37
|
+
const frame = {
|
|
38
|
+
id: ctx.nextScopeId++,
|
|
39
|
+
kind,
|
|
40
|
+
forbidStop: false,
|
|
41
|
+
onExit: /* @__PURE__ */ new Set()
|
|
42
|
+
};
|
|
43
|
+
ctx.scopeStack.push(frame);
|
|
44
|
+
try {
|
|
45
|
+
return handler();
|
|
46
|
+
} finally {
|
|
47
|
+
for (const callback of frame.onExit) callback();
|
|
48
|
+
if (ctx.scopeStack.pop() !== frame) {
|
|
49
|
+
console.warn("Build scope stack is corrupted.");
|
|
50
|
+
ctx.scopeStack.length = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
79
54
|
const block = (opcode, init) => {
|
|
80
55
|
const ctx = getRootContext();
|
|
56
|
+
if (opcode === "control_stop" && ctx.scopeStack.some((scope) => scope.forbidStop)) throw new Error("control_stop is not allowed inside gobox-managed scoped values.");
|
|
81
57
|
const id = nextId();
|
|
82
58
|
const block = {
|
|
83
59
|
opcode,
|
|
@@ -176,7 +152,7 @@ const layoutTopLevelBlocks = (blocks) => {
|
|
|
176
152
|
const substack = (handler) => {
|
|
177
153
|
const ctx = getRootContext();
|
|
178
154
|
const blocks = [];
|
|
179
|
-
catchNewBlocks(handler, (id, block) => {
|
|
155
|
+
catchNewBlocks(() => withScope("stack", handler), (id, block) => {
|
|
180
156
|
ctx.blocks[id] = block;
|
|
181
157
|
blocks.push(block);
|
|
182
158
|
});
|
|
@@ -187,7 +163,7 @@ const attachStack = (parentId, handler) => {
|
|
|
187
163
|
if (!handler) return null;
|
|
188
164
|
const ctx = getRootContext();
|
|
189
165
|
const blocks = [];
|
|
190
|
-
catchNewBlocks(handler, (id, block) => {
|
|
166
|
+
catchNewBlocks(() => withScope("stack", handler), (id, block) => {
|
|
191
167
|
ctx.blocks[id] = block;
|
|
192
168
|
blocks.push(block);
|
|
193
169
|
});
|
|
@@ -205,32 +181,130 @@ const attachStack = (parentId, handler) => {
|
|
|
205
181
|
};
|
|
206
182
|
const createBlocks = (handler) => {
|
|
207
183
|
const blocks = {};
|
|
208
|
-
|
|
184
|
+
const ctx = {
|
|
209
185
|
blocks,
|
|
210
186
|
usedAsValueSet: /* @__PURE__ */ new WeakSet(),
|
|
211
187
|
valueBlockSet: /* @__PURE__ */ new WeakSet(),
|
|
212
|
-
blockToId: /* @__PURE__ */ new WeakMap()
|
|
188
|
+
blockToId: /* @__PURE__ */ new WeakMap(),
|
|
189
|
+
scopeStack: [],
|
|
190
|
+
nextScopeId: 1
|
|
213
191
|
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
192
|
+
rootContext = ctx;
|
|
193
|
+
try {
|
|
194
|
+
const blocksForAddingNext = [];
|
|
195
|
+
catchNewBlocks(() => withScope("run", handler), (id, block) => {
|
|
196
|
+
blocks[id] = block;
|
|
197
|
+
blocksForAddingNext.push(block);
|
|
198
|
+
});
|
|
199
|
+
applyNextAndParent(blocksForAddingNext);
|
|
200
|
+
layoutTopLevelBlocks(blocksForAddingNext);
|
|
201
|
+
const unconnectedValueBlocks = [];
|
|
202
|
+
for (const [blockId, block] of Object.entries(blocks)) if (ctx.valueBlockSet.has(block) && !ctx.usedAsValueSet.has(block)) unconnectedValueBlocks.push({
|
|
203
|
+
id: blockId,
|
|
204
|
+
opcode: block.opcode
|
|
205
|
+
});
|
|
206
|
+
if (unconnectedValueBlocks.length > 0) {
|
|
207
|
+
const formatted = unconnectedValueBlocks.map(({ id, opcode }) => `${opcode} (${id})`).join(", ");
|
|
208
|
+
throw new Error(`Unconnected value block(s): ${formatted}`);
|
|
209
|
+
}
|
|
210
|
+
return blocks;
|
|
211
|
+
} finally {
|
|
228
212
|
rootContext = null;
|
|
229
|
-
throw new Error(`Unconnected value block(s): ${formatted}`);
|
|
230
213
|
}
|
|
231
|
-
rootContext = null;
|
|
232
|
-
return blocks;
|
|
233
214
|
};
|
|
234
215
|
|
|
235
216
|
//#endregion
|
|
236
|
-
|
|
217
|
+
//#region src/core/block-helper.ts
|
|
218
|
+
function isShadowBlock(blockId) {
|
|
219
|
+
try {
|
|
220
|
+
return getRootContext().blocks[blockId]?.shadow ?? false;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function getDefaultValue(inputType) {
|
|
226
|
+
switch (inputType) {
|
|
227
|
+
case InputType.Number:
|
|
228
|
+
case InputType.PositiveNumber:
|
|
229
|
+
case InputType.Integer: return 0;
|
|
230
|
+
case InputType.Angle: return 90;
|
|
231
|
+
case InputType.PositiveInteger: return 1;
|
|
232
|
+
case InputType.String:
|
|
233
|
+
case InputType.Broadcast: return "";
|
|
234
|
+
case InputType.Color: return "#000000";
|
|
235
|
+
default: return 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function fromPrimitiveSource(inputType, source, defaultValue) {
|
|
239
|
+
defaultValue = defaultValue ?? getDefaultValue(inputType);
|
|
240
|
+
if (isHikkakuBlock(source)) {
|
|
241
|
+
if (isShadowBlock(source.id)) return [Shadow.SameBlockShadow, source.id];
|
|
242
|
+
return [
|
|
243
|
+
Shadow.DiffBlockShadow,
|
|
244
|
+
source.id,
|
|
245
|
+
createInput(inputType, defaultValue)
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
return [Shadow.SameBlockShadow, createInput(inputType, source)];
|
|
249
|
+
}
|
|
250
|
+
function createInput(inputType, value) {
|
|
251
|
+
switch (inputType) {
|
|
252
|
+
case InputType.Number:
|
|
253
|
+
case InputType.PositiveNumber:
|
|
254
|
+
case InputType.Integer:
|
|
255
|
+
case InputType.Angle:
|
|
256
|
+
case InputType.PositiveInteger: return [inputType, Number(value)];
|
|
257
|
+
case InputType.String: return [inputType, String(value)];
|
|
258
|
+
case InputType.Broadcast: return [
|
|
259
|
+
inputType,
|
|
260
|
+
String(value),
|
|
261
|
+
String(value)
|
|
262
|
+
];
|
|
263
|
+
case InputType.Color: return [inputType, String(value)];
|
|
264
|
+
case InputType.Variable:
|
|
265
|
+
case InputType.List: throw new Error("unimplemented");
|
|
266
|
+
default: throw new Error(`Unsupported input type: ${inputType}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function fromBooleanSource(source) {
|
|
270
|
+
if (typeof source === "boolean") if (source === true) {
|
|
271
|
+
const TRUE = valueBlock("operator_not", {});
|
|
272
|
+
return [Shadow.SameBlockShadow, TRUE.id];
|
|
273
|
+
} else {
|
|
274
|
+
const FALSE = valueBlock("operator_and", {});
|
|
275
|
+
return [Shadow.SameBlockShadow, FALSE.id];
|
|
276
|
+
}
|
|
277
|
+
return [Shadow.SameBlockShadow, source.id];
|
|
278
|
+
}
|
|
279
|
+
const unwrapCostumeSource = (source) => {
|
|
280
|
+
if (isCostumeReference(source)) return source.name;
|
|
281
|
+
return source;
|
|
282
|
+
};
|
|
283
|
+
const unwrapSoundSource = (source) => {
|
|
284
|
+
if (isSoundReference(source)) return source.name;
|
|
285
|
+
return source;
|
|
286
|
+
};
|
|
287
|
+
const isHikkakuBlock = (block) => {
|
|
288
|
+
return typeof block === "object" && block !== null && "isBlock" in block && block.isBlock === true && "id" in block && typeof block.id === "string";
|
|
289
|
+
};
|
|
290
|
+
const menuInput = (source, createMenu) => {
|
|
291
|
+
if (isHikkakuBlock(source)) {
|
|
292
|
+
const shadow = createMenu();
|
|
293
|
+
return [
|
|
294
|
+
Shadow.DiffBlockShadow,
|
|
295
|
+
source.id,
|
|
296
|
+
shadow.id
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
const menu = createMenu(source);
|
|
300
|
+
return fromPrimitiveSource(InputType.String, menu);
|
|
301
|
+
};
|
|
302
|
+
const isCostumeReference = (source) => {
|
|
303
|
+
return typeof source === "object" && source !== null && "type" in source && source.type === "costume";
|
|
304
|
+
};
|
|
305
|
+
const isSoundReference = (source) => {
|
|
306
|
+
return typeof source === "object" && source !== null && "type" in source && source.type === "sound";
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
export { valueBlock as _, isSoundReference as a, unwrapSoundSource as c, __unstable_onBuildScopeExit as d, attachStack as f, substack as g, getRootContext as h, isHikkakuBlock as i, __unstable_forbidStopInCurrentScope as l, createBlocks as m, fromPrimitiveSource as n, menuInput as o, block as p, isCostumeReference as r, unwrapCostumeSource as s, fromBooleanSource as t, __unstable_getBuildScopeFrame as u };
|