@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 +3 -3
- package/src/__tests__/fashionsolver.test.ts +23 -15
- package/src/fashionsolver.ts +38 -14
- package/src/index.ts +25 -5
- package/src/traits/FabricSimulationTrait.ts +57 -11
- package/src/traits/GarmentTrait.ts +45 -7
- package/src/traits/RunwayChoreographyTrait.ts +57 -13
- package/src/traits/types.ts +22 -4
- package/tsconfig.json +5 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-fashion",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@holoscript/core": "8.0.
|
|
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',
|
|
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.
|
|
79
|
-
expect(r.nestingEfficiency).toBeCloseTo(0.
|
|
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.
|
|
89
|
-
expect(r.fabricAreaCm2).toBeCloseTo(r.totalPieceAreaCm2 / 0.
|
|
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 = [
|
|
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 = [
|
|
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,
|
|
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,
|
|
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 });
|
package/src/fashionsolver.ts
CHANGED
|
@@ -57,7 +57,12 @@ export interface FabricWasteResult {
|
|
|
57
57
|
nestingEfficiency: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export type ColorHarmony =
|
|
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 {
|
|
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)
|
|
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(
|
|
170
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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({
|
|
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: {
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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 = {
|
|
12
|
-
|
|
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 =
|
|
5
|
-
|
|
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 = {
|
|
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 {
|
|
11
|
-
|
|
12
|
-
|
|
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;
|
|
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) {
|
|
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;
|
|
21
|
-
if (
|
|
22
|
-
if (e.type === 'fabric:
|
|
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 =
|
|
5
|
-
|
|
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 = {
|
|
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 {
|
|
11
|
-
|
|
12
|
-
|
|
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') {
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
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 = {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
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) {
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
25
|
-
if (
|
|
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
|
}
|
package/src/traits/types.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export interface HSPlusNode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
{
|
|
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.
|