@fugood/bricks-ctor 2.25.0-beta.1 → 2.25.0-beta.10
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/compile/__tests__/index.test.js +115 -0
- package/compile/action-name-map.ts +23 -0
- package/compile/index.ts +193 -13
- package/package.json +3 -3
- package/skills/bricks-ctor/rules/animation.md +3 -2
- package/skills/bricks-ctor/rules/buttress.md +97 -8
- package/tools/preview-main.mjs +18 -5
- package/types/animation.ts +16 -5
- package/types/bricks/Image.ts +12 -0
- package/types/bricks/Sketch.ts +254 -0
- package/types/bricks/index.ts +1 -0
- package/types/data.ts +1 -1
- package/types/generators/LlmGgml.ts +1 -0
- package/types/generators/LlmMlx.ts +1 -0
- package/types/generators/RealtimeTranscription.ts +8 -0
- package/types/generators/SpeechToTextGgml.ts +5 -0
- package/types/subspace.ts +1 -1
- package/utils/__tests__/id.test.js +58 -0
- package/utils/data.ts +1 -1
- package/utils/event-props.ts +13 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { compile } from '../index'
|
|
2
|
+
|
|
3
|
+
const SUBSPACE_ID = 'SUBSPACE_00000000-0000-0000-0000-000000000001'
|
|
4
|
+
const CANVAS_ID = 'CANVAS_00000000-0000-0000-0000-000000000001'
|
|
5
|
+
const ANIMATION_ID = 'ANIMATION_00000000-0000-0000-0000-000000000001'
|
|
6
|
+
|
|
7
|
+
const makeApp = (animations) => {
|
|
8
|
+
const rootCanvas = {
|
|
9
|
+
__typename: 'Canvas',
|
|
10
|
+
id: CANVAS_ID,
|
|
11
|
+
items: [],
|
|
12
|
+
}
|
|
13
|
+
const rootSubspace = {
|
|
14
|
+
__typename: 'Subspace',
|
|
15
|
+
id: SUBSPACE_ID,
|
|
16
|
+
title: 'Main Subspace',
|
|
17
|
+
layout: {
|
|
18
|
+
width: 96,
|
|
19
|
+
height: 54,
|
|
20
|
+
},
|
|
21
|
+
rootCanvas,
|
|
22
|
+
canvases: [rootCanvas],
|
|
23
|
+
animations,
|
|
24
|
+
bricks: [],
|
|
25
|
+
generators: [],
|
|
26
|
+
data: [],
|
|
27
|
+
dataRouting: [],
|
|
28
|
+
dataCalculation: [],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name: 'Compile animation test',
|
|
33
|
+
rootSubspace,
|
|
34
|
+
subspaces: [rootSubspace],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('compile animations', () => {
|
|
39
|
+
test('normalizes mixed legacy spring config', async () => {
|
|
40
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
41
|
+
try {
|
|
42
|
+
const config = await compile(
|
|
43
|
+
makeApp([
|
|
44
|
+
{
|
|
45
|
+
__typename: 'Animation',
|
|
46
|
+
id: ANIMATION_ID,
|
|
47
|
+
alias: 'btnPressOut',
|
|
48
|
+
title: 'Button Press Out',
|
|
49
|
+
property: 'transform.scale',
|
|
50
|
+
config: {
|
|
51
|
+
__type: 'AnimationSpringConfig',
|
|
52
|
+
toValue: 1,
|
|
53
|
+
friction: 5,
|
|
54
|
+
tension: 200,
|
|
55
|
+
speed: 14,
|
|
56
|
+
bounciness: 8,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(config.subspace_map[SUBSPACE_ID].animation_map[ANIMATION_ID].config).toEqual({
|
|
63
|
+
toValue: 1,
|
|
64
|
+
friction: 5,
|
|
65
|
+
tension: 200,
|
|
66
|
+
})
|
|
67
|
+
const warning = warnSpy.mock.calls[0][0]
|
|
68
|
+
expect(warning).toContain('Resolved animation spring config')
|
|
69
|
+
expect(warning).toContain('Button Press Out')
|
|
70
|
+
expect(warning).toContain('btnPressOut')
|
|
71
|
+
expect(warning).toContain('transform.scale')
|
|
72
|
+
expect(warning).toContain('Main Subspace')
|
|
73
|
+
expect(warning).not.toContain('ANIMATION_')
|
|
74
|
+
expect(warning).not.toContain('SUBSPACE_')
|
|
75
|
+
} finally {
|
|
76
|
+
warnSpy.mockRestore()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('rejects invalid animation property', async () => {
|
|
81
|
+
await expect(
|
|
82
|
+
compile(
|
|
83
|
+
makeApp([
|
|
84
|
+
{
|
|
85
|
+
__typename: 'Animation',
|
|
86
|
+
id: ANIMATION_ID,
|
|
87
|
+
property: 'transform.skewX',
|
|
88
|
+
config: {
|
|
89
|
+
__type: 'AnimationTimingConfig',
|
|
90
|
+
toValue: 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
]),
|
|
94
|
+
),
|
|
95
|
+
).rejects.toThrow(/Invalid animation property/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('rejects invalid animation config type', async () => {
|
|
99
|
+
await expect(
|
|
100
|
+
compile(
|
|
101
|
+
makeApp([
|
|
102
|
+
{
|
|
103
|
+
__typename: 'Animation',
|
|
104
|
+
id: ANIMATION_ID,
|
|
105
|
+
property: 'opacity',
|
|
106
|
+
config: {
|
|
107
|
+
__type: 'AnimationUnknownConfig',
|
|
108
|
+
toValue: 1,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
]),
|
|
112
|
+
),
|
|
113
|
+
).rejects.toThrow(/Invalid animation config type/)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -327,6 +327,29 @@ export const templateActionNameMap = {
|
|
|
327
327
|
animated: 'BRICK_MAPS_ANIMATED',
|
|
328
328
|
},
|
|
329
329
|
},
|
|
330
|
+
BRICK_SKETCH: {
|
|
331
|
+
BRICK_SKETCH_SET_TOOL: {
|
|
332
|
+
tool: 'BRICK_SKETCH_TOOL',
|
|
333
|
+
},
|
|
334
|
+
BRICK_SKETCH_SET_COLOR: {
|
|
335
|
+
color: 'BRICK_SKETCH_COLOR',
|
|
336
|
+
},
|
|
337
|
+
BRICK_SKETCH_SET_STROKE_WIDTH: {
|
|
338
|
+
strokeWidth: 'BRICK_SKETCH_STROKE_WIDTH',
|
|
339
|
+
},
|
|
340
|
+
BRICK_SKETCH_IMPORT_STATE: {
|
|
341
|
+
stateJson: 'BRICK_SKETCH_STATE_JSON',
|
|
342
|
+
},
|
|
343
|
+
BRICK_SKETCH_EXPORT_IMAGE: {
|
|
344
|
+
imageFormat: 'BRICK_SKETCH_IMAGE_FORMAT',
|
|
345
|
+
imageQuality: 'BRICK_SKETCH_IMAGE_QUALITY',
|
|
346
|
+
},
|
|
347
|
+
BRICK_SKETCH_ADD_STROKE: {
|
|
348
|
+
strokePoints: 'BRICK_SKETCH_STROKE_POINTS',
|
|
349
|
+
strokeColor: 'BRICK_SKETCH_STROKE_COLOR',
|
|
350
|
+
strokeWidth: 'BRICK_SKETCH_STROKE_WIDTH',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
330
353
|
|
|
331
354
|
GENERATOR_FILE: {
|
|
332
355
|
GENERATOR_FILE_READ_CONTENT: {
|
package/compile/index.ts
CHANGED
|
@@ -301,6 +301,162 @@ const animationTypeMap = {
|
|
|
301
301
|
AnimationTimingConfig: 'timing',
|
|
302
302
|
AnimationSpringConfig: 'spring',
|
|
303
303
|
AnimationDecayConfig: 'decay',
|
|
304
|
+
} as const
|
|
305
|
+
|
|
306
|
+
type CompiledAnimationType = (typeof animationTypeMap)[keyof typeof animationTypeMap]
|
|
307
|
+
type WarningMetadata = Record<string, unknown>
|
|
308
|
+
|
|
309
|
+
const animationProperties = new Set([
|
|
310
|
+
'transform.translateX',
|
|
311
|
+
'transform.translateY',
|
|
312
|
+
'transform.scale',
|
|
313
|
+
'transform.scaleX',
|
|
314
|
+
'transform.scaleY',
|
|
315
|
+
'transform.rotate',
|
|
316
|
+
'transform.rotateX',
|
|
317
|
+
'transform.rotateY',
|
|
318
|
+
'opacity',
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
const animationComposeTypes = new Set(['parallel', 'sequence'])
|
|
322
|
+
const springConfigFamilies = [
|
|
323
|
+
['stiffness', 'damping', 'mass'],
|
|
324
|
+
['tension', 'friction'],
|
|
325
|
+
['bounciness', 'speed'],
|
|
326
|
+
]
|
|
327
|
+
const springConfigFamilyKeys = new Set(springConfigFamilies.flat())
|
|
328
|
+
|
|
329
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
330
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
331
|
+
|
|
332
|
+
const hasDefinedConfigValue = (config: Record<string, unknown>, key: string) =>
|
|
333
|
+
config[key] !== undefined
|
|
334
|
+
|
|
335
|
+
const assertConfigValue = (
|
|
336
|
+
config: Record<string, unknown>,
|
|
337
|
+
key: string,
|
|
338
|
+
errorReference: string,
|
|
339
|
+
) => {
|
|
340
|
+
if (!hasDefinedConfigValue(config, key)) {
|
|
341
|
+
throw new Error(`Invalid animation config ${errorReference}: missing "${key}"`)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const assertAnimationProperty = (property: unknown, errorReference: string) => {
|
|
346
|
+
if (typeof property !== 'string' || !animationProperties.has(property)) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Invalid animation property${errorReference ? ` ${errorReference}` : ''}: ${String(
|
|
349
|
+
property,
|
|
350
|
+
)}`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return property
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const getAnimationType = (config: unknown, errorReference: string): CompiledAnimationType => {
|
|
357
|
+
if (!isRecord(config)) {
|
|
358
|
+
throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const animationType = animationTypeMap[config.__type as keyof typeof animationTypeMap]
|
|
362
|
+
if (!animationType) {
|
|
363
|
+
throw new Error(`Invalid animation config type ${errorReference}: ${String(config.__type)}`)
|
|
364
|
+
}
|
|
365
|
+
return animationType
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const assertAnimationComposeType = (composeType: unknown, errorReference: string) => {
|
|
369
|
+
if (typeof composeType !== 'string' || !animationComposeTypes.has(composeType)) {
|
|
370
|
+
throw new Error(`Invalid animation compose type ${errorReference}: ${String(composeType)}`)
|
|
371
|
+
}
|
|
372
|
+
return composeType
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pickDefinedConfigValues = (config: Record<string, unknown>, keys: string[]) =>
|
|
376
|
+
keys.reduce((acc, key) => {
|
|
377
|
+
if (hasDefinedConfigValue(config, key)) acc[key] = config[key]
|
|
378
|
+
return acc
|
|
379
|
+
}, {})
|
|
380
|
+
|
|
381
|
+
const getDefinedConfigKeys = (config: Record<string, unknown>, keys: string[]) =>
|
|
382
|
+
keys.filter((key) => hasDefinedConfigValue(config, key))
|
|
383
|
+
|
|
384
|
+
const formatWarningMetadata = (metadata: WarningMetadata = {}) =>
|
|
385
|
+
Object.entries(metadata)
|
|
386
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
387
|
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
388
|
+
.join(', ')
|
|
389
|
+
|
|
390
|
+
const formatWarningReference = (metadata?: WarningMetadata) => {
|
|
391
|
+
const metadataText = formatWarningMetadata(metadata)
|
|
392
|
+
return metadataText ? ` [${metadataText}]` : ''
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const normalizeSpringConfig = (
|
|
396
|
+
config: Record<string, unknown>,
|
|
397
|
+
errorReference: string,
|
|
398
|
+
warningMetadata?: WarningMetadata,
|
|
399
|
+
): Record<string, unknown> => {
|
|
400
|
+
assertConfigValue(config, 'toValue', errorReference)
|
|
401
|
+
|
|
402
|
+
const usedFamilies = springConfigFamilies.filter((keys) =>
|
|
403
|
+
keys.some((key) => hasDefinedConfigValue(config, key)),
|
|
404
|
+
)
|
|
405
|
+
if (usedFamilies.length <= 1) return config
|
|
406
|
+
|
|
407
|
+
const configWithoutSpringFamily = Object.entries(config).reduce((acc, [key, value]) => {
|
|
408
|
+
if (!springConfigFamilyKeys.has(key)) acc[key] = value
|
|
409
|
+
return acc
|
|
410
|
+
}, {})
|
|
411
|
+
|
|
412
|
+
// Match runtime normalization: physical spring values are most explicit,
|
|
413
|
+
// otherwise preserve BRICKS' historical tension/friction controls.
|
|
414
|
+
const resolvedFamily =
|
|
415
|
+
usedFamilies.find((keys) => keys.includes('stiffness')) ||
|
|
416
|
+
usedFamilies.find((keys) => keys.includes('tension')) ||
|
|
417
|
+
usedFamilies[0]
|
|
418
|
+
const resolvedFamilyKeys = getDefinedConfigKeys(config, resolvedFamily)
|
|
419
|
+
const droppedFamilyKeys = usedFamilies
|
|
420
|
+
.filter((keys) => keys !== resolvedFamily)
|
|
421
|
+
.flatMap((keys) => getDefinedConfigKeys(config, keys))
|
|
422
|
+
|
|
423
|
+
console.warn(
|
|
424
|
+
`[Warning] Resolved animation spring config${formatWarningReference(
|
|
425
|
+
warningMetadata,
|
|
426
|
+
)}: using ${resolvedFamilyKeys.join('/')}, dropping ${droppedFamilyKeys.join('/')}`,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
...configWithoutSpringFamily,
|
|
431
|
+
...pickDefinedConfigValues(config, resolvedFamily),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const compileAnimationConfig = (
|
|
436
|
+
animationType: CompiledAnimationType,
|
|
437
|
+
config: unknown,
|
|
438
|
+
errorReference: string,
|
|
439
|
+
warningMetadata?: WarningMetadata,
|
|
440
|
+
) => {
|
|
441
|
+
if (!isRecord(config)) {
|
|
442
|
+
throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const compiledConfig = compileProperty(omit(config, '__type'), errorReference)
|
|
446
|
+
|
|
447
|
+
if (!isRecord(compiledConfig)) {
|
|
448
|
+
throw new Error(`Invalid animation config ${errorReference}: config must compile to an object`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (animationType === 'timing') {
|
|
452
|
+
assertConfigValue(compiledConfig, 'toValue', errorReference)
|
|
453
|
+
} else if (animationType === 'spring') {
|
|
454
|
+
return normalizeSpringConfig(compiledConfig, errorReference, warningMetadata)
|
|
455
|
+
} else if (animationType === 'decay') {
|
|
456
|
+
assertConfigValue(compiledConfig, 'velocity', errorReference)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return compiledConfig
|
|
304
460
|
}
|
|
305
461
|
|
|
306
462
|
const compileFrame = (frame: Canvas['items'][number]['frame']) => ({
|
|
@@ -629,39 +785,63 @@ export const compile = async (app: Application) => {
|
|
|
629
785
|
`(animation index: ${animationIndex}, subspace: ${subspaceId})`,
|
|
630
786
|
)
|
|
631
787
|
|
|
632
|
-
|
|
788
|
+
const animationTypename = animation.__typename
|
|
789
|
+
if (animationTypename === 'Animation') {
|
|
633
790
|
const animationDef = animation as AnimationDef
|
|
791
|
+
const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
|
|
792
|
+
const animationWarningMetadata = {
|
|
793
|
+
animationIndex,
|
|
794
|
+
animationTitle: animationDef.title,
|
|
795
|
+
animationAlias: animationDef.alias,
|
|
796
|
+
animationProperty: animationDef.property,
|
|
797
|
+
subspaceIndex,
|
|
798
|
+
subspaceTitle: subspace.title,
|
|
799
|
+
}
|
|
800
|
+
const animationType = getAnimationType(animationDef.config, animationErrorReference)
|
|
634
801
|
map[animationId] = {
|
|
635
802
|
alias: animationDef.alias,
|
|
636
803
|
title: animationDef.title,
|
|
637
804
|
description: animationDef.description,
|
|
638
805
|
hide_short_ref: animationDef.hideShortRef,
|
|
639
806
|
animationRunType: animationDef.runType,
|
|
640
|
-
property: animationDef.property,
|
|
641
|
-
type:
|
|
642
|
-
config:
|
|
643
|
-
|
|
644
|
-
|
|
807
|
+
property: assertAnimationProperty(animationDef.property, animationErrorReference),
|
|
808
|
+
type: animationType,
|
|
809
|
+
config: compileAnimationConfig(
|
|
810
|
+
animationType,
|
|
811
|
+
animationDef.config,
|
|
812
|
+
animationErrorReference,
|
|
813
|
+
animationWarningMetadata,
|
|
645
814
|
),
|
|
646
815
|
}
|
|
647
|
-
} else if (
|
|
816
|
+
} else if (animationTypename === 'AnimationCompose') {
|
|
648
817
|
const animationDef = animation as AnimationComposeDef
|
|
818
|
+
const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
|
|
649
819
|
map[animationId] = {
|
|
650
820
|
alias: animationDef.alias,
|
|
651
821
|
title: animationDef.title,
|
|
652
822
|
description: animationDef.description,
|
|
653
823
|
hide_short_ref: animationDef.hideShortRef,
|
|
654
824
|
animationRunType: animationDef.runType,
|
|
655
|
-
compose_type:
|
|
825
|
+
compose_type: assertAnimationComposeType(
|
|
826
|
+
animationDef.composeType,
|
|
827
|
+
animationErrorReference,
|
|
828
|
+
),
|
|
656
829
|
item_list: animationDef.items.map((item, index) => {
|
|
657
830
|
const innerAnimation = item()
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
)
|
|
662
|
-
|
|
831
|
+
const innerAnimationId = assertEntryId(
|
|
832
|
+
innerAnimation?.id,
|
|
833
|
+
'ANIMATION',
|
|
834
|
+
`(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
|
|
835
|
+
)
|
|
836
|
+
return { animation_id: innerAnimationId }
|
|
663
837
|
}),
|
|
664
838
|
}
|
|
839
|
+
} else {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
|
|
842
|
+
animationTypename,
|
|
843
|
+
)}`,
|
|
844
|
+
)
|
|
665
845
|
}
|
|
666
846
|
return map
|
|
667
847
|
}, {}),
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.10",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
7
7
|
"build": "bun scripts/build.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
10
|
+
"@fugood/bricks-cli": "^2.25.0-beta.10",
|
|
11
11
|
"@huggingface/gguf": "^0.3.2",
|
|
12
12
|
"@iarna/toml": "^3.0.0",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"oxfmt": "^0.36.0"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "7b909dc28862872743cf0459ff65d16af67721c6"
|
|
29
29
|
}
|
|
@@ -41,12 +41,13 @@ const bounce: AnimationDef = {
|
|
|
41
41
|
toValue: 1,
|
|
42
42
|
friction: 7,
|
|
43
43
|
tension: 40,
|
|
44
|
-
speed: 12,
|
|
45
|
-
bounciness: 8,
|
|
46
44
|
},
|
|
47
45
|
}
|
|
48
46
|
```
|
|
49
47
|
|
|
48
|
+
Use one spring parameter family per config: `tension/friction`, `speed/bounciness`, or
|
|
49
|
+
`stiffness/damping/mass`.
|
|
50
|
+
|
|
50
51
|
### Decay Animation
|
|
51
52
|
Velocity-based deceleration animation.
|
|
52
53
|
|
|
@@ -20,16 +20,26 @@ When mobile devices or embedded systems lack hardware for local AI inference (LL
|
|
|
20
20
|
|
|
21
21
|
## Client Configuration
|
|
22
22
|
|
|
23
|
-
In generator properties, configure
|
|
23
|
+
In generator properties, configure `buttressConnectionSettings`:
|
|
24
24
|
|
|
25
25
|
| Setting | Description |
|
|
26
26
|
|---------|-------------|
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
27
|
+
| `enabled` | Toggle Buttress offloading |
|
|
28
|
+
| `autoDiscoverType` | `auto` (LAN auto-discovery, recommended) or `manual` (typed URL) |
|
|
29
|
+
| `url` | Server URL (`manual` mode only — leave empty for `auto`) |
|
|
30
|
+
| `fallbackType` | If Buttress is unreachable: `use-local` runs locally, `no-op` blocks the load |
|
|
31
|
+
| `strategy` | Execution preference (see below) |
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
The launcher provides the access token automatically — there is **no user-facing access-token setting**. In `auto` mode the launcher discovers servers bound to the device's workspace and uses its workspace-scoped JWT; in `manual` mode it sends the same JWT only when the typed URL belongs to a server bound to the same workspace (verified against the server's `/buttress/info`).
|
|
34
|
+
|
|
35
|
+
### `autoDiscoverType` options
|
|
36
|
+
|
|
37
|
+
| Mode | When to use | What the device does |
|
|
38
|
+
|------|-------------|----------------------|
|
|
39
|
+
| `auto` | LAN deployment with a workspace-bound buttress-server | Listens for UDP announcements and picks the strongest server bound to its workspace; reconnects automatically when the workspace flips |
|
|
40
|
+
| `manual` | Internet-reachable server, mixed networks, or a public/unbound server | Connects to the typed `url` directly; sends a token only when the server is bound to the same workspace |
|
|
41
|
+
|
|
42
|
+
### Strategy options
|
|
33
43
|
|
|
34
44
|
| Strategy | Description |
|
|
35
45
|
|----------|-------------|
|
|
@@ -37,7 +47,11 @@ In generator properties, configure Buttress settings:
|
|
|
37
47
|
| `prefer-buttress` | Use Buttress if available, fallback to local |
|
|
38
48
|
| `prefer-best` | Auto-select based on capability comparison |
|
|
39
49
|
|
|
40
|
-
## Generator Configuration
|
|
50
|
+
## Generator Configuration Examples
|
|
51
|
+
|
|
52
|
+
### Auto-discovery (recommended)
|
|
53
|
+
|
|
54
|
+
Workspace-bound launcher + workspace-bound buttress-server on the same LAN. No URL needed — discovery picks the highest-scoring server that runs the requested backend.
|
|
41
55
|
|
|
42
56
|
```typescript
|
|
43
57
|
import { makeId } from 'bricks-ctor'
|
|
@@ -53,7 +67,7 @@ const llmGenerator: GeneratorLLM = {
|
|
|
53
67
|
contextSize: 8192,
|
|
54
68
|
buttressConnectionSettings: {
|
|
55
69
|
enabled: true,
|
|
56
|
-
|
|
70
|
+
autoDiscoverType: 'auto',
|
|
57
71
|
fallbackType: 'use-local',
|
|
58
72
|
strategy: 'prefer-best',
|
|
59
73
|
},
|
|
@@ -63,6 +77,44 @@ const llmGenerator: GeneratorLLM = {
|
|
|
63
77
|
}
|
|
64
78
|
```
|
|
65
79
|
|
|
80
|
+
### Manual URL
|
|
81
|
+
|
|
82
|
+
Use when the server isn't on the launcher's broadcast domain (cross-subnet, internet, behind a reverse proxy) or you want to point at a specific public/unbound server.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
buttressConnectionSettings: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
autoDiscoverType: 'manual',
|
|
88
|
+
url: 'http://192.168.1.100:2080',
|
|
89
|
+
fallbackType: 'use-local',
|
|
90
|
+
strategy: 'prefer-best',
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> ⚠️ With `fallbackType: 'no-op'` the generator refuses to load locally if Buttress is unreachable. Use `'use-local'` whenever the device can also run the model standalone.
|
|
95
|
+
|
|
96
|
+
## Integrating a discovered buttress-server
|
|
97
|
+
|
|
98
|
+
The ctor-desktop "Local Devices" panel (and the `bricks buttress scan` CLI) can hand you a server's identity, workspace, and announced generator caps. When the user asks to integrate one:
|
|
99
|
+
|
|
100
|
+
1. **Confirm workspace match.** The discovery message says whether the server's workspace matches this project's. If it doesn't, do NOT add the integration — instruct the user to run `bricks buttress unbind && bricks buttress bind` from this workspace's owner CLI on the server host. The launcher won't trust a cross-workspace server.
|
|
101
|
+
|
|
102
|
+
2. **Map announced types to generator templates.** For each `generators[].type` in the announce, target the matching templateKey (create the generator if absent):
|
|
103
|
+
|
|
104
|
+
| Announced type | templateKey | Default model |
|
|
105
|
+
|----------------|------------------------------|---------------|
|
|
106
|
+
| `ggml-llm` | `GENERATOR_LLM` | `ggml-org/gemma-3-12b-it-qat-GGUF` (≥20 GB usable) or `ggml-org/gpt-oss-20b-GGUF` (~12 GB usable) |
|
|
107
|
+
| `mlx-llm` | `GENERATOR_MLX_LLM` | `mlx-community/Qwen3-4B-4bit` (or a larger 4-bit quant if `usableBytes` allows) |
|
|
108
|
+
| `ggml-stt` | `GENERATOR_SPEECH_INFERENCE` | `BricksDisplay/whisper-ggml` (filename `ggml-small-q8_0.bin`) |
|
|
109
|
+
|
|
110
|
+
Pick a model that fits the announced `usableBytes`. Set `contextSize` to match.
|
|
111
|
+
|
|
112
|
+
3. **Use the canonical auto-discovery config** from "Auto-discovery (recommended)" above. Don't switch to manual mode just because the server is on the LAN — auto mode picks the workspace-bound server automatically and rebinds when the device's workspace flips.
|
|
113
|
+
|
|
114
|
+
### Real-device caveat
|
|
115
|
+
|
|
116
|
+
Buttress LAN auto-discovery uses native UDP via `react-native-jsi-udp`. The iOS Simulator silently fails to bind UDP, so auto mode is a no-op there. With `fallbackType: 'use-local'` the generator falls back to local inference in the simulator — dev/test stays functional. **Validate the buttress path itself only when deploying to a real Foundation device; don't gate the build on simulator validation.**
|
|
117
|
+
|
|
66
118
|
## Server Setup
|
|
67
119
|
|
|
68
120
|
### Requirements
|
|
@@ -98,6 +150,43 @@ bricks-buttress --config ./config.toml
|
|
|
98
150
|
|----------|-------------|
|
|
99
151
|
| `HF_TOKEN` | Hugging Face token for model downloads |
|
|
100
152
|
| `ENABLE_OPENAI_COMPAT_ENDPOINT` | Set to `1` for OpenAI-compatible API |
|
|
153
|
+
| `BRICKS_BUTTRESS_STATE_DIR` | Override the workspace state directory (default `~/.bricks-cli/buttress`) |
|
|
154
|
+
|
|
155
|
+
## Bind the Server to a Workspace
|
|
156
|
+
|
|
157
|
+
To use `autoDiscoverType: 'auto'` — and to require workspace-scoped JWT auth — pair the buttress-server with a BRICKS workspace using the `bricks buttress` CLI commands. An unbound server stays in legacy public mode and accepts any LAN client.
|
|
158
|
+
|
|
159
|
+
### One-time bind
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Log into the cloud bricks-server with the workspace owner's account
|
|
163
|
+
bricks auth login
|
|
164
|
+
|
|
165
|
+
# Pair the buttress-server running on this machine with the active workspace
|
|
166
|
+
bricks buttress bind
|
|
167
|
+
|
|
168
|
+
# Restart bricks-buttress so it picks up the new state.json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`bricks buttress bind` writes `~/.bricks-cli/buttress/state.json` containing the workspace id, the issuer's Ed25519 public key, and a stable `serverId` (defaults to `buttress-<machineId>`). Override location with `--state-dir` or `$BRICKS_BUTTRESS_STATE_DIR`. For headless setups, use `bricks buttress bind --print` to emit state.json to stdout.
|
|
172
|
+
|
|
173
|
+
### Verify and manage
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Show local binding + workspace-side bound list
|
|
177
|
+
bricks buttress status
|
|
178
|
+
|
|
179
|
+
# Discover buttress-servers on the LAN (with version, auth state, generator caps)
|
|
180
|
+
bricks buttress scan
|
|
181
|
+
|
|
182
|
+
# Issue a long-lived workspace access token (CI / ctor agents that already hold a workspace token)
|
|
183
|
+
bricks buttress issue-token --ttl 86400
|
|
184
|
+
|
|
185
|
+
# Detach this server from the workspace
|
|
186
|
+
bricks buttress unbind
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
After binding, launchers in the same workspace will auto-discover this server; manual-mode generators will only send their access token if the server's reported workspace matches their own.
|
|
101
190
|
|
|
102
191
|
## Server Configuration (TOML)
|
|
103
192
|
|
package/tools/preview-main.mjs
CHANGED
|
@@ -6,6 +6,12 @@ import { createServer } from 'http'
|
|
|
6
6
|
import { parseArgs } from 'util'
|
|
7
7
|
import * as TOON from '@toon-format/toon'
|
|
8
8
|
|
|
9
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
10
|
+
stream.on('error', (err) => {
|
|
11
|
+
if (err.code !== 'EPIPE') throw err
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
const { values } = parseArgs({
|
|
10
16
|
args: process.argv,
|
|
11
17
|
options: {
|
|
@@ -224,16 +230,23 @@ app.on('ready', () => {
|
|
|
224
230
|
)
|
|
225
231
|
if (takeScreenshotConfig) {
|
|
226
232
|
const { delay, width, height, path } = takeScreenshotConfig
|
|
227
|
-
setTimeout(() => {
|
|
233
|
+
setTimeout(async () => {
|
|
234
|
+
let screenshotFailed = false
|
|
228
235
|
console.log('Taking screenshot')
|
|
229
|
-
|
|
236
|
+
try {
|
|
237
|
+
const image = await mainWindow.webContents.capturePage()
|
|
230
238
|
console.log('Writing screenshot to', path)
|
|
231
|
-
writeFile(path, image.resize({ width, height }).toJPEG(75))
|
|
239
|
+
await writeFile(path, image.resize({ width, height }).toJPEG(75))
|
|
240
|
+
} catch (err) {
|
|
241
|
+
screenshotFailed = true
|
|
242
|
+
console.error('Screenshot capture/write failed', err)
|
|
243
|
+
} finally {
|
|
232
244
|
if (noKeepOpen) {
|
|
233
245
|
console.log('Closing app')
|
|
234
|
-
app.
|
|
246
|
+
if (screenshotFailed) app.exit(1)
|
|
247
|
+
else app.quit()
|
|
235
248
|
}
|
|
236
|
-
}
|
|
249
|
+
}
|
|
237
250
|
}, delay)
|
|
238
251
|
}
|
|
239
252
|
}
|
package/types/animation.ts
CHANGED
|
@@ -44,10 +44,21 @@ export interface AnimationTimingConfig {
|
|
|
44
44
|
export interface AnimationSpringConfig {
|
|
45
45
|
__type: 'AnimationSpringConfig'
|
|
46
46
|
toValue: number // BRICKS Grid unit
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Use one spring parameter family: tension/friction, speed/bounciness,
|
|
48
|
+
// or stiffness/damping/mass.
|
|
49
|
+
friction?: number
|
|
50
|
+
tension?: number
|
|
51
|
+
speed?: number
|
|
52
|
+
bounciness?: number
|
|
53
|
+
stiffness?: number
|
|
54
|
+
damping?: number
|
|
55
|
+
mass?: number
|
|
56
|
+
velocity?: number
|
|
57
|
+
delay?: number
|
|
58
|
+
isInteraction?: boolean
|
|
59
|
+
overshootClamping?: boolean
|
|
60
|
+
restDisplacementThreshold?: number
|
|
61
|
+
restSpeedThreshold?: number
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export interface AnimationDecayConfig {
|
|
@@ -62,7 +73,7 @@ export interface AnimationDef {
|
|
|
62
73
|
__typename: 'Animation'
|
|
63
74
|
id: string
|
|
64
75
|
alias?: string
|
|
65
|
-
title
|
|
76
|
+
title?: string
|
|
66
77
|
description?: string
|
|
67
78
|
hideShortRef?: boolean
|
|
68
79
|
runType?: 'once' | 'loop'
|
package/types/bricks/Image.ts
CHANGED
|
@@ -25,6 +25,10 @@ Default property:
|
|
|
25
25
|
"templateType": "${}",
|
|
26
26
|
"fadeDuration": 0,
|
|
27
27
|
"blurBackgroundRadius": 8,
|
|
28
|
+
"imageFilterEnabled": false,
|
|
29
|
+
"imageFilterBlur": 0,
|
|
30
|
+
"imageFilterBlurMode": "clamp",
|
|
31
|
+
"imageFilterColorMatrix": [],
|
|
28
32
|
"loadSystemIos": "auto",
|
|
29
33
|
"loadSystemAndroid": "auto"
|
|
30
34
|
}
|
|
@@ -50,6 +54,14 @@ Default property:
|
|
|
50
54
|
enableBlurBackground?: boolean | DataLink
|
|
51
55
|
/* The blur radius of the blur filter added to the image background */
|
|
52
56
|
blurBackgroundRadius?: number | DataLink
|
|
57
|
+
/* Enable Skia image filters. When disabled, Image uses the normal platform image renderer. */
|
|
58
|
+
imageFilterEnabled?: boolean | DataLink
|
|
59
|
+
/* Blur amount for the Skia image filter. */
|
|
60
|
+
imageFilterBlur?: number | DataLink
|
|
61
|
+
/* Tile mode for the Skia blur image filter. */
|
|
62
|
+
imageFilterBlurMode?: 'clamp' | 'decal' | 'repeat' | 'mirror' | DataLink
|
|
63
|
+
/* Optional 4x5 color matrix for the Skia image filter. Provide 20 numbers. */
|
|
64
|
+
imageFilterColorMatrix?: Array<number | DataLink> | DataLink
|
|
53
65
|
/* [iOS] The use priority of image loading system (Auto: sdwebimage, fallback to default if failed) */
|
|
54
66
|
loadSystemIos?: 'auto' | 'sdwebimage' | 'default' | DataLink
|
|
55
67
|
/* [Android] The use priority of image loading system (Auto: glide, fallback to fresco if failed) */
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* Auto generated by build script
|
|
2
|
+
*
|
|
3
|
+
* Drawing canvas with undo/redo, import/export state, and image export
|
|
4
|
+
*/
|
|
5
|
+
import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from '../switch'
|
|
6
|
+
import type { Data, DataLink } from '../data'
|
|
7
|
+
import type { Animation, AnimationBasicEvents } from '../animation'
|
|
8
|
+
import type {
|
|
9
|
+
Brick,
|
|
10
|
+
EventAction,
|
|
11
|
+
EventActionForItem,
|
|
12
|
+
ActionWithDataParams,
|
|
13
|
+
ActionWithParams,
|
|
14
|
+
Action,
|
|
15
|
+
EventProperty,
|
|
16
|
+
} from '../common'
|
|
17
|
+
import type { BrickBasicProperty, BrickBasicEvents, BrickBasicEventsForItem } from '../brick-base'
|
|
18
|
+
import type { TemplateEventPropsMap } from '../../utils/event-props'
|
|
19
|
+
|
|
20
|
+
/* Undo the last stroke */
|
|
21
|
+
export type BrickSketchActionUndo = Action & {
|
|
22
|
+
__actionName: 'BRICK_SKETCH_UNDO'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Redo the last undone stroke */
|
|
26
|
+
export type BrickSketchActionRedo = Action & {
|
|
27
|
+
__actionName: 'BRICK_SKETCH_REDO'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Clear all strokes from the canvas */
|
|
31
|
+
export type BrickSketchActionClear = Action & {
|
|
32
|
+
__actionName: 'BRICK_SKETCH_CLEAR'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Set the active tool */
|
|
36
|
+
export type BrickSketchActionSetTool = ActionWithParams & {
|
|
37
|
+
__actionName: 'BRICK_SKETCH_SET_TOOL'
|
|
38
|
+
params?: Array<{
|
|
39
|
+
input: 'tool'
|
|
40
|
+
value?: 'pen' | 'eraser' | DataLink | EventProperty
|
|
41
|
+
mapping?: string
|
|
42
|
+
}>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Set the stroke color */
|
|
46
|
+
export type BrickSketchActionSetColor = ActionWithParams & {
|
|
47
|
+
__actionName: 'BRICK_SKETCH_SET_COLOR'
|
|
48
|
+
params?: Array<{
|
|
49
|
+
input: 'color'
|
|
50
|
+
value?: string | DataLink | EventProperty
|
|
51
|
+
mapping?: string
|
|
52
|
+
}>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Set the stroke width (px) */
|
|
56
|
+
export type BrickSketchActionSetStrokeWidth = ActionWithParams & {
|
|
57
|
+
__actionName: 'BRICK_SKETCH_SET_STROKE_WIDTH'
|
|
58
|
+
params?: Array<{
|
|
59
|
+
input: 'strokeWidth'
|
|
60
|
+
value?: number | DataLink | EventProperty
|
|
61
|
+
mapping?: string
|
|
62
|
+
}>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Import sketch state (JSON string from a previous export) */
|
|
66
|
+
export type BrickSketchActionImportState = ActionWithParams & {
|
|
67
|
+
__actionName: 'BRICK_SKETCH_IMPORT_STATE'
|
|
68
|
+
params?: Array<{
|
|
69
|
+
input: 'stateJson'
|
|
70
|
+
value?: string | DataLink | EventProperty
|
|
71
|
+
mapping?: string
|
|
72
|
+
}>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Export the current sketch state to the BRICK_SKETCH_STATE outlet */
|
|
76
|
+
export type BrickSketchActionExportState = Action & {
|
|
77
|
+
__actionName: 'BRICK_SKETCH_EXPORT_STATE'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Export the canvas as an image (file URI emitted to BRICK_SKETCH_IMAGE_URI) */
|
|
81
|
+
export type BrickSketchActionExportImage = ActionWithParams & {
|
|
82
|
+
__actionName: 'BRICK_SKETCH_EXPORT_IMAGE'
|
|
83
|
+
params?: Array<
|
|
84
|
+
| {
|
|
85
|
+
input: 'imageFormat'
|
|
86
|
+
value?: 'png' | 'jpeg' | DataLink | EventProperty
|
|
87
|
+
mapping?: string
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
input: 'imageQuality'
|
|
91
|
+
value?: number | DataLink | EventProperty
|
|
92
|
+
mapping?: string
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Add a stroke programmatically */
|
|
98
|
+
export type BrickSketchActionAddStroke = ActionWithParams & {
|
|
99
|
+
__actionName: 'BRICK_SKETCH_ADD_STROKE'
|
|
100
|
+
params?: Array<
|
|
101
|
+
| {
|
|
102
|
+
input: 'strokePoints'
|
|
103
|
+
value?: any | EventProperty
|
|
104
|
+
mapping?: string
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
input: 'strokeColor'
|
|
108
|
+
value?: string | DataLink | EventProperty
|
|
109
|
+
mapping?: string
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
input: 'strokeWidth'
|
|
113
|
+
value?: number | DataLink | EventProperty
|
|
114
|
+
mapping?: string
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface BrickSketchDef {
|
|
120
|
+
/*
|
|
121
|
+
Default property:
|
|
122
|
+
{
|
|
123
|
+
"tool": "pen",
|
|
124
|
+
"strokeColor": "#000000",
|
|
125
|
+
"strokeWidth": 3,
|
|
126
|
+
"pressureSensitive": true,
|
|
127
|
+
"layoutType": "cover",
|
|
128
|
+
"scale": 1,
|
|
129
|
+
"mode": "pen",
|
|
130
|
+
"theme": "auto",
|
|
131
|
+
"backgroundPattern": "dot",
|
|
132
|
+
"hideToolbar": false,
|
|
133
|
+
"hideUndo": false,
|
|
134
|
+
"hideRedo": false,
|
|
135
|
+
"hidePencilTool": false,
|
|
136
|
+
"hideColorPicker": false,
|
|
137
|
+
"hideThickness": false,
|
|
138
|
+
"availableColors": [
|
|
139
|
+
"#000000",
|
|
140
|
+
"#ffffff",
|
|
141
|
+
"#e53935",
|
|
142
|
+
"#fb8c00",
|
|
143
|
+
"#fdd835",
|
|
144
|
+
"#43a047",
|
|
145
|
+
"#1e88e5",
|
|
146
|
+
"#8e24aa"
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
*/
|
|
150
|
+
property?: BrickBasicProperty & {
|
|
151
|
+
/* Initial sketch state to load on mount (object or JSON string from a previous export). When `BRICK_SKETCH_STATE` outlet feeds the same value back via binding, the echo is detected and ignored to avoid an update loop. */
|
|
152
|
+
initialState?: string | DataLink | DataLink | {} | DataLink
|
|
153
|
+
/* Drawing tool */
|
|
154
|
+
tool?: 'pen' | 'eraser' | DataLink
|
|
155
|
+
/* Stroke color */
|
|
156
|
+
strokeColor?: string | DataLink
|
|
157
|
+
/* Stroke width (px) */
|
|
158
|
+
strokeWidth?: number | DataLink
|
|
159
|
+
/* Vary stroke width by pen pressure */
|
|
160
|
+
pressureSensitive?: boolean | DataLink
|
|
161
|
+
/* Canvas layout. `cover` fits the brick frame; `extend` lets the canvas grow with content */
|
|
162
|
+
layoutType?: 'cover' | 'extend' | DataLink
|
|
163
|
+
/* Canvas resolution scale (device-pixel multiplier) */
|
|
164
|
+
scale?: number | DataLink
|
|
165
|
+
/* Interaction mode. `pen` = drawing (scroll disabled), `scroll` = scroll/pan (drawing disabled) */
|
|
166
|
+
mode?: 'pen' | 'scroll' | DataLink
|
|
167
|
+
/* Toolbar/UI theme. `auto` follows the system color scheme; `light`/`dark` force the palette. */
|
|
168
|
+
theme?: 'auto' | 'light' | 'dark' | DataLink
|
|
169
|
+
/* Background color of the canvas */
|
|
170
|
+
backgroundColor?: string | DataLink
|
|
171
|
+
/* Background image URL (covers the canvas; rendered behind strokes) */
|
|
172
|
+
backgroundImage?: string | DataLink
|
|
173
|
+
/* Decorative background pattern when no background image is provided */
|
|
174
|
+
backgroundPattern?: 'dot' | 'grid' | 'none' | DataLink
|
|
175
|
+
/* Hide the entire built-in toolbar */
|
|
176
|
+
hideToolbar?: boolean | DataLink
|
|
177
|
+
/* Hide the undo button in the toolbar */
|
|
178
|
+
hideUndo?: boolean | DataLink
|
|
179
|
+
/* Hide the redo button in the toolbar */
|
|
180
|
+
hideRedo?: boolean | DataLink
|
|
181
|
+
/* Hide the pen/eraser tool toggle */
|
|
182
|
+
hidePencilTool?: boolean | DataLink
|
|
183
|
+
/* Hide the color picker */
|
|
184
|
+
hideColorPicker?: boolean | DataLink
|
|
185
|
+
/* Hide the stroke thickness slider */
|
|
186
|
+
hideThickness?: boolean | DataLink
|
|
187
|
+
/* Colors shown in the built-in color picker */
|
|
188
|
+
availableColors?: Array<string | DataLink> | DataLink
|
|
189
|
+
}
|
|
190
|
+
events?: BrickBasicEvents & {
|
|
191
|
+
/* A stroke was just committed (drawn or added programmatically) */
|
|
192
|
+
onStrokeEnd?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStrokeEnd']>>
|
|
193
|
+
/* The canvas was cleared */
|
|
194
|
+
onClear?: Array<EventAction>
|
|
195
|
+
/* Sketch state changed (any commit, undo, redo, clear, or import) */
|
|
196
|
+
onStateChange?: Array<EventAction>
|
|
197
|
+
/* Active tool changed */
|
|
198
|
+
onToolChange?: Array<
|
|
199
|
+
EventAction<string & keyof TemplateEventPropsMap['Sketch']['onToolChange']>
|
|
200
|
+
>
|
|
201
|
+
/* Image export finished */
|
|
202
|
+
onExportImage?: Array<
|
|
203
|
+
EventAction<string & keyof TemplateEventPropsMap['Sketch']['onExportImage']>
|
|
204
|
+
>
|
|
205
|
+
}
|
|
206
|
+
outlets?: {
|
|
207
|
+
/* Current sketch state (strokes, settings, undo/redo stacks) */
|
|
208
|
+
state?: () => Data<{
|
|
209
|
+
strokes?: any[]
|
|
210
|
+
undone?: any[]
|
|
211
|
+
settings?: { [key: string]: any }
|
|
212
|
+
[key: string]: any
|
|
213
|
+
}>
|
|
214
|
+
/* URI of the most recently exported image */
|
|
215
|
+
imageUri?: () => Data<string>
|
|
216
|
+
/* Most recently committed stroke (points, color, width) */
|
|
217
|
+
lastStroke?: () => Data<{
|
|
218
|
+
color?: string
|
|
219
|
+
width?: number
|
|
220
|
+
tool?: string
|
|
221
|
+
points?: any[]
|
|
222
|
+
[key: string]: any
|
|
223
|
+
}>
|
|
224
|
+
}
|
|
225
|
+
animation?: AnimationBasicEvents & {
|
|
226
|
+
onStrokeEnd?: Animation
|
|
227
|
+
onClear?: Animation
|
|
228
|
+
onStateChange?: Animation
|
|
229
|
+
onToolChange?: Animation
|
|
230
|
+
onExportImage?: Animation
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Drawing canvas with undo/redo, import/export state, and image export */
|
|
235
|
+
export type BrickSketch = Brick &
|
|
236
|
+
BrickSketchDef & {
|
|
237
|
+
templateKey: 'BRICK_SKETCH'
|
|
238
|
+
switches?: Array<
|
|
239
|
+
SwitchDef &
|
|
240
|
+
BrickSketchDef & {
|
|
241
|
+
conds?: Array<{
|
|
242
|
+
method: '==' | '!=' | '>' | '<' | '>=' | '<='
|
|
243
|
+
cond:
|
|
244
|
+
| SwitchCondInnerStateCurrentCanvas
|
|
245
|
+
| SwitchCondData
|
|
246
|
+
| {
|
|
247
|
+
__typename: 'SwitchCondInnerStateOutlet'
|
|
248
|
+
outlet: 'state' | 'imageUri' | 'lastStroke'
|
|
249
|
+
value: any
|
|
250
|
+
}
|
|
251
|
+
}>
|
|
252
|
+
}
|
|
253
|
+
>
|
|
254
|
+
}
|
package/types/bricks/index.ts
CHANGED
package/types/data.ts
CHANGED
|
@@ -713,6 +713,7 @@ Default property:
|
|
|
713
713
|
| {
|
|
714
714
|
enabled?: boolean | DataLink
|
|
715
715
|
url?: string | DataLink
|
|
716
|
+
autoDiscoverType?: 'manual' | 'auto' | DataLink
|
|
716
717
|
fallbackType?: 'use-local' | 'no-op' | DataLink
|
|
717
718
|
strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
|
|
718
719
|
}
|
|
@@ -158,6 +158,7 @@ Default property:
|
|
|
158
158
|
| {
|
|
159
159
|
enabled?: boolean | DataLink
|
|
160
160
|
url?: string | DataLink
|
|
161
|
+
autoDiscoverType?: 'manual' | 'auto' | DataLink
|
|
161
162
|
fallbackType?: 'use-local' | 'no-op' | DataLink
|
|
162
163
|
strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
|
|
163
164
|
}
|
|
@@ -174,6 +174,7 @@ Default property:
|
|
|
174
174
|
isCapturing?: boolean
|
|
175
175
|
data?: {
|
|
176
176
|
result?: string
|
|
177
|
+
language?: string
|
|
177
178
|
[key: string]: any
|
|
178
179
|
}
|
|
179
180
|
vadEvent?: {
|
|
@@ -215,6 +216,7 @@ Default property:
|
|
|
215
216
|
isCapturing?: boolean
|
|
216
217
|
data?: {
|
|
217
218
|
result?: string
|
|
219
|
+
language?: string
|
|
218
220
|
[key: string]: any
|
|
219
221
|
}
|
|
220
222
|
vadEvent?: {
|
|
@@ -238,6 +240,10 @@ Default property:
|
|
|
238
240
|
}>
|
|
239
241
|
/* Stabilized transcription text from completed slices */
|
|
240
242
|
stabilizedText?: () => Data<string>
|
|
243
|
+
/* Latest detected language code (e.g. `en`, `zh`, `ja`) from any transcribe slice (best-effort, may flicker on short slices) */
|
|
244
|
+
detectedLanguage?: () => Data<string>
|
|
245
|
+
/* Detected language code of the most recently stabilized (finalized) slice */
|
|
246
|
+
stabilizedLanguage?: () => Data<string>
|
|
241
247
|
/* Audio output file path (auto-generated when saving audio) */
|
|
242
248
|
audioOutputPath?: () => Data<string>
|
|
243
249
|
/* Available microphone devices (Web / Desktop only) */
|
|
@@ -269,6 +275,8 @@ export type GeneratorRealtimeTranscription = Generator &
|
|
|
269
275
|
| 'lastTranscribeEvent'
|
|
270
276
|
| 'lastVadEvent'
|
|
271
277
|
| 'stabilizedText'
|
|
278
|
+
| 'detectedLanguage'
|
|
279
|
+
| 'stabilizedLanguage'
|
|
272
280
|
| 'audioOutputPath'
|
|
273
281
|
| 'devices'
|
|
274
282
|
value: any
|
|
@@ -323,6 +323,7 @@ Default property:
|
|
|
323
323
|
| {
|
|
324
324
|
enabled?: boolean | DataLink
|
|
325
325
|
url?: string | DataLink
|
|
326
|
+
autoDiscoverType?: 'manual' | 'auto' | DataLink
|
|
326
327
|
fallbackType?: 'use-local' | 'no-op' | DataLink
|
|
327
328
|
strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
|
|
328
329
|
}
|
|
@@ -358,9 +359,12 @@ Default property:
|
|
|
358
359
|
transcribeProgress?: () => Data<number>
|
|
359
360
|
/* Inference result */
|
|
360
361
|
transcribeResult?: () => Data<string>
|
|
362
|
+
/* Detected language code of the latest transcription (e.g. `en`, `zh`, `ja`) */
|
|
363
|
+
detectedLanguage?: () => Data<string>
|
|
361
364
|
/* Inference result details */
|
|
362
365
|
transcribeDetails?: () => Data<{
|
|
363
366
|
result?: string
|
|
367
|
+
language?: string
|
|
364
368
|
segments?: Array<{
|
|
365
369
|
start?: number
|
|
366
370
|
end?: number
|
|
@@ -405,6 +409,7 @@ export type GeneratorSpeechInference = Generator &
|
|
|
405
409
|
| 'isTranscribing'
|
|
406
410
|
| 'transcribeProgress'
|
|
407
411
|
| 'transcribeResult'
|
|
412
|
+
| 'detectedLanguage'
|
|
408
413
|
| 'transcribeDetails'
|
|
409
414
|
| 'recordedPath'
|
|
410
415
|
value: any
|
package/types/subspace.ts
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { makeId } from '../id'
|
|
2
|
+
|
|
3
|
+
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
|
4
|
+
const UUID_ONLY_RE = new RegExp(`^${UUID}$`)
|
|
5
|
+
|
|
6
|
+
describe('makeId', () => {
|
|
7
|
+
describe('type prefixes', () => {
|
|
8
|
+
const cases = [
|
|
9
|
+
['animation', 'ANIMATION_'],
|
|
10
|
+
['brick', 'BRICK_'],
|
|
11
|
+
['dynamic-brick', 'DYNAMIC_BRICK_'],
|
|
12
|
+
['canvas', 'CANVAS_'],
|
|
13
|
+
['generator', 'GENERATOR_'],
|
|
14
|
+
['data', 'PROPERTY_BANK_DATA_NODE_'],
|
|
15
|
+
['switch', 'BRICK_STATE_GROUP_'],
|
|
16
|
+
['property_bank_command', 'PROPERTY_BANK_COMMAND_NODE_'],
|
|
17
|
+
['property_bank_calc', 'PROPERTY_BANK_COMMAND_MAP_'],
|
|
18
|
+
['automation_map', 'AUTOMATION_MAP_'],
|
|
19
|
+
['test', 'TEST_'],
|
|
20
|
+
['test_case', 'TEST_CASE_'],
|
|
21
|
+
['test_var', 'TEST_VAR_'],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
test.each(cases)('produces %p prefix', (type, prefix) => {
|
|
25
|
+
expect(makeId(type)).toMatch(new RegExp(`^${prefix}${UUID}$`))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('returns unique random uuids across calls', () => {
|
|
29
|
+
expect(makeId('brick')).not.toBe(makeId('brick'))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('unknown type falls through to an unprefixed uuid', () => {
|
|
33
|
+
expect(makeId('not-a-real-type')).toMatch(UUID_ONLY_RE)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('subspace', () => {
|
|
38
|
+
test('throws because subspace IDs must be fixed', () => {
|
|
39
|
+
expect(() => makeId('subspace')).toThrow(/subspace is not supported/)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('snapshotMode', () => {
|
|
44
|
+
test('produces sequential, deterministic uuids', () => {
|
|
45
|
+
const a = makeId('brick', { snapshotMode: true })
|
|
46
|
+
const b = makeId('brick', { snapshotMode: true })
|
|
47
|
+
expect(a).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
|
|
48
|
+
expect(b).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
|
|
49
|
+
expect(Number(b.slice(-12))).toBe(Number(a.slice(-12)) + 1)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('snapshotMode false falls back to random uuid', () => {
|
|
53
|
+
const id = makeId('canvas', { snapshotMode: false })
|
|
54
|
+
expect(id).toMatch(new RegExp(`^CANVAS_${UUID}$`))
|
|
55
|
+
expect(id).not.toMatch(/CANVAS_00000000-0000-0000-0000-/)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
package/utils/data.ts
CHANGED
package/utils/event-props.ts
CHANGED
|
@@ -141,6 +141,11 @@ export const templateEventPropsMap = {
|
|
|
141
141
|
BRICK_MAPS_REGION_LONGITUDE_DELTA: 'number',
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
|
+
Sketch: {
|
|
145
|
+
onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number' },
|
|
146
|
+
onToolChange: { BRICK_SKETCH_TOOL: 'string' },
|
|
147
|
+
onExportImage: { BRICK_SKETCH_IMAGE_URI: 'string' },
|
|
148
|
+
},
|
|
144
149
|
Tick: {
|
|
145
150
|
ticking: { GENERATOR_TICK_COUNTDOWN: 'number', GENERATOR_TICK_VALUE: 'any' },
|
|
146
151
|
completed: { GENERATOR_TICK_COUNTDOWN: 'number', GENERATOR_TICK_VALUE: 'any' },
|
|
@@ -598,6 +603,7 @@ export const templateEventPropsMap = {
|
|
|
598
603
|
onError: { GENERATOR_SPEECH_INFERENCE_ERROR: 'string' },
|
|
599
604
|
onTranscribed: {
|
|
600
605
|
GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_RESULT: 'string',
|
|
606
|
+
GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_LANGUAGE: 'string',
|
|
601
607
|
GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_START_TIME: 'number',
|
|
602
608
|
GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_END_TIME: 'number',
|
|
603
609
|
GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_TIME: 'number',
|
|
@@ -619,10 +625,11 @@ export const templateEventPropsMap = {
|
|
|
619
625
|
RealtimeTranscription: {
|
|
620
626
|
onTranscribe: {
|
|
621
627
|
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_EVENT:
|
|
622
|
-
'{ type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }',
|
|
628
|
+
'{ type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string language?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }',
|
|
623
629
|
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_TYPE: 'string',
|
|
624
630
|
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_SLICE_INDEX: 'number',
|
|
625
631
|
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_RESULT_TEXT: 'string',
|
|
632
|
+
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_LANGUAGE: 'string',
|
|
626
633
|
GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_IS_CAPTURING: 'boolean',
|
|
627
634
|
GENERATOR_REALTIME_TRANSCRIPTION_VAD_EVENT:
|
|
628
635
|
'{ type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any }',
|
|
@@ -644,10 +651,13 @@ export const templateEventPropsMap = {
|
|
|
644
651
|
GENERATOR_REALTIME_TRANSCRIPTION_STATS:
|
|
645
652
|
'{ type?: string timestamp?: number data?: { [key: string]: any } [key: string]: any }',
|
|
646
653
|
},
|
|
647
|
-
onStabilized: {
|
|
654
|
+
onStabilized: {
|
|
655
|
+
GENERATOR_REALTIME_TRANSCRIPTION_STABILIZED_TEXT: 'string',
|
|
656
|
+
GENERATOR_REALTIME_TRANSCRIPTION_STABILIZED_LANGUAGE: 'string',
|
|
657
|
+
},
|
|
648
658
|
onEnd: {
|
|
649
659
|
GENERATOR_REALTIME_TRANSCRIPTION_END_RESULTS:
|
|
650
|
-
'Array<{ transcribeEvent?: { type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }>',
|
|
660
|
+
'Array<{ transcribeEvent?: { type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string language?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }>',
|
|
651
661
|
GENERATOR_REALTIME_TRANSCRIPTION_END_AUDIO_OUTPUT_PATH: 'string',
|
|
652
662
|
},
|
|
653
663
|
},
|