pixifact 0.14.0 → 0.15.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.
@@ -12,6 +12,14 @@ export type CompilerSceneCommand = {
12
12
  op: 'setSceneProp';
13
13
  prop: string;
14
14
  value?: SceneTemplateValue;
15
+ } | {
16
+ op: 'setSceneDefaultProp';
17
+ prop: string;
18
+ value?: SceneTemplateValue;
19
+ } | {
20
+ op: 'setSceneEvent';
21
+ event: string;
22
+ value?: string;
15
23
  } | {
16
24
  op: 'setNodeId';
17
25
  node: string;
@@ -174,6 +174,21 @@ function setSceneProp(template, command) {
174
174
  selection: { type: 'scene' },
175
175
  };
176
176
  }
177
+ function setSceneDefaultProp(template, command) {
178
+ const defaults = template.propDefaults ??= {};
179
+ const previous = getPropPathValue(defaults, command.prop);
180
+ setPropPathValue(defaults, command.prop, command.value);
181
+ return { ok: true, command, inverse: { op: 'setSceneDefaultProp', prop: command.prop, value: previous }, selection: { type: 'scene' } };
182
+ }
183
+ function setSceneEvent(template, command) {
184
+ const events = template.events ??= {};
185
+ const previous = events[command.event];
186
+ if (command.value === undefined)
187
+ delete events[command.event];
188
+ else
189
+ events[command.event] = command.value;
190
+ return { ok: true, command, inverse: { op: 'setSceneEvent', event: command.event, value: previous }, selection: { type: 'scene' } };
191
+ }
177
192
  function setNodeId(template, command, context) {
178
193
  const located = findCompilerSceneNodeLocation(template.children, command.node, context);
179
194
  if (!located || located.node.kind === 'slotOutlet') {
@@ -275,15 +290,16 @@ function setPropPathValue(props, prop, value) {
275
290
  }
276
291
  function setNodeEvent(template, command, context) {
277
292
  const located = findCompilerSceneNodeLocation(template.children, command.node, context);
278
- if (!located || located.node.kind !== 'sceneInstance') {
293
+ if (!located || located.node.kind === 'slotOutlet') {
279
294
  return { ok: false, command, error: 'Node event target was not found.' };
280
295
  }
281
- const previous = located.node.events[command.event];
296
+ const events = located.node.events ??= {};
297
+ const previous = events[command.event];
282
298
  if (command.value === undefined) {
283
- delete located.node.events[command.event];
299
+ delete events[command.event];
284
300
  }
285
301
  else {
286
- located.node.events[command.event] = command.value;
302
+ events[command.event] = command.value;
287
303
  }
288
304
  return {
289
305
  ok: true,
@@ -422,6 +438,10 @@ export function applyCompilerSceneCommand(template, command, context = {}) {
422
438
  return setSceneName(template, command);
423
439
  case 'setSceneProp':
424
440
  return setSceneProp(template, command);
441
+ case 'setSceneDefaultProp':
442
+ return setSceneDefaultProp(template, command);
443
+ case 'setSceneEvent':
444
+ return setSceneEvent(template, command);
425
445
  case 'setNodeId':
426
446
  return setNodeId(template, command, context);
427
447
  case 'setNodeProp':
@@ -282,7 +282,7 @@ function normalizeSceneReferences(scenePath, template) {
282
282
  return next;
283
283
  }
284
284
  function sceneImportsFor(template, templates, projectRoot, generatedFileDir) {
285
- return [...collectSceneImportPaths(template, templates)]
285
+ return [...collectSceneImportPaths(template)]
286
286
  .sort()
287
287
  .map((scenePath) => {
288
288
  const sceneTemplate = templates.get(scenePath);
@@ -297,31 +297,13 @@ function sceneImportsFor(template, templates, projectRoot, generatedFileDir) {
297
297
  };
298
298
  });
299
299
  }
300
- function collectSceneImportPaths(template, templates) {
300
+ function collectSceneImportPaths(template) {
301
301
  const scenePaths = collectSceneInstancePaths(template.children);
302
- collectSceneStructSourcePaths(template.children, templates, scenePaths);
303
- return scenePaths;
304
- }
305
- function collectSceneStructSourcePaths(nodes, templates, scenePaths) {
306
- for (const node of nodes) {
307
- if (node.kind === 'slotOutlet') {
308
- continue;
309
- }
310
- if (node.kind === 'pixi') {
311
- collectSceneStructSourcePaths(node.children, templates, scenePaths);
312
- continue;
313
- }
314
- const sceneInterface = templates.get(node.scene)?.interface;
315
- for (const [key, value] of Object.entries(node.props)) {
316
- const contract = value && typeof value === 'object' ? sceneInterface?.props[key] : undefined;
317
- if (contract?.type === 'struct' && contract.sourceScene) {
318
- scenePaths.add(contract.sourceScene);
319
- }
320
- }
321
- for (const children of Object.values(node.slots)) {
322
- collectSceneStructSourcePaths(children, templates, scenePaths);
323
- }
302
+ for (const contract of Object.values(template.interface.props)) {
303
+ if (contract.type === 'struct' && contract.sourceScene)
304
+ scenePaths.add(contract.sourceScene);
324
305
  }
306
+ return scenePaths;
325
307
  }
326
308
  function sceneClassAliasesFor(template) {
327
309
  return Object.fromEntries([...collectSceneInstancePaths(template.children)].map((scenePath) => [scenePath, sceneClassAlias(scenePath)]));
@@ -1,4 +1,4 @@
1
- import { isSceneTemplateBindingValue, } from './spec.js';
1
+ import { isSceneTemplateBindingValue, sceneNativeEventNames, } from './spec.js';
2
2
  import { isPixiSceneNodeType, pixiSceneFieldSchema, pixiSceneLayoutProps, pixiSceneNodeAcceptsChildren, pixiSceneNodePropKeys, pixiSceneTransformProps, pixiSceneDisplayProps, } from './pixiNodeSchema.js';
3
3
  import { parseSceneTemplate } from './templateParser.js';
4
4
  import { serializeSceneTemplate } from './templateSerializer.js';
@@ -119,7 +119,40 @@ function sourcePositionFromMessage(source, message) {
119
119
  };
120
120
  }
121
121
  function validateSceneTemplate(template, context) {
122
- return template.children.flatMap((child, index) => validateSceneNode(child, nodePathSegment(index, child), context));
122
+ return [
123
+ ...validateSceneDefaults(template, context),
124
+ ...validateNativeEvents(template.events, '__scene__'),
125
+ ...template.children.flatMap((child, index) => validateSceneNode(child, nodePathSegment(index, child), { ...context, sceneDefaults: template.propDefaults })),
126
+ ];
127
+ }
128
+ function validateSceneDefaults(template, context) {
129
+ if (context.bindingsOnly || !context.sceneInterface)
130
+ return [];
131
+ const diagnostics = [];
132
+ for (const [name, value] of Object.entries(template.propDefaults ?? {})) {
133
+ const contract = context.sceneInterface?.props[name];
134
+ if (!contract) {
135
+ diagnostics.push({ path: '__scene__', prop: `default.${name}`, expected: 'Prop declared by the paired script', actual: 'unknown Prop' });
136
+ continue;
137
+ }
138
+ if (contract.type === 'struct') {
139
+ diagnostics.push(...validateStructFields('__scene__', `default.${name}`, value, contract, template.name));
140
+ continue;
141
+ }
142
+ const expectedType = contract.type === 'variant' ? 'string' : contract.type;
143
+ if (!sceneValueMatchesContractType(value, expectedType) || (contract.type === 'variant' && !contract.variants[String(value)])) {
144
+ diagnostics.push({ path: '__scene__', prop: `default.${name}`, expected: contract.type === 'variant' ? `one of ${Object.keys(contract.variants).join(', ')}` : expectedType, actual: sceneValueType(value) });
145
+ }
146
+ }
147
+ return diagnostics;
148
+ }
149
+ function validateNativeEvents(events, path) {
150
+ return Object.entries(events ?? {}).flatMap(([name, action]) => {
151
+ if (!sceneNativeEventNames.includes(name)) {
152
+ return [{ path, prop: `on:${name}`, expected: `one of ${sceneNativeEventNames.join(', ')}`, actual: name }];
153
+ }
154
+ return action.trim() ? [] : [{ path, prop: `on:${name}`, expected: 'nonempty action name', actual: action }];
155
+ });
123
156
  }
124
157
  function validateSceneNode(node, path, context) {
125
158
  if (node.kind === 'slotOutlet') {
@@ -147,6 +180,7 @@ function validatePixiNode(node, path, context) {
147
180
  }];
148
181
  }
149
182
  const diagnostics = [];
183
+ diagnostics.push(...validateNativeEvents(node.events, path));
150
184
  if (!pixiSceneNodeAcceptsChildren(node.type) && node.children.length > 0) {
151
185
  diagnostics.push({
152
186
  path,
@@ -178,7 +212,7 @@ function validatePixiNode(node, path, context) {
178
212
  }
179
213
  const schema = pixiSceneFieldSchema(prop);
180
214
  if (isSceneTemplateBindingValue(value)) {
181
- diagnostics.push(...validateSceneBinding(path, prop, value, context.sceneInterface, schema?.type, schema?.options));
215
+ diagnostics.push(...validateSceneBinding(path, prop, value, context.sceneInterface, schema?.type, schema?.options, context.sceneDefaults));
182
216
  continue;
183
217
  }
184
218
  if (schema && !sceneValueMatchesFieldType(value, schema.type, schema.options)) {
@@ -252,7 +286,7 @@ function validateSceneInstanceNode(node, path, context) {
252
286
  const contract = sceneInterface.props[prop];
253
287
  if (isSceneTemplateBindingValue(value)) {
254
288
  const expectedType = contract?.type === 'variant' ? 'string' : contract?.type ?? schema?.type;
255
- diagnostics.push(...validateSceneBinding(path, prop, value, context.sceneInterface, expectedType, schema?.options));
289
+ diagnostics.push(...validateSceneBinding(path, prop, value, context.sceneInterface, expectedType, schema?.options, context.sceneDefaults));
256
290
  continue;
257
291
  }
258
292
  if (contract?.type === 'struct') {
@@ -304,7 +338,7 @@ function validateSceneInstanceNode(node, path, context) {
304
338
  }
305
339
  return diagnostics;
306
340
  }
307
- function validateSceneBinding(path, targetProp, binding, sceneInterface, expectedType, options) {
341
+ function validateSceneBinding(path, targetProp, binding, sceneInterface, expectedType, options, sceneDefaults) {
308
342
  const [prop, field] = binding.path;
309
343
  const contract = sceneInterface?.props[prop];
310
344
  if (!contract) {
@@ -349,8 +383,9 @@ function validateSceneBinding(path, targetProp, binding, sceneInterface, expecte
349
383
  }
350
384
  else {
351
385
  sourceType = contract.type;
352
- if (contract.default !== undefined) {
353
- sourceValues = [contract.default];
386
+ const sourceValue = sceneDefaults?.[prop] ?? contract.default;
387
+ if (sourceValue !== undefined && typeof sourceValue !== 'object') {
388
+ sourceValues = [sourceValue];
354
389
  }
355
390
  }
356
391
  const textAcceptsScalar = targetProp === 'text' && ['string', 'number', 'boolean'].includes(sourceType);
@@ -375,13 +410,16 @@ function bindingSourceMatchesTarget(sourceType, values, expectedType, options) {
375
410
  return sourceType === expectedType;
376
411
  }
377
412
  function validateSceneInstanceStructProp(node, path, prop, value, contract) {
378
- if (!value || typeof value !== 'object') {
413
+ return validateStructFields(path, prop, value, contract, node.type);
414
+ }
415
+ function validateStructFields(path, prop, value, contract, ownerType) {
416
+ if (!value || typeof value !== 'object' || isSceneTemplateBindingValue(value)) {
379
417
  return [{
380
418
  path,
381
419
  prop,
382
420
  expected: contract.struct,
383
421
  actual: sceneValueType(value),
384
- hint: `Set ${node.type}.${prop} fields using dot-path attributes such as ${prop}.x="0".`,
422
+ hint: `Set ${ownerType}.${prop} fields using dot-path attributes such as ${prop}.x="0".`,
385
423
  }];
386
424
  }
387
425
  const diagnostics = [];
@@ -403,7 +441,7 @@ function validateSceneInstanceStructProp(node, path, prop, value, contract) {
403
441
  prop: `${prop}.${field}`,
404
442
  expected: fieldContract.type,
405
443
  actual: sceneValueType(fieldValue),
406
- hint: `Set ${node.type}.${prop}.${field} to ${fieldTypeDescription(fieldContract.type)}.`,
444
+ hint: `Set ${ownerType}.${prop}.${field} to ${fieldTypeDescription(fieldContract.type)}.`,
407
445
  });
408
446
  }
409
447
  }
@@ -6,6 +6,7 @@ export interface SceneTemplateBindingValue {
6
6
  path: [string] | [string, string];
7
7
  }
8
8
  export type SceneTemplateValue = SceneTemplateScalarValue | SceneTemplateStructValue | SceneTemplateBindingValue;
9
+ export declare const sceneNativeEventNames: readonly ["pointertap", "pointerdown", "pointerup", "pointerupoutside", "pointerover", "pointerout", "click"];
9
10
  export declare function isSceneTemplateBindingValue(value: SceneTemplateValue | undefined): value is SceneTemplateBindingValue;
10
11
  export type SceneTemplatePrimitivePropType = 'string' | 'number' | 'boolean';
11
12
  export interface SceneTemplatePrimitivePropContract {
@@ -44,6 +45,8 @@ export interface SceneTemplate {
44
45
  version: 2;
45
46
  name: string;
46
47
  props: Record<string, SceneTemplateValue>;
48
+ propDefaults?: Record<string, SceneTemplateValue>;
49
+ events?: Record<string, string>;
47
50
  interface: SceneTemplateInterface;
48
51
  children: SceneTemplateNode[];
49
52
  }
@@ -53,6 +56,7 @@ export interface PixiTemplateNode {
53
56
  type: SceneTemplatePrimitiveType;
54
57
  id?: string;
55
58
  props: Record<string, SceneTemplateValue>;
59
+ events?: Record<string, string>;
56
60
  children: SceneTemplateNode[];
57
61
  }
58
62
  export interface SceneInstanceTemplateNode {
@@ -1,3 +1,4 @@
1
+ export const sceneNativeEventNames = ['pointertap', 'pointerdown', 'pointerup', 'pointerupoutside', 'pointerover', 'pointerout', 'click'];
1
2
  export function isSceneTemplateBindingValue(value) {
2
3
  return !!value && typeof value === 'object' && value.kind === 'binding';
3
4
  }
@@ -43,7 +43,11 @@ export function parseSceneTemplate(source) {
43
43
  return {
44
44
  version: 2,
45
45
  name,
46
- props: parseProps(root, ['name']),
46
+ props: parseProps(root, ['name'], (attribute) => attribute.startsWith('default.') || attribute.startsWith('on:')),
47
+ propDefaults: parseProps({ ...root, attributes: Object.fromEntries(Object.entries(root.attributes)
48
+ .filter(([attribute]) => attribute.startsWith('default.'))
49
+ .map(([attribute, value]) => [attribute.slice('default.'.length), value])) }, [], undefined, true),
50
+ events: parseNativeEvents(root),
47
51
  interface: emptyInterface(),
48
52
  children,
49
53
  };
@@ -85,7 +89,8 @@ function parseTemplateNode(element) {
85
89
  const node = {
86
90
  kind: 'pixi',
87
91
  type: element.name,
88
- props: parseProps(element, ['id', 'slot']),
92
+ props: parseProps(element, ['id', 'slot'], (attribute) => attribute.startsWith('on:')),
93
+ events: parseNativeEvents(element),
89
94
  children: element.children.map(parseTemplateNode),
90
95
  ...(element.attributes.id ? { id: element.attributes.id } : {}),
91
96
  };
@@ -93,13 +98,13 @@ function parseTemplateNode(element) {
93
98
  }
94
99
  throw new Error(`Unsupported template tag <${element.name}>. Add scene="..." or use a built-in Scene name.`);
95
100
  }
96
- function parseProps(element, omitted, shouldOmit = () => false) {
101
+ function parseProps(element, omitted, shouldOmit = () => false, allowQuotedStrings = false) {
97
102
  const props = {};
98
103
  for (const [name, value] of Object.entries(element.attributes)) {
99
104
  if (omitted.includes(name) || shouldOmit(name)) {
100
105
  continue;
101
106
  }
102
- const parsed = parseAttributeValue(name, value);
107
+ const parsed = parseAttributeValue(name, value, allowQuotedStrings);
103
108
  const path = name.split('.');
104
109
  if (path.length === 1) {
105
110
  if (props[name] && typeof props[name] === 'object') {
@@ -134,6 +139,11 @@ function parseEvents(element) {
134
139
  }
135
140
  return events;
136
141
  }
142
+ function parseNativeEvents(element) {
143
+ return Object.fromEntries(Object.entries(element.attributes)
144
+ .filter(([name]) => name.startsWith('on:'))
145
+ .map(([name, value]) => [name.slice(3), value]));
146
+ }
137
147
  function eventName(name) {
138
148
  if (name.startsWith('@')) {
139
149
  return name.slice(1);
@@ -149,7 +159,9 @@ function eventName(name) {
149
159
  function isEventAttribute(name) {
150
160
  return eventName(name) !== undefined;
151
161
  }
152
- function parseAttributeValue(name, value) {
162
+ function parseAttributeValue(name, value, allowQuotedStrings = false) {
163
+ if (allowQuotedStrings && value.startsWith('"') && value.endsWith('"'))
164
+ return JSON.parse(value);
153
165
  const binding = parseBindingValue(value);
154
166
  if (binding) {
155
167
  return binding;
@@ -16,6 +16,8 @@ export function serializeSceneTemplate(template) {
16
16
  const rootAttributes = [
17
17
  ['name', template.name],
18
18
  ...Object.entries(template.props),
19
+ ...Object.entries(template.propDefaults ?? {}).map(([name, value]) => [`default.${name}`, value]),
20
+ ...Object.entries(template.events ?? {}).map(([name, value]) => [`on:${name}`, value]),
19
21
  ];
20
22
  const lines = [`<Scene${serializeAttributes(rootAttributes)}>`];
21
23
  for (const child of template.children) {
@@ -37,6 +39,7 @@ function serializeTemplateNode(node, depth, extraAttributes = []) {
37
39
  ...(node.id ? [['id', node.id]] : []),
38
40
  ...extraAttributes,
39
41
  ...Object.entries(node.props),
42
+ ...Object.entries(node.events ?? {}).map(([name, value]) => [`on:${name}`, value]),
40
43
  ];
41
44
  return serializeElement(node.type, attributes, node.children, depth);
42
45
  }
@@ -89,6 +92,12 @@ function formatAttributeValue(name, value) {
89
92
  if (isBindingValue(value)) {
90
93
  return `{${value.path.join('.')}}`;
91
94
  }
95
+ if (name.startsWith('default.') && typeof value === 'string'
96
+ && (/^(?:true|false|-?\d+(?:\.\d+)?|#[0-9a-fA-F]{6})$/.test(value)
97
+ || value.startsWith('{') || value.endsWith('}')
98
+ || (value.startsWith('"') && value.endsWith('"')))) {
99
+ return JSON.stringify(value);
100
+ }
92
101
  if (typeof value === 'number' && colorPropNames.has(name)) {
93
102
  return `#${value.toString(16).padStart(6, '0').slice(-6)}`;
94
103
  }
@@ -70,6 +70,7 @@ class CompileContext {
70
70
  this.#lines.push(`registerScene(${JSON.stringify(this.options.registrationPath)}, {`);
71
71
  this.#lines.push(` assets: ${JSON.stringify([...this.#textures.keys()])},`);
72
72
  this.#lines.push(` dependencies: ${JSON.stringify(this.options.sceneDependencies ?? [])},`);
73
+ this.#lines.push(` propDefaults: () => (${this.#sceneDefaultProps()}),`);
73
74
  this.#lines.push(` mount: ${functionName},`);
74
75
  this.#lines.push(` prepare: ${this.#prepareFunctionName()},`);
75
76
  this.#lines.push('});');
@@ -106,7 +107,10 @@ class CompileContext {
106
107
  }
107
108
  }
108
109
  if (this.options.scriptImport) {
109
- lines.push(`import { ${this.options.scriptImport.exportName} as ${this.options.scriptImport.localName} } from ${JSON.stringify(this.options.scriptImport.source)};`);
110
+ const localStructs = Object.values(this.template.interface.props)
111
+ .filter((contract) => contract.type === 'struct' && !contract.sourceScene)
112
+ .map((contract) => contract.struct);
113
+ lines.push(`import { ${[`${this.options.scriptImport.exportName} as ${this.options.scriptImport.localName}`, ...new Set(localStructs)].join(', ')} } from ${JSON.stringify(this.options.scriptImport.source)};`);
110
114
  }
111
115
  if (this.#runtimeImports.size > 0) {
112
116
  lines.push(`import { ${[...this.#runtimeImports].sort().join(', ')} } from 'pixifact/scene';`);
@@ -226,6 +230,13 @@ class CompileContext {
226
230
  this.#recordSource(variable, locator);
227
231
  this.#applyNodeId(variable, node.id);
228
232
  this.#applyPixiProps(variable, node.props, false, undefined, node.type);
233
+ if (Object.keys(node.events ?? {}).length > 0 && node.props.eventMode === undefined) {
234
+ this.#lines.push(` ${variable}.eventMode = 'static';`);
235
+ }
236
+ for (const [eventName, actionName] of Object.entries(node.events ?? {})) {
237
+ this.#runtimeImports.add('connectSceneNodeEvent');
238
+ this.#lines.push(` connectSceneNodeEvent(${variable}, ${JSON.stringify(eventName)}, ${JSON.stringify(actionName)}, root, ${actionsParameter});`);
239
+ }
229
240
  this.#applyParentSorting(parent, node.props);
230
241
  this.#lines.push(` ${parent}.addChild(${variable});`);
231
242
  this.#lines.push(` __pixifactNodes[${JSON.stringify(locator)}] = ${variable};`);
@@ -237,7 +248,7 @@ class CompileContext {
237
248
  const variable = node.id || this.#anonymousName(node.type);
238
249
  const locator = compilerSceneNodeLocator(node, path);
239
250
  const constructorName = this.#sceneConstructorName(node);
240
- const constructorArgs = this.#sceneInstanceConstructorArgs(variable, node);
251
+ const constructorArgs = this.#sceneInstanceConstructorArgs(node);
241
252
  this.#lines.push(` const ${variable} = new ${constructorName}(${constructorArgs});`);
242
253
  this.#recordSource(variable, locator, true);
243
254
  this.#applyPixiProps(variable, node.props, true, node);
@@ -264,7 +275,7 @@ class CompileContext {
264
275
  const variable = node.id || this.#anonymousName(node.type);
265
276
  const locator = compilerSceneNodeLocator(node, path);
266
277
  const constructorName = this.#sceneConstructorName(node);
267
- const constructorArgs = this.#sceneInstanceConstructorArgs(variable, node);
278
+ const constructorArgs = this.#sceneInstanceConstructorArgs(node);
268
279
  this.#lines.push(` const ${variable} = new ${constructorName}(${constructorArgs});`);
269
280
  this.#recordSource(variable, locator, true);
270
281
  this.#applyPixiProps(variable, node.props, true, node);
@@ -322,7 +333,7 @@ class CompileContext {
322
333
  #sceneConstructorName(node) {
323
334
  return this.options.sceneClassAliases?.[node.scene] ?? node.type;
324
335
  }
325
- #sceneInstanceConstructorArgs(variable, node) {
336
+ #sceneInstanceConstructorArgs(node) {
326
337
  const sceneInterface = this.options.sceneInterfaces?.[node.scene];
327
338
  if (!sceneInterface) {
328
339
  return '';
@@ -339,8 +350,7 @@ class CompileContext {
339
350
  continue;
340
351
  }
341
352
  if (contract.type === 'struct' && value && typeof value === 'object') {
342
- const structVariable = this.#createStructValue(variable, key, value, contract);
343
- entries.push(`${key}: ${structVariable}`);
353
+ entries.push(`${key}: ${this.#value(value)}`);
344
354
  continue;
345
355
  }
346
356
  entries.push(`${key}: ${this.#value(value)}`);
@@ -353,29 +363,11 @@ class CompileContext {
353
363
  return [];
354
364
  }
355
365
  const structs = new Set();
356
- this.#collectSceneStructImports(this.template.children, scene, structs);
357
- return [...structs].sort();
358
- }
359
- #collectSceneStructImports(nodes, sourceScene, structs) {
360
- for (const node of nodes) {
361
- if (node.kind === 'slotOutlet') {
362
- continue;
363
- }
364
- if (node.kind === 'pixi') {
365
- this.#collectSceneStructImports(node.children, sourceScene, structs);
366
- continue;
367
- }
368
- const sceneInterface = this.options.sceneInterfaces?.[node.scene];
369
- for (const [key, value] of Object.entries(node.props)) {
370
- const contract = value && typeof value === 'object' ? sceneInterface?.props[key] : undefined;
371
- if (contract?.type === 'struct' && this.#structContractSourceScene(node.scene, contract) === sourceScene) {
372
- structs.add(contract.struct);
373
- }
374
- }
375
- for (const children of Object.values(node.slots)) {
376
- this.#collectSceneStructImports(children, sourceScene, structs);
377
- }
366
+ for (const contract of Object.values(this.template.interface.props)) {
367
+ if (contract.type === 'struct' && contract.sourceScene === scene)
368
+ structs.add(contract.struct);
378
369
  }
370
+ return [...structs].sort();
379
371
  }
380
372
  #usesSceneClassImport(sceneImport) {
381
373
  const scene = this.#sceneImportScene(sceneImport);
@@ -413,9 +405,6 @@ class CompileContext {
413
405
  return sceneImport.scene
414
406
  ?? Object.entries(this.options.sceneClassAliases ?? {}).find(([, alias]) => alias === sceneImport.localName)?.[0];
415
407
  }
416
- #structContractSourceScene(instanceScene, contract) {
417
- return contract.sourceScene ?? instanceScene;
418
- }
419
408
  #applyPixiProps(variable, props, instance = false, sceneInstance, pixiType) {
420
409
  const publicProps = new Set(Object.keys(sceneInstance ? this.options.sceneInterfaces?.[sceneInstance.scene]?.props ?? {} : {}));
421
410
  const bindingEntries = Object.entries(props).filter((entry) => (isSceneBindingValue(entry[1]) && !publicProps.has(entry[0])));
@@ -515,6 +504,25 @@ class CompileContext {
515
504
  this.#lines.push(` root.setSize(${this.#value(width ?? 0)}, ${this.#value(height ?? 0)});`);
516
505
  }
517
506
  this.#applyPixiProps('root', props);
507
+ if (Object.keys(this.template.events ?? {}).length > 0 && props.eventMode === undefined) {
508
+ this.#lines.push(" root.eventMode = 'static';");
509
+ }
510
+ for (const [eventName, actionName] of Object.entries(this.template.events ?? {})) {
511
+ this.#runtimeImports.add('connectSceneNodeEvent');
512
+ this.#lines.push(` connectSceneNodeEvent(root, ${JSON.stringify(eventName)}, ${JSON.stringify(actionName)}, root, ${this.options.actionsParameter || 'actions'});`);
513
+ }
514
+ }
515
+ #sceneDefaultProps() {
516
+ const editorDefaults = this.template.propDefaults ?? {};
517
+ const entries = Object.entries(this.template.interface.props).flatMap(([name, contract]) => {
518
+ const explicit = editorDefaults[name];
519
+ if (contract.type === 'struct') {
520
+ const overrides = explicit && typeof explicit === 'object' ? explicit : {};
521
+ return [`${JSON.stringify(name)}: Object.assign(new ${contract.struct}(), ${this.#value(overrides)})`];
522
+ }
523
+ return explicit === undefined ? [] : [`${JSON.stringify(name)}: ${this.#value(explicit)}`];
524
+ });
525
+ return `{ ${entries.join(', ')} }`;
518
526
  }
519
527
  #applyLayoutProps(variable, props) {
520
528
  const entries = pixiSceneLayoutProps.flatMap((key) => {
@@ -631,6 +639,8 @@ class CompileContext {
631
639
  : `{ texture: ${this.#textureValue(props.texture)} }`;
632
640
  }
633
641
  #hasEvents() {
642
+ if (Object.keys(this.template.events ?? {}).length > 0)
643
+ return true;
634
644
  for (const child of this.template.children) {
635
645
  if (this.#nodeHasEvents(child)) {
636
646
  return true;
@@ -648,7 +658,7 @@ class CompileContext {
648
658
  }
649
659
  return Object.values(node.slots).some((children) => children.some((child) => this.#nodeHasEvents(child)));
650
660
  }
651
- return node.children.some((child) => this.#nodeHasEvents(child));
661
+ return Object.keys(node.events ?? {}).length > 0 || node.children.some((child) => this.#nodeHasEvents(child));
652
662
  }
653
663
  #isTextStyleProp(key) {
654
664
  return textStyleProps.has(key);
@@ -1,5 +1,5 @@
1
1
  import { activateRuntimeTree } from '../runtime/runtimeBehavior.js';
2
- import { mountSceneClass } from './sceneRuntime.js';
2
+ import { mountSceneClass, scenePropDefaultsForClass } from './sceneRuntime.js';
3
3
  import { initializeSceneProps } from './sceneBindingRuntime.js';
4
4
  const metadataByConstructor = new WeakMap();
5
5
  export function scene() {
@@ -14,7 +14,7 @@ export function scene() {
14
14
  const initialProps = args[0] && typeof args[0] === 'object'
15
15
  ? args[0]
16
16
  : {};
17
- initializeSceneProps(this, metadata.props, initialProps);
17
+ initializeSceneProps(this, metadata.props, initialProps, scenePropDefaultsForClass(SceneClass));
18
18
  const result = mountSceneClass(this, SceneClass);
19
19
  for (const [property, id] of metadata.parts) {
20
20
  Object.defineProperty(this, property, {
@@ -1,3 +1,4 @@
1
+ import type { Container } from 'pixi.js';
1
2
  export type SceneEventListener<TPayload = void> = TPayload extends void ? () => void : (payload: TPayload) => void;
2
3
  export interface SceneEvent<TPayload = void> {
3
4
  connect(listener: SceneEventListener<TPayload>): () => void;
@@ -6,3 +7,4 @@ export interface SceneEvent<TPayload = void> {
6
7
  export type SceneActions = Record<string, (() => void) | undefined>;
7
8
  export declare function createEvent<TPayload = void>(): SceneEvent<TPayload>;
8
9
  export declare function connectSceneEvent(event: SceneEvent, actionName: string, owner: object, actions?: SceneActions): () => void;
10
+ export declare function connectSceneNodeEvent(target: Container, eventName: string, actionName: string, owner: object, actions?: SceneActions): void;
@@ -27,3 +27,16 @@ export function connectSceneEvent(event, actionName, owner, actions = {}) {
27
27
  }
28
28
  throw new Error(`Scene action "${actionName}" was not found.`);
29
29
  }
30
+ export function connectSceneNodeEvent(target, eventName, actionName, owner, actions = {}) {
31
+ const action = actions[actionName];
32
+ if (action) {
33
+ target.on(eventName, action);
34
+ return;
35
+ }
36
+ const ownerAction = owner[actionName];
37
+ if (typeof ownerAction === 'function') {
38
+ target.on(eventName, () => ownerAction.call(owner));
39
+ return;
40
+ }
41
+ throw new Error(`Scene action "${actionName}" was not found.`);
42
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Container } from 'pixi.js';
2
2
  import type { ScenePropDecoratorOptions, SceneTemplateBindingValue, SceneTemplateInterface, SceneTemplateScalarValue } from '../compiler/spec.js';
3
- export declare function initializeSceneProps(target: object, definitions: ReadonlyMap<string, ScenePropDecoratorOptions>, initialProps?: Record<string, unknown>): void;
4
- export declare function initializeScenePropsFromInterface(target: object, sceneInterface: SceneTemplateInterface, initialProps?: Record<string, unknown>): void;
3
+ export declare function initializeSceneProps(target: object, definitions: ReadonlyMap<string, ScenePropDecoratorOptions>, initialProps?: Record<string, unknown>, sceneDefaults?: Record<string, unknown>): void;
4
+ export declare function initializeScenePropsFromInterface(target: object, sceneInterface: SceneTemplateInterface, initialProps?: Record<string, unknown>, sceneDefaults?: Record<string, unknown>): void;
5
5
  export declare function setSceneProp(target: object, name: string, value: unknown): void;
6
6
  export declare function getSceneProp<T = unknown>(target: object, name: string): T;
7
7
  export declare function readSceneBindingValue<T = unknown>(target: object, binding: SceneTemplateBindingValue): T;
@@ -3,7 +3,7 @@ const states = new WeakMap();
3
3
  const layoutProps = new Set(['left', 'right', 'top', 'bottom', 'horizontal', 'vertical']);
4
4
  const textStyleProps = new Set(['fontSize', 'fontFamily', 'fontWeight', 'fill']);
5
5
  const colorProps = new Set(['fill', 'fillColor', 'strokeColor', 'tint']);
6
- export function initializeSceneProps(target, definitions, initialProps = {}) {
6
+ export function initializeSceneProps(target, definitions, initialProps = {}, sceneDefaults = {}) {
7
7
  const state = {
8
8
  definitions: Object.fromEntries(definitions),
9
9
  subscribers: new Map(),
@@ -11,9 +11,13 @@ export function initializeSceneProps(target, definitions, initialProps = {}) {
11
11
  };
12
12
  states.set(target, state);
13
13
  for (const [name, definition] of Object.entries(state.definitions)) {
14
+ const fallback = Object.hasOwn(sceneDefaults, name) ? sceneDefaults[name] : definition.default;
15
+ const initial = initialProps[name];
14
16
  state.values[name] = Object.hasOwn(initialProps, name)
15
- ? initialProps[name]
16
- : definition.default;
17
+ ? initial && fallback && typeof initial === 'object' && typeof fallback === 'object'
18
+ ? Object.assign(fallback, initial)
19
+ : initial
20
+ : fallback;
17
21
  Object.defineProperty(target, name, {
18
22
  configurable: true,
19
23
  enumerable: true,
@@ -22,14 +26,14 @@ export function initializeSceneProps(target, definitions, initialProps = {}) {
22
26
  });
23
27
  }
24
28
  }
25
- export function initializeScenePropsFromInterface(target, sceneInterface, initialProps = {}) {
29
+ export function initializeScenePropsFromInterface(target, sceneInterface, initialProps = {}, sceneDefaults = {}) {
26
30
  initializeSceneProps(target, new Map(Object.entries(sceneInterface.props).map(([name, contract]) => [
27
31
  name,
28
32
  {
29
33
  ...('default' in contract ? { default: contract.default } : {}),
30
34
  ...(contract.type === 'variant' ? { variants: contract.variants } : {}),
31
35
  },
32
- ])), initialProps);
36
+ ])), initialProps, sceneDefaults);
33
37
  }
34
38
  export function setSceneProp(target, name, value) {
35
39
  const state = sceneBindingState(target);
@@ -9,11 +9,13 @@ export interface SceneMountResult {
9
9
  export interface SceneDefinition {
10
10
  assets: string[];
11
11
  dependencies: string[];
12
+ propDefaults?: () => Record<string, unknown>;
12
13
  mount(root: Group): SceneMountResult;
13
14
  prepare(): Promise<void>;
14
15
  }
15
16
  export declare function registerScene(path: string, definition: SceneDefinition): void;
16
17
  export declare function registerSceneClass(constructor: object, path: string): void;
18
+ export declare function scenePropDefaultsForClass(constructor: object): Record<string, unknown>;
17
19
  export declare function mountScene(root: Group, path: string): SceneMountResult;
18
20
  export declare function mountSceneClass(root: Group, constructor: object): SceneMountResult;
19
21
  export declare function prepareScene(path: string): Promise<void>;
@@ -11,6 +11,10 @@ export function registerScene(path, definition) {
11
11
  export function registerSceneClass(constructor, path) {
12
12
  scenePathsByConstructor.set(constructor, path);
13
13
  }
14
+ export function scenePropDefaultsForClass(constructor) {
15
+ const path = scenePathsByConstructor.get(constructor);
16
+ return path ? sceneDefinitions.get(path)?.propDefaults?.() ?? {} : {};
17
+ }
14
18
  export function mountScene(root, path) {
15
19
  const definition = sceneDefinitions.get(path);
16
20
  if (!definition) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixifact",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Pixifact 2D UI 和轻量游戏 Scene 语义框架。",
5
5
  "packageManager": "bun@1.3.13",
6
6
  "main": "./dist/index.js",