@holoscript/plugin-fashion 2.0.1 → 2.0.2

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-fashion",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "main": "src/index.ts",
5
5
  "peerDependencies": {
6
- "@holoscript/core": "8.0.6"
6
+ "@holoscript/core": ">=8.0.0"
7
7
  },
8
8
  "license": "MIT",
9
9
  "scripts": {
10
10
  "test": "vitest run --passWithNoTests",
11
11
  "test:coverage": "vitest run --coverage --passWithNoTests"
12
12
  }
13
- }
13
+ }
@@ -29,20 +29,20 @@ describe('patternGrading', () => {
29
29
 
30
30
  it('base size measurements equal base values', () => {
31
31
  const r = patternGrading(rules, sizes, baseSizeIndex);
32
- const base = r.sizes.find(s => s.size === 'M')!;
32
+ const base = r.sizes.find((s) => s.size === 'M')!;
33
33
  expect(base.measurements['chest']).toBe(88);
34
34
  expect(base.measurements['waist']).toBe(68);
35
35
  });
36
36
 
37
37
  it('next size up = base + increment', () => {
38
38
  const r = patternGrading(rules, sizes, baseSizeIndex);
39
- const large = r.sizes.find(s => s.size === 'L')!;
39
+ const large = r.sizes.find((s) => s.size === 'L')!;
40
40
  expect(large.measurements['chest']).toBeCloseTo(88 + 4, 1);
41
41
  });
42
42
 
43
43
  it('next size down = base − increment', () => {
44
44
  const r = patternGrading(rules, sizes, baseSizeIndex);
45
- const small = r.sizes.find(s => s.size === 'S')!;
45
+ const small = r.sizes.find((s) => s.size === 'S')!;
46
46
  expect(small.measurements['chest']).toBeCloseTo(88 - 4, 1);
47
47
  });
48
48
 
@@ -70,13 +70,13 @@ describe('patternGrading', () => {
70
70
  describe('fabricWasteEstimator', () => {
71
71
  const pieces = [
72
72
  { name: 'front', widthCm: 60, heightCm: 80, quantity: 2 },
73
- { name: 'back', widthCm: 55, heightCm: 80, quantity: 2 },
74
- { name: 'sleeve',widthCm: 40, heightCm: 60, quantity: 2 },
73
+ { name: 'back', widthCm: 55, heightCm: 80, quantity: 2 },
74
+ { name: 'sleeve', widthCm: 40, heightCm: 60, quantity: 2 },
75
75
  ];
76
76
 
77
77
  it('nestingEfficiency matches input factor', () => {
78
- const r = fabricWasteEstimator(pieces, 150, 0.80);
79
- expect(r.nestingEfficiency).toBeCloseTo(0.80, 4);
78
+ const r = fabricWasteEstimator(pieces, 150, 0.8);
79
+ expect(r.nestingEfficiency).toBeCloseTo(0.8, 4);
80
80
  });
81
81
 
82
82
  it('wastePct = 1 − nestingEfficiency', () => {
@@ -85,8 +85,8 @@ describe('fabricWasteEstimator', () => {
85
85
  });
86
86
 
87
87
  it('fabricAreaCm2 = totalPieceAreaCm2 / efficiency', () => {
88
- const r = fabricWasteEstimator(pieces, 150, 0.80);
89
- expect(r.fabricAreaCm2).toBeCloseTo(r.totalPieceAreaCm2 / 0.80, 2);
88
+ const r = fabricWasteEstimator(pieces, 150, 0.8);
89
+ expect(r.fabricAreaCm2).toBeCloseTo(r.totalPieceAreaCm2 / 0.8, 2);
90
90
  });
91
91
 
92
92
  it('totalPieceAreaCm2 = sum of width × height × qty', () => {
@@ -168,13 +168,19 @@ describe('trendMomentum', () => {
168
168
  });
169
169
 
170
170
  it('velocity > 0', () => {
171
- const periods = [{ period: 'A', unitsSold: 100 }, { period: 'B', unitsSold: 150 }];
171
+ const periods = [
172
+ { period: 'A', unitsSold: 100 },
173
+ { period: 'B', unitsSold: 150 },
174
+ ];
172
175
  const r = trendMomentum(periods);
173
176
  expect(r.velocity).toBeGreaterThan(0);
174
177
  });
175
178
 
176
179
  it('projectedNextPeriod ≥ 0', () => {
177
- const periods = [{ period: 'A', unitsSold: 50 }, { period: 'B', unitsSold: 60 }];
180
+ const periods = [
181
+ { period: 'A', unitsSold: 50 },
182
+ { period: 'B', unitsSold: 60 },
183
+ ];
178
184
  const r = trendMomentum(periods);
179
185
  expect(r.projectedNextPeriod).toBeGreaterThanOrEqual(0);
180
186
  });
@@ -215,7 +221,8 @@ describe('buildFashionReceipt', () => {
215
221
  it('accepted=true for efficient nesting', () => {
216
222
  const fabricWaste = fabricWasteEstimator(
217
223
  [{ name: 'front', widthCm: 50, heightCm: 60, quantity: 2 }],
218
- 150, 0.85, // 15% waste < 25%
224
+ 150,
225
+ 0.85 // 15% waste < 25%
219
226
  );
220
227
  const receipt = buildFashionReceipt({ fabricWaste, converged: true });
221
228
  expect(receipt.acceptance.accepted).toBe(true);
@@ -224,7 +231,8 @@ describe('buildFashionReceipt', () => {
224
231
  it('accepted=false for high fabric waste', () => {
225
232
  const fabricWaste = fabricWasteEstimator(
226
233
  [{ name: 'front', widthCm: 50, heightCm: 60, quantity: 2 }],
227
- 150, 0.70, // 30% waste > 25%
234
+ 150,
235
+ 0.7 // 30% waste > 25%
228
236
  );
229
237
  expect(fabricWaste.wastePct).toBeGreaterThan(0.25);
230
238
  const receipt = buildFashionReceipt({ fabricWaste, converged: true });
@@ -236,8 +244,8 @@ describe('buildFashionReceipt', () => {
236
244
  const trend = trendMomentum([
237
245
  { period: 'A', unitsSold: 200 },
238
246
  { period: 'B', unitsSold: 100 },
239
- { period: 'C', unitsSold: 50 },
240
- { period: 'D', unitsSold: 20 },
247
+ { period: 'C', unitsSold: 50 },
248
+ { period: 'D', unitsSold: 20 },
241
249
  ]);
242
250
  expect(trend.trend).toBe('down');
243
251
  const receipt = buildFashionReceipt({ trend, converged: true });
@@ -57,7 +57,12 @@ export interface FabricWasteResult {
57
57
  nestingEfficiency: number;
58
58
  }
59
59
 
60
- export type ColorHarmony = 'complementary' | 'analogous' | 'triadic' | 'split-complementary' | 'neutral';
60
+ export type ColorHarmony =
61
+ | 'complementary'
62
+ | 'analogous'
63
+ | 'triadic'
64
+ | 'split-complementary'
65
+ | 'neutral';
61
66
 
62
67
  export interface ColorHarmonyResult {
63
68
  harmony: ColorHarmony;
@@ -87,7 +92,9 @@ export interface CostPerWearResult {
87
92
  estimatedWears: number;
88
93
  }
89
94
 
90
- export interface FashionReceiptOptions { runId?: string; }
95
+ export interface FashionReceiptOptions {
96
+ runId?: string;
97
+ }
91
98
 
92
99
  export interface FashionAnalysisResult {
93
100
  grading?: PatternGradingResult;
@@ -106,11 +113,12 @@ export interface FashionAnalysisResult {
106
113
  export function patternGrading(
107
114
  rules: GradingRule[],
108
115
  sizes: string[],
109
- baseSizeIndex: number,
116
+ baseSizeIndex: number
110
117
  ): PatternGradingResult {
111
118
  if (rules.length === 0) throw new Error('No grading rules');
112
119
  if (sizes.length === 0) throw new Error('No sizes');
113
- if (baseSizeIndex < 0 || baseSizeIndex >= sizes.length) throw new Error('baseSizeIndex out of range');
120
+ if (baseSizeIndex < 0 || baseSizeIndex >= sizes.length)
121
+ throw new Error('baseSizeIndex out of range');
114
122
 
115
123
  const gradedSizes: GradedSize[] = sizes.map((size, idx) => {
116
124
  const step = idx - baseSizeIndex;
@@ -139,7 +147,7 @@ export function patternGrading(
139
147
  export function fabricWasteEstimator(
140
148
  pieces: PatternPiece[],
141
149
  fabricWidthCm: number,
142
- nestingEfficiencyFactor = 0.82,
150
+ nestingEfficiencyFactor = 0.82
143
151
  ): FabricWasteResult {
144
152
  if (pieces.length === 0) throw new Error('No pattern pieces');
145
153
  if (fabricWidthCm <= 0) throw new Error('Fabric width must be positive');
@@ -166,8 +174,11 @@ export function fabricWasteEstimator(
166
174
  * Analogous: |Δhue| ≤ 30° → low contrast, harmonious
167
175
  * Triadic: |Δhue| ≈ 120° → balanced
168
176
  */
169
- export function colorHarmonyScore(primaryHueDeg: number, secondaryHueDeg: number): ColorHarmonyResult {
170
- const diff = Math.abs(((primaryHueDeg - secondaryHueDeg) % 360 + 360) % 360);
177
+ export function colorHarmonyScore(
178
+ primaryHueDeg: number,
179
+ secondaryHueDeg: number
180
+ ): ColorHarmonyResult {
181
+ const diff = Math.abs((((primaryHueDeg - secondaryHueDeg) % 360) + 360) % 360);
171
182
  const norm = diff > 180 ? 360 - diff : diff;
172
183
 
173
184
  let harmony: ColorHarmony;
@@ -210,7 +221,7 @@ export function trendMomentum(periods: SalesPeriod[]): TrendResult {
210
221
 
211
222
  const firstHalf = periods.slice(0, Math.floor(n / 2));
212
223
  const secondHalf = periods.slice(Math.ceil(n / 2));
213
- const avgFirst = firstHalf.reduce((s, p) => s + p.unitsSold, 0) / firstHalf.length;
224
+ const avgFirst = firstHalf.reduce((s, p) => s + p.unitsSold, 0) / firstHalf.length;
214
225
  const avgSecond = secondHalf.reduce((s, p) => s + p.unitsSold, 0) / secondHalf.length;
215
226
 
216
227
  const changePct = avgFirst > 0 ? (avgSecond - avgFirst) / avgFirst : 0;
@@ -230,7 +241,7 @@ export function trendMomentum(periods: SalesPeriod[]): TrendResult {
230
241
  export function costPerWear(
231
242
  purchasePriceUSD: number,
232
243
  careCostPerWearUSD: number,
233
- estimatedWears: number,
244
+ estimatedWears: number
234
245
  ): CostPerWearResult {
235
246
  if (purchasePriceUSD < 0) throw new Error('Purchase price must be ≥ 0');
236
247
  if (estimatedWears <= 0) throw new Error('estimatedWears must be positive');
@@ -247,18 +258,27 @@ export function costPerWear(
247
258
 
248
259
  export function buildFashionReceipt(
249
260
  result: FashionAnalysisResult,
250
- options?: FashionReceiptOptions,
261
+ options?: FashionReceiptOptions
251
262
  ): DomainSimulationReceipt {
252
263
  const violations: Array<{ criterion: string; message: string }> = [];
253
264
 
254
265
  if (result.fabricWaste && result.fabricWaste.wastePct > 0.25) {
255
- violations.push({ criterion: 'fabric_waste', message: `Fabric waste ${(result.fabricWaste.wastePct * 100).toFixed(1)}% exceeds 25% threshold — review nesting` });
266
+ violations.push({
267
+ criterion: 'fabric_waste',
268
+ message: `Fabric waste ${(result.fabricWaste.wastePct * 100).toFixed(1)}% exceeds 25% threshold — review nesting`,
269
+ });
256
270
  }
257
271
  if (result.colorHarmony && result.colorHarmony.harmonyScore < 50) {
258
- violations.push({ criterion: 'color_harmony', message: `Color harmony score ${result.colorHarmony.harmonyScore} below 50 — consider palette revision` });
272
+ violations.push({
273
+ criterion: 'color_harmony',
274
+ message: `Color harmony score ${result.colorHarmony.harmonyScore} below 50 — consider palette revision`,
275
+ });
259
276
  }
260
277
  if (result.trend && result.trend.trend === 'down') {
261
- violations.push({ criterion: 'trend', message: `Category showing declining trend (velocity ${result.trend.velocity.toFixed(1)} units/period)` });
278
+ violations.push({
279
+ criterion: 'trend',
280
+ message: `Category showing declining trend (velocity ${result.trend.velocity.toFixed(1)} units/period)`,
281
+ });
262
282
  }
263
283
 
264
284
  return buildDomainSimulationReceipt({
@@ -272,7 +292,11 @@ export function buildFashionReceipt(
272
292
  colorHarmonyScore: result.colorHarmony?.harmonyScore ?? null,
273
293
  trendDirection: result.trend?.trend ?? null,
274
294
  },
275
- cael: { version: 'cael.v1', event: 'fashion.design_analysis', solverType: 'fashion.pattern-grading' },
295
+ cael: {
296
+ version: 'cael.v1',
297
+ event: 'fashion.design_analysis',
298
+ solverType: 'fashion.pattern-grading',
299
+ },
276
300
  acceptance: { accepted: violations.length === 0, violations },
277
301
  });
278
302
  }
package/src/index.ts CHANGED
@@ -1,12 +1,32 @@
1
1
  export * from './fashionsolver';
2
- export { createGarmentHandler, type GarmentConfig, type GarmentCategory } from './traits/GarmentTrait';
3
- export { createFabricSimulationHandler, type FabricSimulationConfig, type FabricType } from './traits/FabricSimulationTrait';
4
- export { createRunwayChoreographyHandler, type RunwayChoreographyConfig, type RunwaySegment } from './traits/RunwayChoreographyTrait';
2
+ export {
3
+ createGarmentHandler,
4
+ type GarmentConfig,
5
+ type GarmentCategory,
6
+ } from './traits/GarmentTrait';
7
+ export {
8
+ createFabricSimulationHandler,
9
+ type FabricSimulationConfig,
10
+ type FabricType,
11
+ } from './traits/FabricSimulationTrait';
12
+ export {
13
+ createRunwayChoreographyHandler,
14
+ type RunwayChoreographyConfig,
15
+ type RunwaySegment,
16
+ } from './traits/RunwayChoreographyTrait';
5
17
  export * from './traits/types';
6
18
 
7
19
  import { createGarmentHandler } from './traits/GarmentTrait';
8
20
  import { createFabricSimulationHandler } from './traits/FabricSimulationTrait';
9
21
  import { createRunwayChoreographyHandler } from './traits/RunwayChoreographyTrait';
10
22
 
11
- export const pluginMeta = { name: '@holoscript/plugin-fashion', version: '1.0.0', traits: ['garment', 'fabric_simulation', 'runway_choreography'] };
12
- export const traitHandlers = [createGarmentHandler(), createFabricSimulationHandler(), createRunwayChoreographyHandler()];
23
+ export const pluginMeta = {
24
+ name: '@holoscript/plugin-fashion',
25
+ version: '1.0.0',
26
+ traits: ['garment', 'fabric_simulation', 'runway_choreography'],
27
+ };
28
+ export const traitHandlers = [
29
+ createGarmentHandler(),
30
+ createFabricSimulationHandler(),
31
+ createRunwayChoreographyHandler(),
32
+ ];
@@ -1,25 +1,71 @@
1
1
  /** @fabric_simulation Trait — Cloth physics simulation. @trait fabric_simulation */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type FabricType = 'woven' | 'knit' | 'denim' | 'silk' | 'leather' | 'synthetic' | 'lace' | 'tulle';
5
- export interface FabricSimulationConfig { fabricType: FabricType; stiffness: number; elasticity: number; drapeCoefficient: number; windResistance: number; gravityScale: number; collisionMargin: number; vertexCount: number; }
4
+ export type FabricType =
5
+ | 'woven'
6
+ | 'knit'
7
+ | 'denim'
8
+ | 'silk'
9
+ | 'leather'
10
+ | 'synthetic'
11
+ | 'lace'
12
+ | 'tulle';
13
+ export interface FabricSimulationConfig {
14
+ fabricType: FabricType;
15
+ stiffness: number;
16
+ elasticity: number;
17
+ drapeCoefficient: number;
18
+ windResistance: number;
19
+ gravityScale: number;
20
+ collisionMargin: number;
21
+ vertexCount: number;
22
+ }
6
23
 
7
- const defaultConfig: FabricSimulationConfig = { fabricType: 'woven', stiffness: 0.5, elasticity: 0.3, drapeCoefficient: 0.7, windResistance: 0.2, gravityScale: 1.0, collisionMargin: 0.01, vertexCount: 1000 };
24
+ const defaultConfig: FabricSimulationConfig = {
25
+ fabricType: 'woven',
26
+ stiffness: 0.5,
27
+ elasticity: 0.3,
28
+ drapeCoefficient: 0.7,
29
+ windResistance: 0.2,
30
+ gravityScale: 1.0,
31
+ collisionMargin: 0.01,
32
+ vertexCount: 1000,
33
+ };
8
34
 
9
35
  export function createFabricSimulationHandler(): TraitHandler<FabricSimulationConfig> {
10
- return { name: 'fabric_simulation', defaultConfig,
11
- onAttach(n: HSPlusNode, c: FabricSimulationConfig, ctx: TraitContext) { n.__fabricState = { isSimulating: false, frameCount: 0, settledPercent: 0 }; ctx.emit?.('fabric:initialized', { type: c.fabricType, vertices: c.vertexCount }); },
12
- onDetach(n: HSPlusNode, _c: FabricSimulationConfig, ctx: TraitContext) { delete n.__fabricState; ctx.emit?.('fabric:destroyed'); },
36
+ return {
37
+ name: 'fabric_simulation',
38
+ defaultConfig,
39
+ onAttach(n: HSPlusNode, c: FabricSimulationConfig, ctx: TraitContext) {
40
+ n.__fabricState = { isSimulating: false, frameCount: 0, settledPercent: 0 };
41
+ ctx.emit?.('fabric:initialized', { type: c.fabricType, vertices: c.vertexCount });
42
+ },
43
+ onDetach(n: HSPlusNode, _c: FabricSimulationConfig, ctx: TraitContext) {
44
+ delete n.__fabricState;
45
+ ctx.emit?.('fabric:destroyed');
46
+ },
13
47
  onUpdate(n: HSPlusNode, c: FabricSimulationConfig, ctx: TraitContext, _d: number) {
14
- const s = n.__fabricState as Record<string, unknown> | undefined; if (!s || !s.isSimulating) return;
48
+ const s = n.__fabricState as Record<string, unknown> | undefined;
49
+ if (!s || !s.isSimulating) return;
15
50
  (s.frameCount as number)++;
16
51
  s.settledPercent = Math.min(100, (s.frameCount as number) * c.drapeCoefficient);
17
- if ((s.settledPercent as number) >= 100) { s.isSimulating = false; ctx.emit?.('fabric:settled'); }
52
+ if ((s.settledPercent as number) >= 100) {
53
+ s.isSimulating = false;
54
+ ctx.emit?.('fabric:settled');
55
+ }
18
56
  },
19
57
  onEvent(n: HSPlusNode, _c: FabricSimulationConfig, ctx: TraitContext, e: TraitEvent) {
20
- const s = n.__fabricState as Record<string, unknown> | undefined; if (!s) return;
21
- if (e.type === 'fabric:start') { s.isSimulating = true; s.frameCount = 0; s.settledPercent = 0; ctx.emit?.('fabric:simulating'); }
22
- if (e.type === 'fabric:apply_wind') { ctx.emit?.('fabric:wind_applied', { force: e.payload?.force }); }
58
+ const s = n.__fabricState as Record<string, unknown> | undefined;
59
+ if (!s) return;
60
+ if (e.type === 'fabric:start') {
61
+ s.isSimulating = true;
62
+ s.frameCount = 0;
63
+ s.settledPercent = 0;
64
+ ctx.emit?.('fabric:simulating');
65
+ }
66
+ if (e.type === 'fabric:apply_wind') {
67
+ ctx.emit?.('fabric:wind_applied', { force: e.payload?.force });
68
+ }
23
69
  },
24
70
  };
25
71
  }
@@ -1,18 +1,56 @@
1
1
  /** @garment Trait — Clothing item definition. @trait garment */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export type GarmentCategory = 'top' | 'bottom' | 'dress' | 'outerwear' | 'footwear' | 'accessory' | 'swimwear' | 'activewear';
5
- export interface GarmentConfig { name: string; category: GarmentCategory; sizes: string[]; colors: string[]; fabric: string; weight_gsm: number; season: 'spring' | 'summer' | 'fall' | 'winter' | 'all'; price: number; sku: string; }
4
+ export type GarmentCategory =
5
+ | 'top'
6
+ | 'bottom'
7
+ | 'dress'
8
+ | 'outerwear'
9
+ | 'footwear'
10
+ | 'accessory'
11
+ | 'swimwear'
12
+ | 'activewear';
13
+ export interface GarmentConfig {
14
+ name: string;
15
+ category: GarmentCategory;
16
+ sizes: string[];
17
+ colors: string[];
18
+ fabric: string;
19
+ weight_gsm: number;
20
+ season: 'spring' | 'summer' | 'fall' | 'winter' | 'all';
21
+ price: number;
22
+ sku: string;
23
+ }
6
24
 
7
- const defaultConfig: GarmentConfig = { name: '', category: 'top', sizes: ['S','M','L'], colors: ['black'], fabric: 'cotton', weight_gsm: 200, season: 'all', price: 0, sku: '' };
25
+ const defaultConfig: GarmentConfig = {
26
+ name: '',
27
+ category: 'top',
28
+ sizes: ['S', 'M', 'L'],
29
+ colors: ['black'],
30
+ fabric: 'cotton',
31
+ weight_gsm: 200,
32
+ season: 'all',
33
+ price: 0,
34
+ sku: '',
35
+ };
8
36
 
9
37
  export function createGarmentHandler(): TraitHandler<GarmentConfig> {
10
- return { name: 'garment', defaultConfig,
11
- onAttach(n: HSPlusNode, c: GarmentConfig, ctx: TraitContext) { n.__garmentState = { variants: c.sizes.length * c.colors.length, inStock: true }; ctx.emit?.('garment:created', { name: c.name, category: c.category }); },
12
- onDetach(n: HSPlusNode, _c: GarmentConfig, ctx: TraitContext) { delete n.__garmentState; ctx.emit?.('garment:removed'); },
38
+ return {
39
+ name: 'garment',
40
+ defaultConfig,
41
+ onAttach(n: HSPlusNode, c: GarmentConfig, ctx: TraitContext) {
42
+ n.__garmentState = { variants: c.sizes.length * c.colors.length, inStock: true };
43
+ ctx.emit?.('garment:created', { name: c.name, category: c.category });
44
+ },
45
+ onDetach(n: HSPlusNode, _c: GarmentConfig, ctx: TraitContext) {
46
+ delete n.__garmentState;
47
+ ctx.emit?.('garment:removed');
48
+ },
13
49
  onUpdate() {},
14
50
  onEvent(_n: HSPlusNode, c: GarmentConfig, ctx: TraitContext, e: TraitEvent) {
15
- if (e.type === 'garment:check_fit') { ctx.emit?.('garment:fit_result', { garment: c.name, sizes: c.sizes }); }
51
+ if (e.type === 'garment:check_fit') {
52
+ ctx.emit?.('garment:fit_result', { garment: c.name, sizes: c.sizes });
53
+ }
16
54
  },
17
55
  };
18
56
  }
@@ -1,28 +1,72 @@
1
1
  /** @runway_choreography Trait — Fashion show runway planning. @trait runway_choreography */
2
2
  import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
3
3
 
4
- export interface RunwaySegment { modelId: string; garmentIds: string[]; walkDurationS: number; pauseAtEndS: number; music: string; lightingCue: string; }
5
- export interface RunwayChoreographyConfig { segments: RunwaySegment[]; runwayLengthM: number; totalDurationS: number; musicPlaylist: string[]; }
6
- export interface RunwayChoreographyState { currentSegment: number; isRunning: boolean; elapsedS: number; }
4
+ export interface RunwaySegment {
5
+ modelId: string;
6
+ garmentIds: string[];
7
+ walkDurationS: number;
8
+ pauseAtEndS: number;
9
+ music: string;
10
+ lightingCue: string;
11
+ }
12
+ export interface RunwayChoreographyConfig {
13
+ segments: RunwaySegment[];
14
+ runwayLengthM: number;
15
+ totalDurationS: number;
16
+ musicPlaylist: string[];
17
+ }
18
+ export interface RunwayChoreographyState {
19
+ currentSegment: number;
20
+ isRunning: boolean;
21
+ elapsedS: number;
22
+ }
7
23
 
8
- const defaultConfig: RunwayChoreographyConfig = { segments: [], runwayLengthM: 20, totalDurationS: 600, musicPlaylist: [] };
24
+ const defaultConfig: RunwayChoreographyConfig = {
25
+ segments: [],
26
+ runwayLengthM: 20,
27
+ totalDurationS: 600,
28
+ musicPlaylist: [],
29
+ };
9
30
 
10
31
  export function createRunwayChoreographyHandler(): TraitHandler<RunwayChoreographyConfig> {
11
- return { name: 'runway_choreography', defaultConfig,
12
- onAttach(n: HSPlusNode, c: RunwayChoreographyConfig, ctx: TraitContext) { n.__runwayState = { currentSegment: 0, isRunning: false, elapsedS: 0 }; ctx.emit?.('runway:ready', { segments: c.segments.length }); },
13
- onDetach(n: HSPlusNode, _c: RunwayChoreographyConfig, ctx: TraitContext) { delete n.__runwayState; ctx.emit?.('runway:ended'); },
32
+ return {
33
+ name: 'runway_choreography',
34
+ defaultConfig,
35
+ onAttach(n: HSPlusNode, c: RunwayChoreographyConfig, ctx: TraitContext) {
36
+ n.__runwayState = { currentSegment: 0, isRunning: false, elapsedS: 0 };
37
+ ctx.emit?.('runway:ready', { segments: c.segments.length });
38
+ },
39
+ onDetach(n: HSPlusNode, _c: RunwayChoreographyConfig, ctx: TraitContext) {
40
+ delete n.__runwayState;
41
+ ctx.emit?.('runway:ended');
42
+ },
14
43
  onUpdate(n: HSPlusNode, c: RunwayChoreographyConfig, ctx: TraitContext, delta: number) {
15
- const s = n.__runwayState as RunwayChoreographyState | undefined; if (!s?.isRunning) return;
44
+ const s = n.__runwayState as RunwayChoreographyState | undefined;
45
+ if (!s?.isRunning) return;
16
46
  s.elapsedS += delta / 1000;
17
47
  const seg = c.segments[s.currentSegment];
18
- if (seg && s.elapsedS >= seg.walkDurationS + seg.pauseAtEndS) { s.currentSegment++; s.elapsedS = 0;
19
- if (s.currentSegment >= c.segments.length) { s.isRunning = false; ctx.emit?.('runway:show_complete'); }
20
- else ctx.emit?.('runway:next_model', { segment: s.currentSegment, model: c.segments[s.currentSegment]?.modelId });
48
+ if (seg && s.elapsedS >= seg.walkDurationS + seg.pauseAtEndS) {
49
+ s.currentSegment++;
50
+ s.elapsedS = 0;
51
+ if (s.currentSegment >= c.segments.length) {
52
+ s.isRunning = false;
53
+ ctx.emit?.('runway:show_complete');
54
+ } else
55
+ ctx.emit?.('runway:next_model', {
56
+ segment: s.currentSegment,
57
+ model: c.segments[s.currentSegment]?.modelId,
58
+ });
21
59
  }
22
60
  },
23
61
  onEvent(n: HSPlusNode, _c: RunwayChoreographyConfig, ctx: TraitContext, e: TraitEvent) {
24
- const s = n.__runwayState as RunwayChoreographyState | undefined; if (!s) return;
25
- if (e.type === 'runway:start') { s.isRunning = true; s.currentSegment = 0; s.elapsedS = 0; ctx.emit?.('runway:started'); }
62
+ const s = n.__runwayState as RunwayChoreographyState | undefined;
63
+ if (!s) return;
64
+ if (e.type === 'runway:start') {
65
+ s.isRunning = true;
66
+ s.currentSegment = 0;
67
+ s.elapsedS = 0;
68
+ ctx.emit?.('runway:started');
69
+ }
26
70
  },
27
71
  };
28
72
  }
@@ -1,4 +1,22 @@
1
- export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
2
- export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
3
- export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
4
- export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
1
+ export interface HSPlusNode {
2
+ id?: string;
3
+ properties?: Record<string, unknown>;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface TraitContext {
7
+ emit?: (event: string, payload?: unknown) => void;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface TraitEvent {
11
+ type: string;
12
+ payload?: Record<string, unknown>;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface TraitHandler<T = unknown> {
16
+ name: string;
17
+ defaultConfig: T;
18
+ onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
19
+ onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
20
+ onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
21
+ onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
22
+ }
package/tsconfig.json CHANGED
@@ -1 +1,5 @@
1
- { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
4
+ "include": ["src"]
5
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-2026 HoloScript Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.