@world-engines/blocks-editor 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.
Files changed (76) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +40 -0
  3. package/dist/adapters/trigger.d.ts +72 -0
  4. package/dist/adapters/trigger.js +206 -0
  5. package/dist/blockly-config.d.ts +3 -0
  6. package/dist/blockly-config.js +7 -0
  7. package/dist/blocks/ctx.d.ts +67 -0
  8. package/dist/blocks/ctx.js +96 -0
  9. package/dist/blocks/registry-taxonomy.d.ts +12 -0
  10. package/dist/blocks/registry-taxonomy.js +79 -0
  11. package/dist/blocks/registry.d.ts +39 -0
  12. package/dist/blocks/registry.js +1000 -0
  13. package/dist/blocks/time-fields.d.ts +16 -0
  14. package/dist/blocks/time-fields.js +45 -0
  15. package/dist/blocks/toolbox.d.ts +53 -0
  16. package/dist/blocks/toolbox.js +195 -0
  17. package/dist/fields/AttrPickerField.d.ts +13 -0
  18. package/dist/fields/AttrPickerField.js +48 -0
  19. package/dist/fields/EdgePickerField.d.ts +10 -0
  20. package/dist/fields/EdgePickerField.js +36 -0
  21. package/dist/fields/EntityPickerField.d.ts +10 -0
  22. package/dist/fields/EntityPickerField.js +36 -0
  23. package/dist/fields/StatePickerField.d.ts +13 -0
  24. package/dist/fields/StatePickerField.js +47 -0
  25. package/dist/fields/TimePickerField.d.ts +10 -0
  26. package/dist/fields/TimePickerField.js +81 -0
  27. package/dist/fields/picker-events.d.ts +21 -0
  28. package/dist/fields/picker-events.js +28 -0
  29. package/dist/fields/register.d.ts +6 -0
  30. package/dist/fields/register.js +22 -0
  31. package/dist/i18n/messages-zh.d.ts +6 -0
  32. package/dist/i18n/messages-zh.js +34 -0
  33. package/dist/index.d.ts +21 -0
  34. package/dist/index.js +11 -0
  35. package/dist/migration/index.d.ts +1 -0
  36. package/dist/migration/index.js +1 -0
  37. package/dist/migration/types.d.ts +91 -0
  38. package/dist/migration/types.js +1 -0
  39. package/dist/react/PickerHost.d.ts +6 -0
  40. package/dist/react/PickerHost.js +110 -0
  41. package/dist/sdk-to-block/index.d.ts +4 -0
  42. package/dist/sdk-to-block/index.js +4 -0
  43. package/dist/sdk-to-block/parser.d.ts +2 -0
  44. package/dist/sdk-to-block/parser.js +101 -0
  45. package/dist/sdk-to-block/register.d.ts +3 -0
  46. package/dist/sdk-to-block/register.js +27 -0
  47. package/dist/sdk-to-block/signature.d.ts +31 -0
  48. package/dist/sdk-to-block/signature.js +12 -0
  49. package/dist/sdk-to-block/to-block-def.d.ts +25 -0
  50. package/dist/sdk-to-block/to-block-def.js +71 -0
  51. package/dist/themes/index.d.ts +4 -0
  52. package/dist/themes/index.js +12 -0
  53. package/dist/themes/liquid-glass-dark.d.ts +57 -0
  54. package/dist/themes/liquid-glass-dark.js +187 -0
  55. package/dist/themes/liquid-glass.css +195 -0
  56. package/dist/view-to-block/index.d.ts +10 -0
  57. package/dist/view-to-block/index.js +67 -0
  58. package/dist/workspace.d.ts +33 -0
  59. package/dist/workspace.js +306 -0
  60. package/package.json +65 -0
  61. package/public/blockly-media/1x1.gif +0 -0
  62. package/public/blockly-media/click.mp3 +0 -0
  63. package/public/blockly-media/delete-icon.svg +1 -0
  64. package/public/blockly-media/delete.mp3 +0 -0
  65. package/public/blockly-media/disconnect.mp3 +0 -0
  66. package/public/blockly-media/drop.mp3 +0 -0
  67. package/public/blockly-media/dropdown-arrow.svg +1 -0
  68. package/public/blockly-media/foldout-icon.svg +1 -0
  69. package/public/blockly-media/handclosed.cur +0 -0
  70. package/public/blockly-media/handdelete.cur +0 -0
  71. package/public/blockly-media/handopen.cur +0 -0
  72. package/public/blockly-media/pilcrow.png +0 -0
  73. package/public/blockly-media/quote0.png +0 -0
  74. package/public/blockly-media/quote1.png +0 -0
  75. package/public/blockly-media/resize-handle.svg +3 -0
  76. package/public/blockly-media/sprites.svg +74 -0
@@ -0,0 +1,16 @@
1
+ export interface WorldTimeBounds {
2
+ readonly months_per_year: number;
3
+ readonly days_per_month: number;
4
+ readonly hours_per_day: number;
5
+ readonly minutes_per_hour: number;
6
+ readonly seconds_per_minute: number;
7
+ }
8
+ export type WorldTimePointKey = "year" | "month" | "day" | "hour" | "minute" | "second";
9
+ export interface TimeFieldSpec {
10
+ readonly key: WorldTimePointKey;
11
+ readonly labelKey: string;
12
+ readonly lo: (cfg: WorldTimeBounds) => number;
13
+ readonly hi: (cfg: WorldTimeBounds) => number;
14
+ }
15
+ export declare const DEFAULT_WORLD_TIME_BOUNDS: WorldTimeBounds;
16
+ export declare const TIME_FIELDS: ReadonlyArray<TimeFieldSpec>;
@@ -0,0 +1,45 @@
1
+ export const DEFAULT_WORLD_TIME_BOUNDS = {
2
+ months_per_year: 12,
3
+ days_per_month: 30,
4
+ hours_per_day: 24,
5
+ minutes_per_hour: 60,
6
+ seconds_per_minute: 60
7
+ };
8
+ export const TIME_FIELDS = [
9
+ {
10
+ key: "year",
11
+ labelKey: "editor.world_time.year",
12
+ lo: () => 0,
13
+ hi: () => Number.MAX_SAFE_INTEGER
14
+ },
15
+ {
16
+ key: "month",
17
+ labelKey: "editor.world_time.month",
18
+ lo: () => 1,
19
+ hi: (cfg) => cfg.months_per_year
20
+ },
21
+ {
22
+ key: "day",
23
+ labelKey: "editor.world_time.day",
24
+ lo: () => 1,
25
+ hi: (cfg) => cfg.days_per_month
26
+ },
27
+ {
28
+ key: "hour",
29
+ labelKey: "editor.world_time.hour",
30
+ lo: () => 0,
31
+ hi: (cfg) => cfg.hours_per_day - 1
32
+ },
33
+ {
34
+ key: "minute",
35
+ labelKey: "editor.world_time.minute",
36
+ lo: () => 0,
37
+ hi: (cfg) => cfg.minutes_per_hour - 1
38
+ },
39
+ {
40
+ key: "second",
41
+ labelKey: "editor.world_time.second",
42
+ lo: () => 0,
43
+ hi: (cfg) => cfg.seconds_per_minute - 1
44
+ }
45
+ ];
@@ -0,0 +1,53 @@
1
+ export interface ToolboxBlockEntry {
2
+ readonly kind: "block";
3
+ readonly type: string;
4
+ }
5
+ export interface ToolboxLabelEntry {
6
+ readonly kind: "label";
7
+ readonly text: string;
8
+ }
9
+ export interface ToolboxSepEntry {
10
+ readonly kind: "sep";
11
+ readonly gap?: number;
12
+ }
13
+ export type ToolboxCategoryContent = ToolboxBlockEntry | ToolboxLabelEntry | ToolboxSepEntry;
14
+ export interface ToolboxCategoryEntry {
15
+ readonly kind: "category";
16
+ readonly name: string;
17
+ readonly categorystyle: string;
18
+ readonly contents: ReadonlyArray<ToolboxCategoryContent>;
19
+ readonly disabled?: boolean;
20
+ }
21
+ export interface ToolboxJson {
22
+ readonly kind: "categoryToolbox";
23
+ readonly contents: ReadonlyArray<ToolboxCategoryEntry>;
24
+ }
25
+ export declare const CATEGORY_IDS: {
26
+ readonly events: "events";
27
+ readonly control: "control";
28
+ readonly sensing: "sensing";
29
+ readonly operators: "operators";
30
+ readonly variables: "variables";
31
+ readonly sdkInterfaces: "sdk_interfaces";
32
+ readonly viewInterfaces: "view_interfaces";
33
+ };
34
+ export type CategoryId = (typeof CATEGORY_IDS)[keyof typeof CATEGORY_IDS];
35
+ export declare const CATEGORY_STYLE_KEYS: {
36
+ readonly events: "events_category";
37
+ readonly control: "control_category";
38
+ readonly sensing: "sensing_category";
39
+ readonly operators: "operators_category";
40
+ readonly variables: "variables_category";
41
+ readonly sdk_interfaces: "sdk_interfaces_category";
42
+ readonly view_interfaces: "view_interfaces_category";
43
+ };
44
+ export interface ToolboxBuildOptions {
45
+ readonly schema?: "trigger-definition" | "trigger";
46
+ readonly sdkBlockTypes?: ReadonlyArray<string>;
47
+ readonly viewBlockTypes?: ReadonlyArray<string>;
48
+ }
49
+ export declare function buildToolbox(options?: ToolboxBuildOptions): ToolboxJson;
50
+ export declare function getToolbox(): ToolboxJson;
51
+ export declare function listToolboxCategoryNames(): ReadonlyArray<string>;
52
+ export declare function listToolboxCategoryIds(): ReadonlyArray<CategoryId>;
53
+ export declare function categoryDisplayNameOf(id: CategoryId): string;
@@ -0,0 +1,195 @@
1
+ import { BLOCK_TYPES } from "./registry.js";
2
+ export const CATEGORY_IDS = {
3
+ events: "events",
4
+ control: "control",
5
+ sensing: "sensing",
6
+ operators: "operators",
7
+ variables: "variables",
8
+ sdkInterfaces: "sdk_interfaces",
9
+ viewInterfaces: "view_interfaces"
10
+ };
11
+ export const CATEGORY_STYLE_KEYS = {
12
+ [CATEGORY_IDS.events]: "events_category",
13
+ [CATEGORY_IDS.control]: "control_category",
14
+ [CATEGORY_IDS.sensing]: "sensing_category",
15
+ [CATEGORY_IDS.operators]: "operators_category",
16
+ [CATEGORY_IDS.variables]: "variables_category",
17
+ [CATEGORY_IDS.sdkInterfaces]: "sdk_interfaces_category",
18
+ [CATEGORY_IDS.viewInterfaces]: "view_interfaces_category"
19
+ };
20
+ const GROUP_SEP_GAP = 6;
21
+ const block = (type) => ({ kind: "block", type });
22
+ const groupLabel = (text) => ({ kind: "label", text });
23
+ const sep = () => ({ kind: "sep", gap: GROUP_SEP_GAP });
24
+ const TRIGGER_DEFINITION_CONTENTS = [
25
+ groupLabel("定义"),
26
+ block(BLOCK_TYPES.triggerRoot),
27
+ sep(),
28
+ groupLabel("组合条件"),
29
+ block(BLOCK_TYPES.opAnd),
30
+ block(BLOCK_TYPES.opOr),
31
+ block(BLOCK_TYPES.opNot),
32
+ sep(),
33
+ groupLabel("可执行条件"),
34
+ block(BLOCK_TYPES.leafTime),
35
+ block(BLOCK_TYPES.leafEvent),
36
+ block(BLOCK_TYPES.leafNl),
37
+ block(BLOCK_TYPES.leafDaySegment)
38
+ ];
39
+ const GLOBAL_CATEGORIES = [
40
+ {
41
+ kind: "category",
42
+ name: "事件",
43
+ categorystyle: CATEGORY_STYLE_KEYS.events,
44
+ contents: [
45
+ block("when_scenario_start"),
46
+ block("when_value_changed"),
47
+ block("when_time_changed"),
48
+ block("when_entity_state_changed"),
49
+ block("when_var_changed"),
50
+ block("when_time_reached"),
51
+ block("when_recall_success")
52
+ ]
53
+ },
54
+ {
55
+ kind: "category",
56
+ name: "控制",
57
+ categorystyle: CATEGORY_STYLE_KEYS.control,
58
+ contents: [
59
+ block("if"),
60
+ block("if_else"),
61
+ block("repeat"),
62
+ block("repeat_until"),
63
+ block("wait_seconds"),
64
+ block("stop_this_script")
65
+ ]
66
+ },
67
+ {
68
+ kind: "category",
69
+ name: "侦测",
70
+ categorystyle: CATEGORY_STYLE_KEYS.sensing,
71
+ contents: [
72
+ block("sense_time_year"),
73
+ block("sense_time_month"),
74
+ block("sense_time_day"),
75
+ block("sense_time_hour"),
76
+ block("sense_time_minute"),
77
+ block("sense_time_second"),
78
+ block("sense_object_state"),
79
+ block("sense_object_attr"),
80
+ block("sense_edge_relation"),
81
+ block("sense_relation_ref"),
82
+ block("sense_object_frozen")
83
+ ]
84
+ },
85
+ {
86
+ kind: "category",
87
+ name: "运算",
88
+ categorystyle: CATEGORY_STYLE_KEYS.operators,
89
+ contents: [
90
+ block("op_time_crossed"),
91
+ block("op_and"),
92
+ block("op_or"),
93
+ block("op_not"),
94
+ block("op_equals"),
95
+ block("op_greater"),
96
+ block("op_less"),
97
+ block("op_contains"),
98
+ block("op_add"),
99
+ block("op_subtract"),
100
+ block("op_multiply"),
101
+ block("op_divide"),
102
+ block("op_mod"),
103
+ block("op_round"),
104
+ block("op_abs"),
105
+ block("op_join"),
106
+ block("op_letter_of"),
107
+ block("op_length_of")
108
+ ]
109
+ },
110
+ {
111
+ kind: "category",
112
+ name: "变量与世界",
113
+ categorystyle: CATEGORY_STYLE_KEYS.variables,
114
+ contents: [
115
+ block("var_read"),
116
+ block("var_set"),
117
+ block("var_change"),
118
+ block("var_const_boolean"),
119
+ block("var_const_value"),
120
+ block("var_const_time"),
121
+ sep(),
122
+ block("entity_freeze"),
123
+ block("entity_toggle"),
124
+ block("entity_enable"),
125
+ block("entity_disable"),
126
+ block("attr_set"),
127
+ block("time_advance"),
128
+ block("time_set")
129
+ ]
130
+ }
131
+ ];
132
+ const DYNAMIC_CATEGORY_NAMES = {
133
+ [CATEGORY_IDS.sdkInterfaces]: "SDK 接口",
134
+ [CATEGORY_IDS.viewInterfaces]: "视图操作"
135
+ };
136
+ const dynamicCategory = (id, blockTypes) => {
137
+ if (blockTypes.length === 0)
138
+ return null;
139
+ const unique = new Set(blockTypes);
140
+ if (unique.size !== blockTypes.length) {
141
+ throw new Error(`toolbox_dynamic_block_type_duplicate:${id}`);
142
+ }
143
+ return {
144
+ kind: "category",
145
+ name: DYNAMIC_CATEGORY_NAMES[id],
146
+ categorystyle: CATEGORY_STYLE_KEYS[id],
147
+ contents: blockTypes.map(block)
148
+ };
149
+ };
150
+ export function buildToolbox(options = {}) {
151
+ if (options.schema === "trigger") {
152
+ const sdkCategory = dynamicCategory(CATEGORY_IDS.sdkInterfaces, options.sdkBlockTypes ?? []);
153
+ const viewCategory = dynamicCategory(CATEGORY_IDS.viewInterfaces, options.viewBlockTypes ?? []);
154
+ return {
155
+ kind: "categoryToolbox",
156
+ contents: [
157
+ ...GLOBAL_CATEGORIES,
158
+ ...(sdkCategory === null ? [] : [sdkCategory]),
159
+ ...(viewCategory === null ? [] : [viewCategory])
160
+ ]
161
+ };
162
+ }
163
+ return {
164
+ kind: "categoryToolbox",
165
+ contents: [{
166
+ kind: "category",
167
+ name: "◇ 触发器条件",
168
+ categorystyle: CATEGORY_STYLE_KEYS.events,
169
+ contents: TRIGGER_DEFINITION_CONTENTS
170
+ }]
171
+ };
172
+ }
173
+ const DEFAULT_TOOLBOX = buildToolbox();
174
+ export function getToolbox() {
175
+ return DEFAULT_TOOLBOX;
176
+ }
177
+ export function listToolboxCategoryNames() {
178
+ return GLOBAL_CATEGORIES.map((category) => category.name);
179
+ }
180
+ export function listToolboxCategoryIds() {
181
+ return [
182
+ CATEGORY_IDS.events,
183
+ CATEGORY_IDS.control,
184
+ CATEGORY_IDS.sensing,
185
+ CATEGORY_IDS.operators,
186
+ CATEGORY_IDS.variables
187
+ ];
188
+ }
189
+ export function categoryDisplayNameOf(id) {
190
+ if (id === CATEGORY_IDS.sdkInterfaces || id === CATEGORY_IDS.viewInterfaces) {
191
+ return DYNAMIC_CATEGORY_NAMES[id];
192
+ }
193
+ const index = listToolboxCategoryIds().indexOf(id);
194
+ return index >= 0 ? GLOBAL_CATEGORIES[index]?.name ?? id : id;
195
+ }
@@ -0,0 +1,13 @@
1
+ import * as Blockly from "blockly";
2
+ export interface AttrPickerFieldOptions extends Blockly.FieldConfig {
3
+ readonly value?: string;
4
+ readonly entityFieldName?: string;
5
+ }
6
+ export declare class AttrPickerField extends Blockly.Field<string> {
7
+ private entityFieldName_;
8
+ constructor(value?: string, entityFieldName?: string);
9
+ static fromJson(options: Blockly.FieldConfig): AttrPickerField;
10
+ private readEntityId;
11
+ protected showEditor_(): void;
12
+ protected getText_(): string | null;
13
+ }
@@ -0,0 +1,48 @@
1
+ import * as Blockly from "blockly";
2
+ import { findAttrLabel, readCtxFromWorkspace } from "../blocks/ctx.js";
3
+ import { dispatchPickerRequest, toAttrCandidates } from "./picker-events.js";
4
+ const PLACEHOLDER_LABEL = "(选择对象)";
5
+ const PICKER_TITLE = "选择对象";
6
+ const ENTITY_FIELD_NAME_DEFAULT = "ENTITY";
7
+ export class AttrPickerField extends Blockly.Field {
8
+ entityFieldName_;
9
+ constructor(value = "", entityFieldName = ENTITY_FIELD_NAME_DEFAULT) {
10
+ super(value);
11
+ this.entityFieldName_ = entityFieldName;
12
+ }
13
+ static fromJson(options) {
14
+ const opts = options;
15
+ return new AttrPickerField(opts.value ?? "", opts.entityFieldName ?? ENTITY_FIELD_NAME_DEFAULT);
16
+ }
17
+ readEntityId() {
18
+ const source = this.getSourceBlock();
19
+ if (!source)
20
+ return "";
21
+ const raw = source.getFieldValue(this.entityFieldName_);
22
+ return typeof raw === "string" ? raw : "";
23
+ }
24
+ showEditor_() {
25
+ const source = this.getSourceBlock();
26
+ if (!source)
27
+ return;
28
+ const ctx = readCtxFromWorkspace(source.workspace ?? null);
29
+ const entityId = this.readEntityId();
30
+ const candidates = toAttrCandidates(ctx, entityId);
31
+ dispatchPickerRequest({
32
+ kind: "attr",
33
+ title: PICKER_TITLE,
34
+ candidates,
35
+ currentValue: this.getValue() ?? "",
36
+ onSelect: (id) => this.setValue(id)
37
+ });
38
+ }
39
+ getText_() {
40
+ const source = this.getSourceBlock();
41
+ const ctx = readCtxFromWorkspace(source?.workspace ?? null);
42
+ const id = this.getValue();
43
+ if (id === null || id === undefined || id.length === 0)
44
+ return PLACEHOLDER_LABEL;
45
+ const entityId = this.readEntityId();
46
+ return findAttrLabel(ctx, entityId, id);
47
+ }
48
+ }
@@ -0,0 +1,10 @@
1
+ import * as Blockly from "blockly";
2
+ export interface EdgePickerFieldOptions extends Blockly.FieldConfig {
3
+ readonly value?: string;
4
+ }
5
+ export declare class EdgePickerField extends Blockly.Field<string> {
6
+ constructor(value?: string);
7
+ static fromJson(options: Blockly.FieldConfig): EdgePickerField;
8
+ protected showEditor_(): void;
9
+ protected getText_(): string | null;
10
+ }
@@ -0,0 +1,36 @@
1
+ import * as Blockly from "blockly";
2
+ import { findEdgeLabel, readCtxFromWorkspace } from "../blocks/ctx.js";
3
+ import { dispatchPickerRequest, toEdgeCandidates } from "./picker-events.js";
4
+ const PLACEHOLDER_LABEL = "(选择对象)";
5
+ const PICKER_TITLE = "选择对象";
6
+ export class EdgePickerField extends Blockly.Field {
7
+ constructor(value = "") {
8
+ super(value);
9
+ }
10
+ static fromJson(options) {
11
+ const opts = options;
12
+ return new EdgePickerField(opts.value ?? "");
13
+ }
14
+ showEditor_() {
15
+ const source = this.getSourceBlock();
16
+ if (!source)
17
+ return;
18
+ const ctx = readCtxFromWorkspace(source.workspace ?? null);
19
+ const candidates = toEdgeCandidates(ctx);
20
+ dispatchPickerRequest({
21
+ kind: "edge",
22
+ title: PICKER_TITLE,
23
+ candidates,
24
+ currentValue: this.getValue() ?? "",
25
+ onSelect: (id) => this.setValue(id)
26
+ });
27
+ }
28
+ getText_() {
29
+ const source = this.getSourceBlock();
30
+ const ctx = readCtxFromWorkspace(source?.workspace ?? null);
31
+ const id = this.getValue();
32
+ if (id === null || id === undefined || id.length === 0)
33
+ return PLACEHOLDER_LABEL;
34
+ return findEdgeLabel(ctx, id);
35
+ }
36
+ }
@@ -0,0 +1,10 @@
1
+ import * as Blockly from "blockly";
2
+ export interface EntityPickerFieldOptions extends Blockly.FieldConfig {
3
+ readonly value?: string;
4
+ }
5
+ export declare class EntityPickerField extends Blockly.Field<string> {
6
+ constructor(value?: string);
7
+ static fromJson(options: Blockly.FieldConfig): EntityPickerField;
8
+ protected showEditor_(): void;
9
+ protected getText_(): string | null;
10
+ }
@@ -0,0 +1,36 @@
1
+ import * as Blockly from "blockly";
2
+ import { findEntityName, readCtxFromWorkspace } from "../blocks/ctx.js";
3
+ import { dispatchPickerRequest, toEntityCandidates } from "./picker-events.js";
4
+ const PLACEHOLDER_LABEL = "(选择对象)";
5
+ const PICKER_TITLE = "选择对象";
6
+ export class EntityPickerField extends Blockly.Field {
7
+ constructor(value = "") {
8
+ super(value);
9
+ }
10
+ static fromJson(options) {
11
+ const opts = options;
12
+ return new EntityPickerField(opts.value ?? "");
13
+ }
14
+ showEditor_() {
15
+ const source = this.getSourceBlock();
16
+ if (!source)
17
+ return;
18
+ const ctx = readCtxFromWorkspace(source.workspace ?? null);
19
+ const candidates = toEntityCandidates(ctx);
20
+ dispatchPickerRequest({
21
+ kind: "entity",
22
+ title: PICKER_TITLE,
23
+ candidates,
24
+ currentValue: this.getValue() ?? "",
25
+ onSelect: (id) => this.setValue(id)
26
+ });
27
+ }
28
+ getText_() {
29
+ const source = this.getSourceBlock();
30
+ const ctx = readCtxFromWorkspace(source?.workspace ?? null);
31
+ const id = this.getValue();
32
+ if (id === null || id === undefined || id.length === 0)
33
+ return PLACEHOLDER_LABEL;
34
+ return findEntityName(ctx, id);
35
+ }
36
+ }
@@ -0,0 +1,13 @@
1
+ import * as Blockly from "blockly";
2
+ export interface StatePickerFieldOptions extends Blockly.FieldConfig {
3
+ readonly value?: string;
4
+ readonly entityFieldName?: string;
5
+ }
6
+ export declare class StatePickerField extends Blockly.Field<string> {
7
+ private entityFieldName_;
8
+ constructor(value?: string, entityFieldName?: string);
9
+ static fromJson(options: Blockly.FieldConfig): StatePickerField;
10
+ private readEntityId;
11
+ protected showEditor_(): void;
12
+ protected getText_(): string | null;
13
+ }
@@ -0,0 +1,47 @@
1
+ import * as Blockly from "blockly";
2
+ import { findStateLabel, readCtxFromWorkspace } from "../blocks/ctx.js";
3
+ import { dispatchPickerRequest, toStateCandidates } from "./picker-events.js";
4
+ const PLACEHOLDER_LABEL = "(选择状态)";
5
+ const ENTITY_FIELD_NAME_DEFAULT = "ENTITY";
6
+ export class StatePickerField extends Blockly.Field {
7
+ entityFieldName_;
8
+ constructor(value = "", entityFieldName = ENTITY_FIELD_NAME_DEFAULT) {
9
+ super(value);
10
+ this.entityFieldName_ = entityFieldName;
11
+ }
12
+ static fromJson(options) {
13
+ const opts = options;
14
+ return new StatePickerField(opts.value ?? "", opts.entityFieldName ?? ENTITY_FIELD_NAME_DEFAULT);
15
+ }
16
+ readEntityId() {
17
+ const source = this.getSourceBlock();
18
+ if (!source)
19
+ return "";
20
+ const raw = source.getFieldValue(this.entityFieldName_);
21
+ return typeof raw === "string" ? raw : "";
22
+ }
23
+ showEditor_() {
24
+ const source = this.getSourceBlock();
25
+ if (!source)
26
+ return;
27
+ const ctx = readCtxFromWorkspace(source.workspace ?? null);
28
+ const entityId = this.readEntityId();
29
+ const candidates = toStateCandidates(ctx, entityId);
30
+ dispatchPickerRequest({
31
+ kind: "state",
32
+ title: "选择状态",
33
+ candidates,
34
+ currentValue: this.getValue() ?? "",
35
+ onSelect: (id) => this.setValue(id)
36
+ });
37
+ }
38
+ getText_() {
39
+ const source = this.getSourceBlock();
40
+ const ctx = readCtxFromWorkspace(source?.workspace ?? null);
41
+ const id = this.getValue();
42
+ if (id === null || id === undefined || id.length === 0)
43
+ return PLACEHOLDER_LABEL;
44
+ const entityId = this.readEntityId();
45
+ return findStateLabel(ctx, entityId, id);
46
+ }
47
+ }
@@ -0,0 +1,10 @@
1
+ import * as Blockly from "blockly";
2
+ export interface TimePickerFieldOptions extends Blockly.FieldConfig {
3
+ readonly value?: string;
4
+ }
5
+ export declare class TimePickerField extends Blockly.Field<string> {
6
+ constructor(value?: string);
7
+ static fromJson(options: Blockly.FieldConfig): TimePickerField;
8
+ protected showEditor_(): void;
9
+ protected getText_(): string | null;
10
+ }
@@ -0,0 +1,81 @@
1
+ import * as Blockly from "blockly";
2
+ import { readCtxFromWorkspace } from "../blocks/ctx.js";
3
+ import { DEFAULT_WORLD_TIME_BOUNDS, TIME_FIELDS } from "../blocks/time-fields.js";
4
+ import { dispatchPickerRequest } from "./picker-events.js";
5
+ const PLACEHOLDER_LABEL = "(选择时间)";
6
+ const DEFAULT_TIME_PAYLOAD = {
7
+ year: 0,
8
+ month: 1,
9
+ day: 1,
10
+ hour: 0,
11
+ minute: 0,
12
+ second: 0
13
+ };
14
+ const parseTimePayload = (raw) => {
15
+ if (raw.length === 0)
16
+ return DEFAULT_TIME_PAYLOAD;
17
+ try {
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed !== null
20
+ && typeof parsed === "object"
21
+ && "year" in parsed
22
+ && "month" in parsed) {
23
+ const p = parsed;
24
+ return {
25
+ year: typeof p.year === "number" ? p.year : 0,
26
+ month: typeof p.month === "number" ? p.month : 1,
27
+ day: typeof p.day === "number" ? p.day : 1,
28
+ hour: typeof p.hour === "number" ? p.hour : 0,
29
+ minute: typeof p.minute === "number" ? p.minute : 0,
30
+ second: typeof p.second === "number" ? p.second : 0
31
+ };
32
+ }
33
+ }
34
+ catch {
35
+ // ignore parse error
36
+ }
37
+ return DEFAULT_TIME_PAYLOAD;
38
+ };
39
+ const formatTimeLabel = (p) => `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")} ${String(p.hour).padStart(2, "0")}:${String(p.minute).padStart(2, "0")}:${String(p.second).padStart(2, "0")}`;
40
+ export class TimePickerField extends Blockly.Field {
41
+ constructor(value = "") {
42
+ super(value);
43
+ }
44
+ static fromJson(options) {
45
+ const opts = options;
46
+ return new TimePickerField(opts.value ?? "");
47
+ }
48
+ showEditor_() {
49
+ const source = this.getSourceBlock();
50
+ if (!source)
51
+ return;
52
+ const ctx = readCtxFromWorkspace(source.workspace ?? null);
53
+ const bounds = ctx.worldTime ?? DEFAULT_WORLD_TIME_BOUNDS;
54
+ const current = parseTimePayload(this.getValue() ?? "");
55
+ const candidates = TIME_FIELDS.map((spec) => {
56
+ const lo = spec.lo(bounds);
57
+ const hi = spec.hi(bounds);
58
+ return {
59
+ id: spec.key,
60
+ name: spec.key,
61
+ subtitle: `${lo} ~ ${hi === Number.MAX_SAFE_INTEGER ? "∞" : hi}`
62
+ };
63
+ });
64
+ dispatchPickerRequest({
65
+ kind: "time",
66
+ title: "选择时间",
67
+ candidates,
68
+ currentValue: this.getValue() ?? "",
69
+ onSelect: (id) => this.setValue(id)
70
+ });
71
+ // 触发 picker 后附带 currentValue 让 UI 层可解析已存值并渲染表单
72
+ void current;
73
+ }
74
+ getText_() {
75
+ const value = this.getValue();
76
+ if (value === null || value === undefined || value.length === 0) {
77
+ return PLACEHOLDER_LABEL;
78
+ }
79
+ return formatTimeLabel(parseTimePayload(value));
80
+ }
81
+ }
@@ -0,0 +1,21 @@
1
+ import type { BlocksEditorCtx } from "../blocks/ctx.js";
2
+ export declare const PICKER_EVENT_NAME: "blocks-editor:open-picker";
3
+ export type PickerKind = "entity" | "attr" | "edge" | "state" | "time" | "variable";
4
+ export interface PickerCandidate {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly subtitle?: string;
8
+ }
9
+ export interface PickerRequestDetail {
10
+ readonly kind: PickerKind;
11
+ readonly title: string;
12
+ readonly candidates: ReadonlyArray<PickerCandidate>;
13
+ readonly currentValue: string;
14
+ readonly onSelect: (value: string) => void;
15
+ readonly onCancel?: () => void;
16
+ }
17
+ export declare function dispatchPickerRequest(detail: PickerRequestDetail): void;
18
+ export declare function toEntityCandidates(ctx: BlocksEditorCtx): PickerCandidate[];
19
+ export declare function toAttrCandidates(ctx: BlocksEditorCtx, entityId: string): PickerCandidate[];
20
+ export declare function toEdgeCandidates(ctx: BlocksEditorCtx): PickerCandidate[];
21
+ export declare function toStateCandidates(ctx: BlocksEditorCtx, entityId: string): PickerCandidate[];
@@ -0,0 +1,28 @@
1
+ export const PICKER_EVENT_NAME = "blocks-editor:open-picker";
2
+ export function dispatchPickerRequest(detail) {
3
+ if (typeof window === "undefined")
4
+ return;
5
+ window.dispatchEvent(new CustomEvent(PICKER_EVENT_NAME, { detail }));
6
+ }
7
+ export function toEntityCandidates(ctx) {
8
+ return ctx.entity_list.map((e) => ({ id: e.id, name: e.name }));
9
+ }
10
+ export function toAttrCandidates(ctx, entityId) {
11
+ const entity = ctx.entity_list.find((e) => e.id === entityId);
12
+ if (!entity)
13
+ return [];
14
+ return entity.attrs.map((a) => ({ id: a.key, name: a.label }));
15
+ }
16
+ export function toEdgeCandidates(ctx) {
17
+ const edges = ctx.edge_list ?? [];
18
+ return edges.map((e) => ({
19
+ id: e.id,
20
+ name: e.label,
21
+ subtitle: `${e.src_id} → ${e.dst_id}`
22
+ }));
23
+ }
24
+ export function toStateCandidates(ctx, entityId) {
25
+ const entity = ctx.entity_list.find((e) => e.id === entityId);
26
+ const states = entity?.states ?? [];
27
+ return states.map((s) => ({ id: s.key, name: s.label }));
28
+ }