@things-factory/figure-service 10.1.245 → 10.1.247
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/README.md +19 -0
- package/dist-server/routers/figure-thumbnail-router.js +16 -1
- package/dist-server/routers/figure-thumbnail-router.js.map +1 -1
- package/dist-server/service/figure/figure-ai-scenarios-run.d.ts +44 -0
- package/dist-server/service/figure/figure-ai-scenarios-run.js +94 -0
- package/dist-server/service/figure/figure-ai-scenarios-run.js.map +1 -0
- package/dist-server/service/figure/figure-ai-scenarios.d.ts +68 -0
- package/dist-server/service/figure/figure-ai-scenarios.js +814 -0
- package/dist-server/service/figure/figure-ai-scenarios.js.map +1 -0
- package/dist-server/service/figure/figure-context.js +3 -0
- package/dist-server/service/figure/figure-context.js.map +1 -1
- package/dist-server/service/figure/figure-llm-provider.d.ts +10 -0
- package/dist-server/service/figure/figure-llm-provider.js +19 -0
- package/dist-server/service/figure/figure-llm-provider.js.map +1 -0
- package/dist-server/service/figure/figure-mutation.js +19 -23
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose-steps.d.ts +5 -1
- package/dist-server/service/figure/figure-propose-steps.js +14 -2
- package/dist-server/service/figure/figure-propose-steps.js.map +1 -1
- package/dist-server/service/figure/figure-query.d.ts +8 -1
- package/dist-server/service/figure/figure-query.js +36 -10
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/scripts/figure-ai-scenarios-agent.cjs +98 -0
- package/server/routers/figure-thumbnail-router.ts +18 -2
- package/server/service/figure/figure-ai-scenarios-run.ts +127 -0
- package/server/service/figure/figure-ai-scenarios.live.test.ts +59 -0
- package/server/service/figure/figure-ai-scenarios.test.ts +77 -0
- package/server/service/figure/figure-ai-scenarios.ts +792 -0
- package/server/service/figure/figure-context.ts +3 -0
- package/server/service/figure/figure-llm-provider.ts +20 -0
- package/server/service/figure/figure-llm-smoke.test.ts +5 -68
- package/server/service/figure/figure-mutation.ts +17 -23
- package/server/service/figure/figure-propose-steps.test.ts +55 -0
- package/server/service/figure/figure-propose-steps.ts +14 -3
- package/server/service/figure/figure-public-db.test.ts +108 -0
- package/server/service/figure/figure-query.ts +35 -9
- package/server/service/figure/figure-thumbnail-db.test.ts +16 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FIGURE_AI_SCENARIOS = void 0;
|
|
4
|
+
exports.scenarioPartsOf = scenarioPartsOf;
|
|
5
|
+
exports.mixerDraft = mixerDraft;
|
|
6
|
+
exports.portRevisionMisses = portRevisionMisses;
|
|
7
|
+
/**
|
|
8
|
+
* AI 모델링 시나리오 — what a person asks the figure AI for, and what the figure must then be (2026-11-03, 사용자 「여러가지
|
|
9
|
+
* 모델링 시나리오를 구성해서 AI를 통한 모델링이 잘 구현되는지 지속적으로 점검할 수 있도록 하라」).
|
|
10
|
+
*
|
|
11
|
+
* Each scenario is a request in a person's words, the draft it is asked on (none for a new figure), and a check that
|
|
12
|
+
* says what the figure that came back misses -- read from the figure by the kernel (its parts' shapes, fastenings,
|
|
13
|
+
* motion, clips, data), never from the steps the model wrote, so any way of making it that the kernel takes passes.
|
|
14
|
+
* Each also holds a reference answer the check must pass and a wrong one it must fail: the checks are themselves held to
|
|
15
|
+
* known verdicts without a provider (figure-ai-scenarios.test.ts). The live run (figure-ai-scenarios.live.test.ts) asks a
|
|
16
|
+
* real model for each and writes a report to compare runs by.
|
|
17
|
+
*
|
|
18
|
+
* The scenarios are spread over what the kernel can say (ADR-0100 shapes and §16's taper and pipe, ADR-0093 fastenings,
|
|
19
|
+
* ADR-0159 joints, repeats, clips, the figure's size, data bindings, mirrored chains), new figures and revisions both.
|
|
20
|
+
*/
|
|
21
|
+
const figure_model_1 = require("@hatiolab/figure-model");
|
|
22
|
+
/** Every part of a figure, as the kernel evaluates it. */
|
|
23
|
+
function scenarioPartsOf(asset) {
|
|
24
|
+
const geometry = (0, figure_model_1.compileFigureAsset)(asset).evaluate().geometry;
|
|
25
|
+
const boxes = (0, figure_model_1.figureOccupiedBoxesOf)(geometry, (0, figure_model_1.figureWritersOf)(asset));
|
|
26
|
+
const nodes = asset.document.model.nodes;
|
|
27
|
+
const joints = (asset.joints ?? []);
|
|
28
|
+
return asset.appearance.map((look) => {
|
|
29
|
+
const id = look.target;
|
|
30
|
+
const node = nodes.find(n => n.id === `${id}.shape`);
|
|
31
|
+
const joint = joints.find(j => j.body1?.part === id);
|
|
32
|
+
const box = boxes.find(b => b.part === id);
|
|
33
|
+
const body = geometry.find(g => g.id === id);
|
|
34
|
+
const motion = (joint?.motion ?? []);
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
kind: (0, figure_model_1.figureShapeDescriptionOf)(node, asset.document.model)?.kind ?? String(node?.op ?? ''),
|
|
38
|
+
centre: { x: box?.centre.x ?? 0, y: box?.centre.y ?? 0, z: box?.centre.z ?? 0 },
|
|
39
|
+
extent: { x: box?.extent.x ?? 0, y: box?.extent.y ?? 0, z: box?.extent.z ?? 0 },
|
|
40
|
+
colour: String(asset.palette?.[look.material?.token] ?? look.material?.token ?? '').toLowerCase(),
|
|
41
|
+
...(joint ? { on: joint.body0?.part ?? 'mounting-plane' } : {}),
|
|
42
|
+
motion: motion.map(m => m.type),
|
|
43
|
+
controls: motion.map((m, i) => m.state?.id ?? m.id ?? (i ? `${joint.id}.${i}` : joint.id)),
|
|
44
|
+
limits: motion.map(m => ({ lower: m.lowerLimit, upper: m.upperLimit })),
|
|
45
|
+
/* The kernel's own reading of which part a body was placed by: a row's copies are named `["<part>.repeat", i, …]`. */
|
|
46
|
+
copies: boxes.filter(b => b.part === id).length || 1,
|
|
47
|
+
shape: body?.shape,
|
|
48
|
+
node
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const near = (a, b, tol = 0.5) => Math.abs(a - b) <= tol;
|
|
53
|
+
const bottom = (p) => p.centre.y - p.extent.y / 2;
|
|
54
|
+
const top = (p) => p.centre.y + p.extent.y / 2;
|
|
55
|
+
/** Every part fastened to something: the plane, or another part (each part fastened once, as the grammar teaches). */
|
|
56
|
+
function loose(parts) {
|
|
57
|
+
return parts.filter(p => !p.on).map(p => `${p.id} is fastened to nothing`);
|
|
58
|
+
}
|
|
59
|
+
/** A clip that plays one of `controls`. */
|
|
60
|
+
const clipsPlaying = (asset, controls) => (asset.drivers ?? []).filter((d) => controls.includes(d.state));
|
|
61
|
+
const steps = (list) => list;
|
|
62
|
+
/* ── The drafts revisions are asked on ────────────────────────────────────────────────── */
|
|
63
|
+
/** A mixer: a body standing on the floor, a port fastened to its front. */
|
|
64
|
+
function mixerDraft() {
|
|
65
|
+
return (0, figure_model_1.applyFigureProposal)((0, figure_model_1.createFigureAsset)('Mixer'), [
|
|
66
|
+
{ kind: 'add-part', part: { id: 'body', kind: 'cylinder', dimensions: { radius: 140, length: 320 } } },
|
|
67
|
+
{ kind: 'add-part', part: { id: 'port', kind: 'box', dimensions: { width: 80, height: 60, depth: 30 } } },
|
|
68
|
+
{ kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } },
|
|
69
|
+
/* The body's front is its curved side: a point on it, by the outward normal there (ADR-0140 3.9; push Y, +Z is 270°). */
|
|
70
|
+
{ kind: 'attach', part: 'port', to: { part: 'body', surface: { around: 270, along: 'centre' } } },
|
|
71
|
+
{ kind: 'set-placement', placement: 'floor' }
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What "make the port twice as wide and give it an accent colour" asks of the mixer, said as what is missing: the port
|
|
76
|
+
* twice as wide (X), its height and depth kept, a colour it did not have, and the body left as it was.
|
|
77
|
+
*/
|
|
78
|
+
function portRevisionMisses(before, after) {
|
|
79
|
+
const was = Object.fromEntries(scenarioPartsOf(before).map(p => [p.id, p]));
|
|
80
|
+
const now = Object.fromEntries(scenarioPartsOf(after).map(p => [p.id, p]));
|
|
81
|
+
const misses = [];
|
|
82
|
+
if (!now.port)
|
|
83
|
+
return ['the port is gone'];
|
|
84
|
+
const size = (p) => (0, figure_model_1.figureShapeBoundsOf)(p.shape);
|
|
85
|
+
const extentOf = (p, axis) => size(p)[axis].max - size(p)[axis].min;
|
|
86
|
+
if (!near(extentOf(now.port, 'x'), extentOf(was.port, 'x') * 2))
|
|
87
|
+
misses.push(`port width ${extentOf(now.port, 'x')}, asked ${extentOf(was.port, 'x') * 2}`);
|
|
88
|
+
for (const axis of ['y', 'z'])
|
|
89
|
+
if (!near(extentOf(now.port, axis), extentOf(was.port, axis)))
|
|
90
|
+
misses.push(`port ${axis} changed to ${extentOf(now.port, axis)}`);
|
|
91
|
+
if (now.port.colour === was.port.colour)
|
|
92
|
+
misses.push(`port colour still ${was.port.colour}`);
|
|
93
|
+
if (!now.body)
|
|
94
|
+
misses.push('the body is gone');
|
|
95
|
+
else
|
|
96
|
+
for (const axis of ['x', 'y', 'z'])
|
|
97
|
+
if (!near(extentOf(now.body, axis), extentOf(was.body, axis)))
|
|
98
|
+
misses.push(`body ${axis} changed to ${extentOf(now.body, axis)}`);
|
|
99
|
+
if (now.body && now.body.colour !== was.body.colour)
|
|
100
|
+
misses.push(`body colour changed to ${now.body.colour}`);
|
|
101
|
+
return misses;
|
|
102
|
+
}
|
|
103
|
+
/** An elbow: up from the floor, a quarter bend toward +X, along. */
|
|
104
|
+
function elbowDraft() {
|
|
105
|
+
return (0, figure_model_1.applyFigureProposal)((0, figure_model_1.createFigureAsset)('Elbow'), [
|
|
106
|
+
{ kind: 'add-part', part: { id: 'pipe', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 400 }] } },
|
|
107
|
+
{ kind: 'attach', part: 'pipe', to: { plane: 'mounting-plane' } }
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
/** A plain box standing on the floor. */
|
|
111
|
+
function boxDraft() {
|
|
112
|
+
return (0, figure_model_1.applyFigureProposal)((0, figure_model_1.createFigureAsset)('Bin'), [
|
|
113
|
+
{ kind: 'add-part', part: { id: 'bin', kind: 'box', dimensions: { width: 600, height: 300, depth: 400 } } },
|
|
114
|
+
{ kind: 'attach', part: 'bin', to: { plane: 'mounting-plane' } }
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
/** A table: a top on four legs. */
|
|
118
|
+
function tableDraft() {
|
|
119
|
+
return (0, figure_model_1.applyFigureProposal)((0, figure_model_1.createFigureAsset)('Table'), [
|
|
120
|
+
{ kind: 'add-part', part: { id: 'top', kind: 'box', dimensions: { width: 1200, height: 40, depth: 700 } } },
|
|
121
|
+
{ kind: 'add-part', part: { id: 'leg', kind: 'box', dimensions: { width: 50, height: 700, depth: 50 } } },
|
|
122
|
+
{ kind: 'attach', part: 'leg', to: { plane: 'mounting-plane' } },
|
|
123
|
+
{ kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } }
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
/** A cabinet with a lamp on its top. */
|
|
127
|
+
function lampDraft() {
|
|
128
|
+
return (0, figure_model_1.applyFigureProposal)((0, figure_model_1.createFigureAsset)('Cabinet'), [
|
|
129
|
+
{ kind: 'add-part', part: { id: 'cabinet', kind: 'box', dimensions: { width: 600, height: 1200, depth: 500 } } },
|
|
130
|
+
{ kind: 'add-part', part: { id: 'lamp', kind: 'cylinder', dimensions: { radius: 40, length: 80 } } },
|
|
131
|
+
{ kind: 'attach', part: 'cabinet', to: { plane: 'mounting-plane' } },
|
|
132
|
+
{ kind: 'attach', part: 'lamp', to: { part: 'cabinet', face: 'top' } }
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
/* ── The scenarios ────────────────────────────────────────────────────────────────────── */
|
|
136
|
+
const accent = (target, colour, token = 'palette.accent') => ({ kind: 'set-material', target, material: { token }, palette: { [token]: colour } });
|
|
137
|
+
exports.FIGURE_AI_SCENARIOS = [
|
|
138
|
+
{
|
|
139
|
+
id: 'roll-press',
|
|
140
|
+
title: '새 피규어: 프레임 안 두 롤러, 서로 다른 색',
|
|
141
|
+
covers: ['add-part cylinder', 'attach', 'set-material'],
|
|
142
|
+
prompt: 'two cylindrical rollers inside a simple rectangular press frame; make the rollers visually distinct',
|
|
143
|
+
name: 'Roll press',
|
|
144
|
+
/*
|
|
145
|
+
Two rollers -- two parts, or one repeated in a row -- that stand out: a colour the frame does not have. Read so after
|
|
146
|
+
a model's row of two was failed for being one part (2026-11-03, the first runs).
|
|
147
|
+
*/
|
|
148
|
+
check: after => {
|
|
149
|
+
const parts = scenarioPartsOf(after);
|
|
150
|
+
const rollers = parts.filter(p => p.kind === 'ellipse');
|
|
151
|
+
const frame = parts.filter(p => p.kind !== 'ellipse');
|
|
152
|
+
const misses = [...loose(parts)];
|
|
153
|
+
const drawn = rollers.reduce((sum, r) => sum + r.copies, 0);
|
|
154
|
+
if (drawn < 2)
|
|
155
|
+
misses.push(`${drawn} round bodies drawn, two rollers asked`);
|
|
156
|
+
if (!frame.length)
|
|
157
|
+
misses.push('no frame');
|
|
158
|
+
else if (rollers.length && rollers.every(r => frame.some(f => f.colour === r.colour)))
|
|
159
|
+
misses.push(`the rollers are the frame's colour, ${rollers[0].colour}`);
|
|
160
|
+
return misses;
|
|
161
|
+
},
|
|
162
|
+
reference: steps([
|
|
163
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'box', dimensions: { width: 1000, height: 80, depth: 500 } } },
|
|
164
|
+
{ kind: 'add-part', part: { id: 'roller-a', kind: 'cylinder', dimensions: { radius: 80, length: 400 } } },
|
|
165
|
+
{ kind: 'add-part', part: { id: 'roller-b', kind: 'cylinder', dimensions: { radius: 80, length: 400 } } },
|
|
166
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
167
|
+
{ kind: 'attach', part: 'roller-a', to: { part: 'base', face: 'top' }, align: { x: { mm: -200 } } },
|
|
168
|
+
{ kind: 'attach', part: 'roller-b', to: { part: 'base', face: 'top' }, align: { x: { mm: 200 } } },
|
|
169
|
+
accent('roller-b', '#d08a2c')
|
|
170
|
+
]),
|
|
171
|
+
wrong: steps([
|
|
172
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'box', dimensions: { width: 1000, height: 80, depth: 500 } } },
|
|
173
|
+
{ kind: 'add-part', part: { id: 'roller-a', kind: 'cylinder', dimensions: { radius: 80, length: 400 } } },
|
|
174
|
+
{ kind: 'add-part', part: { id: 'roller-b', kind: 'cylinder', dimensions: { radius: 80, length: 400 } } },
|
|
175
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
176
|
+
{ kind: 'attach', part: 'roller-a', to: { part: 'base', face: 'top' }, align: { x: { mm: -200 } } },
|
|
177
|
+
{ kind: 'attach', part: 'roller-b', to: { part: 'base', face: 'top' }, align: { x: { mm: 200 } } },
|
|
178
|
+
/* Rollers drawn in the frame's own colour: nothing stands out. */
|
|
179
|
+
accent('roller-a', '#477b9e', 'palette.primary'),
|
|
180
|
+
accent('roller-b', '#477b9e', 'palette.primary'),
|
|
181
|
+
accent('base', '#477b9e', 'palette.primary')
|
|
182
|
+
])
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'lift-table',
|
|
186
|
+
title: '새 피규어: 받침 · 기둥 · 상판을 쌓기',
|
|
187
|
+
covers: ['attach part face', 'stacking'],
|
|
188
|
+
prompt: '리프트 테이블: 바닥에 넓은 받침, 그 위에 원기둥 기둥, 기둥 위에 받침만 한 상판',
|
|
189
|
+
name: 'Lift table',
|
|
190
|
+
check: after => {
|
|
191
|
+
const parts = scenarioPartsOf(after);
|
|
192
|
+
const misses = [...loose(parts)];
|
|
193
|
+
if (parts.length < 3)
|
|
194
|
+
return [...misses, `${parts.length} parts, three asked`];
|
|
195
|
+
const sorted = [...parts].sort((a, b) => top(b) - top(a));
|
|
196
|
+
const [highest] = sorted;
|
|
197
|
+
const column = parts.find(p => p.kind === 'ellipse');
|
|
198
|
+
if (!column)
|
|
199
|
+
misses.push('no round column');
|
|
200
|
+
else {
|
|
201
|
+
if (!(highest.extent.x * highest.extent.z > column.extent.x * column.extent.z))
|
|
202
|
+
misses.push('the top is not wider than the column');
|
|
203
|
+
if (!parts.some(p => p !== column && near(bottom(column), top(p), 1)))
|
|
204
|
+
misses.push('the column does not stand on another part');
|
|
205
|
+
if (!near(bottom(highest), top(column), 1))
|
|
206
|
+
misses.push('the top does not sit on the column');
|
|
207
|
+
}
|
|
208
|
+
if (!parts.some(p => near(bottom(p), 0, 1)))
|
|
209
|
+
misses.push('nothing stands on the floor');
|
|
210
|
+
return misses;
|
|
211
|
+
},
|
|
212
|
+
reference: steps([
|
|
213
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'box', dimensions: { width: 1200, height: 80, depth: 800 } } },
|
|
214
|
+
{ kind: 'add-part', part: { id: 'column', kind: 'cylinder', dimensions: { radius: 90, length: 600 } } },
|
|
215
|
+
{ kind: 'add-part', part: { id: 'top', kind: 'box', dimensions: { width: 1200, height: 60, depth: 800 } } },
|
|
216
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
217
|
+
{ kind: 'attach', part: 'column', to: { part: 'base', face: 'top' } },
|
|
218
|
+
{ kind: 'attach', part: 'top', to: { part: 'column', face: 'top' } }
|
|
219
|
+
]),
|
|
220
|
+
wrong: steps([
|
|
221
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'box', dimensions: { width: 1200, height: 80, depth: 800 } } },
|
|
222
|
+
{ kind: 'add-part', part: { id: 'column', kind: 'cylinder', dimensions: { radius: 90, length: 600 } } },
|
|
223
|
+
{ kind: 'add-part', part: { id: 'top', kind: 'box', dimensions: { width: 1200, height: 60, depth: 800 } } },
|
|
224
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
225
|
+
{ kind: 'attach', part: 'column', to: { part: 'base', face: 'top' } },
|
|
226
|
+
{ kind: 'attach', part: 'top', to: { part: 'base', face: 'top' } }
|
|
227
|
+
])
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'elbow-flange',
|
|
231
|
+
title: '새 피규어: 엘보 배관과 끝의 플랜지 (ADR-0100 §16.2)',
|
|
232
|
+
covers: ['add-part pipe', 'bend', 'attach to a pipe end'],
|
|
233
|
+
prompt: '바닥에서 위로 올라가다 90도 꺾여 옆으로 가는 지름 80 배관, 배관 끝에 원판 플랜지',
|
|
234
|
+
name: 'Elbow',
|
|
235
|
+
check: after => {
|
|
236
|
+
const parts = scenarioPartsOf(after);
|
|
237
|
+
const misses = [...loose(parts)];
|
|
238
|
+
const pipe = parts.find(p => p.kind === 'sweep');
|
|
239
|
+
if (!pipe)
|
|
240
|
+
return [...misses, 'no part along a path (a pipe)'];
|
|
241
|
+
if (!pipe.shape?.sweep?.path?.some((s) => s.kind === 'bend'))
|
|
242
|
+
misses.push('the pipe has no bend');
|
|
243
|
+
if (!parts.some(p => p.on === pipe.id))
|
|
244
|
+
misses.push('nothing is fastened to the pipe');
|
|
245
|
+
return misses;
|
|
246
|
+
},
|
|
247
|
+
reference: steps([
|
|
248
|
+
{ kind: 'add-part', part: { id: 'pipe', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 400 }] } },
|
|
249
|
+
{ kind: 'attach', part: 'pipe', to: { plane: 'mounting-plane' } },
|
|
250
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 70, length: 20 } } },
|
|
251
|
+
{ kind: 'attach', part: 'flange', to: { part: 'pipe', face: 'right' } }
|
|
252
|
+
]),
|
|
253
|
+
wrong: steps([
|
|
254
|
+
{ kind: 'add-part', part: { id: 'pipe', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 400 }] } },
|
|
255
|
+
{ kind: 'attach', part: 'pipe', to: { plane: 'mounting-plane' } },
|
|
256
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 70, length: 20 } } },
|
|
257
|
+
{ kind: 'attach', part: 'flange', to: { plane: 'mounting-plane' } }
|
|
258
|
+
])
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'conveyor',
|
|
262
|
+
title: '새 피규어: 프레임 위 롤러를 반복으로',
|
|
263
|
+
covers: ['make-repeat'],
|
|
264
|
+
prompt: 'a roller conveyor: a 2 m long frame with rollers repeated along its length every 150 mm',
|
|
265
|
+
name: 'Conveyor',
|
|
266
|
+
check: after => {
|
|
267
|
+
const parts = scenarioPartsOf(after);
|
|
268
|
+
const misses = [...loose(parts)];
|
|
269
|
+
const rows = (after.joints ?? []).filter((j) => j.repeat);
|
|
270
|
+
if (!rows.length)
|
|
271
|
+
misses.push('no part repeated in a row');
|
|
272
|
+
const geometry = (0, figure_model_1.compileFigureAsset)(after).evaluate().geometry;
|
|
273
|
+
if (geometry.length < 8)
|
|
274
|
+
misses.push(`${geometry.length} bodies drawn, a row of rollers asked`);
|
|
275
|
+
const longest = Math.max(...parts.map(p => Math.max(p.extent.x, p.extent.z)));
|
|
276
|
+
if (longest < 1500)
|
|
277
|
+
misses.push(`the longest part is ${Math.round(longest)} mm, a 2 m frame asked`);
|
|
278
|
+
return misses;
|
|
279
|
+
},
|
|
280
|
+
reference: steps([
|
|
281
|
+
{ kind: 'add-part', part: { id: 'frame', kind: 'box', dimensions: { width: 2000, height: 100, depth: 500 } } },
|
|
282
|
+
{ kind: 'add-part', part: { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 480 } } },
|
|
283
|
+
{ kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } },
|
|
284
|
+
{ kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } },
|
|
285
|
+
{ kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 150 }
|
|
286
|
+
]),
|
|
287
|
+
wrong: steps([
|
|
288
|
+
{ kind: 'add-part', part: { id: 'frame', kind: 'box', dimensions: { width: 2000, height: 100, depth: 500 } } },
|
|
289
|
+
{ kind: 'add-part', part: { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 480 } } },
|
|
290
|
+
{ kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } },
|
|
291
|
+
{ kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } }
|
|
292
|
+
])
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'two-joint-arm',
|
|
296
|
+
title: '새 피규어: 두 관절로 도는 팔',
|
|
297
|
+
covers: ['set-joint revolute', 'a joint on a moving part'],
|
|
298
|
+
prompt: '바닥 받침 위에 아래팔과 위팔이 있고, 받침-아래팔, 아래팔-위팔이 각각 회전 관절인 로봇 팔',
|
|
299
|
+
name: 'Arm',
|
|
300
|
+
check: after => {
|
|
301
|
+
const parts = scenarioPartsOf(after);
|
|
302
|
+
const misses = [...loose(parts)];
|
|
303
|
+
const turning = parts.filter(p => p.motion.includes('revolute'));
|
|
304
|
+
if (turning.length < 2)
|
|
305
|
+
misses.push(`${turning.length} turning joints, two asked`);
|
|
306
|
+
else if (!turning.some(p => turning.some(q => q.id === p.on)))
|
|
307
|
+
misses.push('no turning part hangs from another turning part');
|
|
308
|
+
return misses;
|
|
309
|
+
},
|
|
310
|
+
reference: steps([
|
|
311
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'cylinder', dimensions: { radius: 150, length: 100 } } },
|
|
312
|
+
{ kind: 'add-part', part: { id: 'lower-arm', kind: 'box', dimensions: { width: 80, height: 500, depth: 80 } } },
|
|
313
|
+
{ kind: 'add-part', part: { id: 'upper-arm', kind: 'box', dimensions: { width: 60, height: 400, depth: 60 } } },
|
|
314
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
315
|
+
{ kind: 'set-joint', joint: { id: 'shoulder', body0: { part: 'base', face: 'top' }, body1: { part: 'lower-arm' }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] } },
|
|
316
|
+
{ kind: 'set-joint', joint: { id: 'elbow', body0: { part: 'lower-arm', face: 'top' }, body1: { part: 'upper-arm' }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -120, upperLimit: 120 }] } }
|
|
317
|
+
]),
|
|
318
|
+
wrong: steps([
|
|
319
|
+
{ kind: 'add-part', part: { id: 'base', kind: 'cylinder', dimensions: { radius: 150, length: 100 } } },
|
|
320
|
+
{ kind: 'add-part', part: { id: 'lower-arm', kind: 'box', dimensions: { width: 80, height: 500, depth: 80 } } },
|
|
321
|
+
{ kind: 'add-part', part: { id: 'upper-arm', kind: 'box', dimensions: { width: 60, height: 400, depth: 60 } } },
|
|
322
|
+
{ kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
|
|
323
|
+
{ kind: 'set-joint', joint: { id: 'shoulder', body0: { part: 'base', face: 'top' }, body1: { part: 'lower-arm' }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -90, upperLimit: 90 }] } },
|
|
324
|
+
{ kind: 'attach', part: 'upper-arm', to: { part: 'lower-arm', face: 'top' } }
|
|
325
|
+
])
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'cabinet-door',
|
|
329
|
+
title: '새 피규어: 경첩 문 0~90°와 여는 클립',
|
|
330
|
+
covers: ['set-joint limits', 'set-driver clip'],
|
|
331
|
+
prompt: 'a cabinet with one door on a hinge that opens from 0 to 90 degrees, and a clip that opens it in 2 seconds',
|
|
332
|
+
name: 'Cabinet',
|
|
333
|
+
check: after => {
|
|
334
|
+
const parts = scenarioPartsOf(after);
|
|
335
|
+
const misses = [...loose(parts)];
|
|
336
|
+
const door = parts.find(p => p.motion.includes('revolute'));
|
|
337
|
+
if (!door)
|
|
338
|
+
return [...misses, 'no hinged part'];
|
|
339
|
+
const span = door.limits.map(l => (l.upper ?? Infinity) - (l.lower ?? -Infinity))[0];
|
|
340
|
+
if (!(span >= 80 && span <= 100))
|
|
341
|
+
misses.push(`the hinge turns ${span}°, 0 to 90 asked`);
|
|
342
|
+
if (!clipsPlaying(after, door.controls).length)
|
|
343
|
+
misses.push("no clip plays the door's hinge");
|
|
344
|
+
return misses;
|
|
345
|
+
},
|
|
346
|
+
reference: steps([
|
|
347
|
+
{ kind: 'add-part', part: { id: 'cabinet', kind: 'box', dimensions: { width: 600, height: 1200, depth: 500 } } },
|
|
348
|
+
{ kind: 'add-part', part: { id: 'door', kind: 'box', dimensions: { width: 600, height: 1200, depth: 20 } } },
|
|
349
|
+
{ kind: 'attach', part: 'cabinet', to: { plane: 'mounting-plane' } },
|
|
350
|
+
{ kind: 'set-joint', joint: { id: 'hinge', body0: { part: 'cabinet', face: 'front' }, body1: { part: 'door' }, origin: { align: { x: 'min' } }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'door-open' } }] } },
|
|
351
|
+
{ kind: 'set-driver', state: 'door-open', clip: 'open', duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 90 }], loop: 'hold' }
|
|
352
|
+
]),
|
|
353
|
+
wrong: steps([
|
|
354
|
+
{ kind: 'add-part', part: { id: 'cabinet', kind: 'box', dimensions: { width: 600, height: 1200, depth: 500 } } },
|
|
355
|
+
{ kind: 'add-part', part: { id: 'door', kind: 'box', dimensions: { width: 600, height: 1200, depth: 20 } } },
|
|
356
|
+
{ kind: 'attach', part: 'cabinet', to: { plane: 'mounting-plane' } },
|
|
357
|
+
{ kind: 'set-joint', joint: { id: 'hinge', body0: { part: 'cabinet', face: 'front' }, body1: { part: 'door' }, origin: { align: { x: 'min' } }, motion: [{ type: 'revolute', axis: 'Y', lowerLimit: 0, upperLimit: 90, state: { id: 'door-open' } }] } }
|
|
358
|
+
])
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: 'hopper',
|
|
362
|
+
title: '새 피규어: 윗면이 넓은 사각 호퍼 (ADR-0100 §16.1)',
|
|
363
|
+
covers: ['add-part tapered-box'],
|
|
364
|
+
prompt: '아래가 좁고 위가 넓은 사각 호퍼: 아랫면 300×300, 윗면 800×800, 높이 600, 모서리는 둥글게',
|
|
365
|
+
name: 'Hopper',
|
|
366
|
+
check: after => {
|
|
367
|
+
const parts = scenarioPartsOf(after);
|
|
368
|
+
const misses = [...loose(parts)];
|
|
369
|
+
const hopper = parts.find(p => p.kind === 'tapered');
|
|
370
|
+
if (!hopper)
|
|
371
|
+
return [...misses, 'no part whose top is another size than its bottom'];
|
|
372
|
+
const l = hopper.shape.loft;
|
|
373
|
+
const [from, to] = [l.section.size, l.to.size];
|
|
374
|
+
if (!(to.x > from.x && to.z > from.z))
|
|
375
|
+
misses.push(`its top ${to.x}×${to.z} is not wider than its bottom ${from.x}×${from.z}`);
|
|
376
|
+
return misses;
|
|
377
|
+
},
|
|
378
|
+
reference: steps([
|
|
379
|
+
{ kind: 'add-part', part: { id: 'hopper', kind: 'tapered-box', dimensions: { width: 300, depth: 300, height: 600, topWidth: 800, topDepth: 800, round: 30, topRound: 60 } } },
|
|
380
|
+
{ kind: 'attach', part: 'hopper', to: { plane: 'mounting-plane' } }
|
|
381
|
+
]),
|
|
382
|
+
wrong: steps([
|
|
383
|
+
{ kind: 'add-part', part: { id: 'hopper', kind: 'tapered-box', dimensions: { width: 800, depth: 800, height: 600, topWidth: 300, topDepth: 300, round: 60, topRound: 30 } } },
|
|
384
|
+
{ kind: 'attach', part: 'hopper', to: { plane: 'mounting-plane' } }
|
|
385
|
+
])
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: 'quadruped-legs',
|
|
389
|
+
title: '새 피규어: 몸통과 좌우 대칭인 다리 넷, 다리마다 무릎',
|
|
390
|
+
covers: ['mirror-part chain', 'joints'],
|
|
391
|
+
prompt: 'a four-legged robot body: a box body and four legs, each leg a thigh and a shin with a hip and a knee joint; the legs symmetric left and right',
|
|
392
|
+
name: 'Quadruped',
|
|
393
|
+
check: after => {
|
|
394
|
+
const parts = scenarioPartsOf(after);
|
|
395
|
+
const misses = [...loose(parts)];
|
|
396
|
+
const moving = parts.filter(p => p.motion.length);
|
|
397
|
+
if (moving.length < 8)
|
|
398
|
+
misses.push(`${moving.length} moving parts, four legs of two joints asked`);
|
|
399
|
+
/* Left and right: every moving part has its reflection across the body's middle along Z or X. */
|
|
400
|
+
const mirrored = (p, axis) => moving.some(q => q !== p && near(q.centre[axis], -p.centre[axis], 1) && ['x', 'y', 'z'].filter(x => x !== axis).every(x => near(q.centre[x], p.centre[x], 1)));
|
|
401
|
+
const lonely = moving.filter(p => !mirrored(p, 'z') && !mirrored(p, 'x'));
|
|
402
|
+
if (lonely.length)
|
|
403
|
+
misses.push(`no mirror of ${lonely.map(p => p.id).join(', ')}`);
|
|
404
|
+
return misses;
|
|
405
|
+
},
|
|
406
|
+
reference: steps([
|
|
407
|
+
{ kind: 'add-part', part: { id: 'body', kind: 'box', dimensions: { width: 800, height: 200, depth: 300 } } },
|
|
408
|
+
{ kind: 'set-placement', placement: 'floor' },
|
|
409
|
+
{ kind: 'attach', part: 'body', to: { plane: 'mounting-plane' }, gap: 500 },
|
|
410
|
+
{ kind: 'add-part', part: { id: 'fl-thigh', kind: 'box', dimensions: { width: 60, height: 250, depth: 60 } } },
|
|
411
|
+
{ kind: 'add-part', part: { id: 'fl-shin', kind: 'box', dimensions: { width: 40, height: 250, depth: 40 } } },
|
|
412
|
+
{ kind: 'set-joint', joint: { id: 'fl-hip', body0: { part: 'body', face: 'bottom' }, body1: { part: 'fl-thigh' }, origin: { align: { x: 'max', z: 'max' } }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -45, upperLimit: 45 }] } },
|
|
413
|
+
{ kind: 'set-joint', joint: { id: 'fl-knee', body0: { part: 'fl-thigh', face: 'bottom' }, body1: { part: 'fl-shin' }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: 0, upperLimit: 120 }] } },
|
|
414
|
+
{ kind: 'mirror-part', target: 'fl-thigh', axis: 'z', chain: true, prefix: { from: 'fl', to: 'fr' } },
|
|
415
|
+
{ kind: 'mirror-part', target: 'fl-thigh', axis: 'x', chain: true, prefix: { from: 'fl', to: 'bl' }, keepMotion: true },
|
|
416
|
+
{ kind: 'mirror-part', target: 'fr-thigh', axis: 'x', chain: true, prefix: { from: 'fr', to: 'br' }, keepMotion: true }
|
|
417
|
+
]),
|
|
418
|
+
wrong: steps([
|
|
419
|
+
{ kind: 'add-part', part: { id: 'body', kind: 'box', dimensions: { width: 800, height: 200, depth: 300 } } },
|
|
420
|
+
{ kind: 'set-placement', placement: 'floor' },
|
|
421
|
+
{ kind: 'attach', part: 'body', to: { plane: 'mounting-plane' }, gap: 500 },
|
|
422
|
+
{ kind: 'add-part', part: { id: 'fl-thigh', kind: 'box', dimensions: { width: 60, height: 250, depth: 60 } } },
|
|
423
|
+
{ kind: 'add-part', part: { id: 'fl-shin', kind: 'box', dimensions: { width: 40, height: 250, depth: 40 } } },
|
|
424
|
+
{ kind: 'set-joint', joint: { id: 'fl-hip', body0: { part: 'body', face: 'bottom' }, body1: { part: 'fl-thigh' }, origin: { align: { x: 'max', z: 'max' } }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: -45, upperLimit: 45 }] } },
|
|
425
|
+
{ kind: 'set-joint', joint: { id: 'fl-knee', body0: { part: 'fl-thigh', face: 'bottom' }, body1: { part: 'fl-shin' }, motion: [{ type: 'revolute', axis: 'Z', lowerLimit: 0, upperLimit: 120 }] } }
|
|
426
|
+
])
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: 'pipeline-run',
|
|
430
|
+
title: '새 피규어: 굽힘 셋 이상인 곡선 배관과 끝 플랜지 (ADR-0100 §16.2)',
|
|
431
|
+
covers: ['add-part pipe', 'several bends', 'attach to a pipe end'],
|
|
432
|
+
prompt: '펌프에서 탱크로 가는 지름 100 배관: 바닥에서 1m 올라가 옆으로 꺾고, 다시 앞으로 꺾고, 마지막에 다시 옆으로 꺾여 탱크 옆면으로 들어가게. 배관 끝에는 플랜지',
|
|
433
|
+
name: 'Pipeline',
|
|
434
|
+
check: after => {
|
|
435
|
+
const parts = scenarioPartsOf(after);
|
|
436
|
+
const misses = [...loose(parts)];
|
|
437
|
+
const pipes = parts.filter(p => p.kind === 'sweep');
|
|
438
|
+
const bends = pipes.reduce((n, p) => n + (p.shape?.sweep?.path ?? []).filter((s) => s.kind === 'bend').length, 0);
|
|
439
|
+
if (!pipes.length)
|
|
440
|
+
return [...misses, 'no part along a path (a pipe)'];
|
|
441
|
+
if (bends < 3)
|
|
442
|
+
misses.push(`${bends} bends, three asked`);
|
|
443
|
+
/* A flange on the pipe's far end, up in the air: not on its start, on the floor. */
|
|
444
|
+
if (!parts.some(p => pipes.some(q => q.id === p.on) && p.kind !== 'sweep' && bottom(p) > 100))
|
|
445
|
+
misses.push('no flange on the end of a pipe');
|
|
446
|
+
return misses;
|
|
447
|
+
},
|
|
448
|
+
/* Up Y; toward 90 is +X; heading +X, toward 0 is +Z; heading +Z, toward 180 is +X again -- its end the right face. */
|
|
449
|
+
reference: steps([
|
|
450
|
+
{ kind: 'add-part', part: { id: 'line', kind: 'pipe', dimensions: { radius: 50, wall: 5 }, path: [{ line: 1000 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 800 }, { bend: { radius: 150, angle: 90, toward: 0 } }, { line: 600 }, { bend: { radius: 150, angle: 90, toward: 180 } }, { line: 400 }] } },
|
|
451
|
+
{ kind: 'attach', part: 'line', to: { plane: 'mounting-plane' } },
|
|
452
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 90, length: 20 } } },
|
|
453
|
+
{ kind: 'attach', part: 'flange', to: { part: 'line', face: 'right' } }
|
|
454
|
+
]),
|
|
455
|
+
wrong: steps([
|
|
456
|
+
{ kind: 'add-part', part: { id: 'line', kind: 'pipe', dimensions: { radius: 50, wall: 5 }, path: [{ line: 1000 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 800 }] } },
|
|
457
|
+
{ kind: 'attach', part: 'line', to: { plane: 'mounting-plane' } },
|
|
458
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 90, length: 20 } } },
|
|
459
|
+
{ kind: 'attach', part: 'flange', to: { part: 'line', face: 'right' } }
|
|
460
|
+
])
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'pipe-rack',
|
|
464
|
+
title: '새 피규어: 나란히 가는 곡선 배관 두 줄',
|
|
465
|
+
covers: ['several pipes', 'attach align'],
|
|
466
|
+
prompt: 'two parallel pipes side by side, 300 mm apart, each rising 800 mm and bending 90 degrees to run 1 m sideways',
|
|
467
|
+
name: 'Pipe rack',
|
|
468
|
+
check: after => {
|
|
469
|
+
const parts = scenarioPartsOf(after);
|
|
470
|
+
const misses = [...loose(parts)];
|
|
471
|
+
const bent = parts.filter(p => p.kind === 'sweep' && (p.shape?.sweep?.path ?? []).some((s) => s.kind === 'bend'));
|
|
472
|
+
if (bent.length < 2)
|
|
473
|
+
return [...misses, `${bent.length} bent pipes, two asked`];
|
|
474
|
+
const [a, b] = bent;
|
|
475
|
+
const apart = Math.hypot(a.centre.x - b.centre.x, a.centre.z - b.centre.z);
|
|
476
|
+
if (!(apart > 200 && apart < 400))
|
|
477
|
+
misses.push(`the pipes are ${Math.round(apart)} mm apart, 300 asked`);
|
|
478
|
+
if (!near(a.extent.y, b.extent.y, 1))
|
|
479
|
+
misses.push('the pipes rise to different heights');
|
|
480
|
+
return misses;
|
|
481
|
+
},
|
|
482
|
+
reference: steps([
|
|
483
|
+
{ kind: 'add-part', part: { id: 'pipe-a', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 800 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 1000 }] } },
|
|
484
|
+
{ kind: 'add-part', part: { id: 'pipe-b', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 800 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 1000 }] } },
|
|
485
|
+
{ kind: 'attach', part: 'pipe-a', to: { plane: 'mounting-plane' }, align: { z: { mm: -150 } } },
|
|
486
|
+
{ kind: 'attach', part: 'pipe-b', to: { plane: 'mounting-plane' }, align: { z: { mm: 150 } } }
|
|
487
|
+
]),
|
|
488
|
+
wrong: steps([
|
|
489
|
+
{ kind: 'add-part', part: { id: 'pipe-a', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 800 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 1000 }] } },
|
|
490
|
+
{ kind: 'add-part', part: { id: 'pipe-b', kind: 'pipe', dimensions: { radius: 40, wall: 4 }, path: [{ line: 800 }] } },
|
|
491
|
+
{ kind: 'attach', part: 'pipe-a', to: { plane: 'mounting-plane' }, align: { z: { mm: -150 } } },
|
|
492
|
+
{ kind: 'attach', part: 'pipe-b', to: { plane: 'mounting-plane' }, align: { z: { mm: 150 } } }
|
|
493
|
+
])
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: 'tapering-bent-bar',
|
|
497
|
+
title: '새 피규어: 아래는 굵고 위로 갈수록 가는 휜 막대 (ADR-0100 §16.2.9)',
|
|
498
|
+
/*
|
|
499
|
+
One pipe narrowing along its length (endRadius), or pieces of different sizes fastened end to end: any such making
|
|
500
|
+
passes. The check reads only what stands: something bends, and it is thicker at its lowest than at its highest.
|
|
501
|
+
*/
|
|
502
|
+
covers: ['pipe endRadius', 'pipe wall 0', 'pieces of different sizes fastened end to end'],
|
|
503
|
+
prompt: '바닥에서 올라가며 한 번 휘어지는 막대. 아래쪽은 지름 80으로 굵고, 위로 갈수록 가늘어져 끝은 지름 40',
|
|
504
|
+
name: 'Bent bar',
|
|
505
|
+
check: after => {
|
|
506
|
+
const parts = scenarioPartsOf(after);
|
|
507
|
+
const misses = [...loose(parts)];
|
|
508
|
+
if (!parts.some(p => p.kind === 'sweep' && (p.shape?.sweep?.path ?? []).some((s) => s.kind === 'bend')))
|
|
509
|
+
misses.push('nothing bends');
|
|
510
|
+
/* How thick each part is at each height: a pipe at its stations, its section there; anything else at its bottom and top. */
|
|
511
|
+
const across = (section) => 2 * Math.max(...Object.values(section.radius ?? {}).map(Number), ...Object.values(section.size ?? {}).map(v => Number(v) / 2));
|
|
512
|
+
const samples = parts.flatMap(p => {
|
|
513
|
+
const w = p.shape?.sweep;
|
|
514
|
+
if (!w)
|
|
515
|
+
return [{ y: bottom(p), thick: Math.min(p.extent.x, p.extent.z) }, { y: top(p), thick: Math.min(p.extent.x, p.extent.z) }];
|
|
516
|
+
const pose = (0, figure_model_1.compileFigureAsset)(after).evaluate().geometry.find(g => g.id === p.id)?.pose;
|
|
517
|
+
const y = (q) => (pose ? pose.r[1][0] * q[0] + pose.r[1][1] * q[1] + pose.r[1][2] * q[2] + pose.t[1] : q[1]);
|
|
518
|
+
return w.stations.map((st) => ({ y: y(st.p), thick: across((0, figure_model_1.figureSweepSectionAt)(w, st.t)) }));
|
|
519
|
+
});
|
|
520
|
+
const low = samples.reduce((a, b) => (b.y < a.y ? b : a));
|
|
521
|
+
const high = samples.reduce((a, b) => (b.y > a.y ? b : a));
|
|
522
|
+
if (!(low.thick > high.thick + 1))
|
|
523
|
+
misses.push(`${Math.round(low.thick)} thick at its lowest, ${Math.round(high.thick)} at its highest: thicker below asked`);
|
|
524
|
+
if (parts.some(p => p.shape?.sweep?.wall !== undefined))
|
|
525
|
+
misses.push('a bar is hollow');
|
|
526
|
+
return misses;
|
|
527
|
+
},
|
|
528
|
+
reference: steps([
|
|
529
|
+
{ kind: 'add-part', part: { id: 'bar', kind: 'pipe', dimensions: { radius: 40, wall: 0, endRadius: 20 }, path: [{ line: 600 }, { bend: { radius: 200, angle: 60, toward: 90 } }, { line: 300 }] } },
|
|
530
|
+
{ kind: 'attach', part: 'bar', to: { plane: 'mounting-plane' } }
|
|
531
|
+
]),
|
|
532
|
+
wrong: steps([
|
|
533
|
+
{ kind: 'add-part', part: { id: 'bar', kind: 'pipe', dimensions: { radius: 20, wall: 0, endRadius: 40 }, path: [{ line: 600 }, { bend: { radius: 200, angle: 60, toward: 90 } }, { line: 300 }] } },
|
|
534
|
+
{ kind: 'attach', part: 'bar', to: { plane: 'mounting-plane' } }
|
|
535
|
+
])
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: 'pipe-down-into-tank',
|
|
539
|
+
title: '새 피규어: 아래로 꺾여 내려가는 배관 끝에 플랜지 (ADR-0100 §16.2.7)',
|
|
540
|
+
covers: ['pipe end heading down', 'attach to a pipe end'],
|
|
541
|
+
prompt: '바닥에서 1m 올라가 옆으로 800mm 가다가 아래로 꺾여 500mm 내려가는 배관. 내려간 끝에 플랜지를 달아 줘',
|
|
542
|
+
name: 'Drop pipe',
|
|
543
|
+
check: after => {
|
|
544
|
+
const parts = scenarioPartsOf(after);
|
|
545
|
+
const misses = [...loose(parts)];
|
|
546
|
+
const pipe = parts.find(p => p.kind === 'sweep' && p.shape.sweep.stations.at(-1).n[1] < -0.999);
|
|
547
|
+
if (!pipe)
|
|
548
|
+
return [...misses, 'no pipe ends heading down'];
|
|
549
|
+
const joints = (after.joints ?? []);
|
|
550
|
+
const onEnd = joints.find(j => j.body0?.part === pipe.id && pipe.shape.faces[j.body0.face]?.station === 'last');
|
|
551
|
+
if (!onEnd)
|
|
552
|
+
misses.push("nothing is fastened to the pipe's lower end");
|
|
553
|
+
return misses;
|
|
554
|
+
},
|
|
555
|
+
/* Up; toward 90 is +X; heading +X, toward 90 turns it down. Its end, heading down, is its bottom face. */
|
|
556
|
+
reference: steps([
|
|
557
|
+
{ kind: 'add-part', part: { id: 'drop', kind: 'pipe', dimensions: { radius: 50, wall: 5 }, path: [{ line: 1000 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 800 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 500 }] } },
|
|
558
|
+
{ kind: 'attach', part: 'drop', to: { plane: 'mounting-plane' } },
|
|
559
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 90, length: 20 } } },
|
|
560
|
+
{ kind: 'attach', part: 'flange', to: { part: 'drop', face: 'bottom' } }
|
|
561
|
+
]),
|
|
562
|
+
wrong: steps([
|
|
563
|
+
{ kind: 'add-part', part: { id: 'drop', kind: 'pipe', dimensions: { radius: 50, wall: 5 }, path: [{ line: 1000 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 800 }, { bend: { radius: 150, angle: 90, toward: 90 } }, { line: 500 }] } },
|
|
564
|
+
{ kind: 'attach', part: 'drop', to: { plane: 'mounting-plane' } },
|
|
565
|
+
{ kind: 'add-part', part: { id: 'flange', kind: 'cylinder', dimensions: { radius: 90, length: 20 } } },
|
|
566
|
+
{ kind: 'attach', part: 'flange', to: { plane: 'mounting-plane' } }
|
|
567
|
+
])
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: 'mixer-port',
|
|
571
|
+
title: '고치기: 포트 폭 두 배와 새 색, 몸통은 그대로',
|
|
572
|
+
covers: ['set-section across', 'set-material', 'a revision changes only what is asked'],
|
|
573
|
+
prompt: 'make the port twice as wide and give it an accent colour',
|
|
574
|
+
base: mixerDraft,
|
|
575
|
+
check: (after, before) => portRevisionMisses(before, after),
|
|
576
|
+
reference: steps([{ kind: 'set-section', part: 'port', across: { x: 160 } }, accent('port', '#d08a2c')]),
|
|
577
|
+
wrong: steps([accent('port', '#d08a2c')])
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: 'open-tote',
|
|
581
|
+
title: '고치기: 상자를 위가 열린 통으로, 벽 10mm',
|
|
582
|
+
covers: ['set-cavity'],
|
|
583
|
+
prompt: '이 상자를 위가 열린 통으로 만들어 줘. 벽 두께 10mm, 바닥 두께 15mm',
|
|
584
|
+
base: boxDraft,
|
|
585
|
+
check: after => {
|
|
586
|
+
const bin = scenarioPartsOf(after).find(p => p.id === 'bin');
|
|
587
|
+
if (!bin)
|
|
588
|
+
return ['the box is gone'];
|
|
589
|
+
const c = bin.shape?.cavity;
|
|
590
|
+
if (!c)
|
|
591
|
+
return ['no inside'];
|
|
592
|
+
const misses = [];
|
|
593
|
+
if (c.end !== 'open')
|
|
594
|
+
misses.push('its top is not open');
|
|
595
|
+
if (c.start === 'open')
|
|
596
|
+
misses.push('its bottom is open too');
|
|
597
|
+
if (!Object.values(c.wall).every(w => near(w, 10)))
|
|
598
|
+
misses.push(`walls ${Object.values(c.wall).join(', ')}, 10 asked`);
|
|
599
|
+
return misses;
|
|
600
|
+
},
|
|
601
|
+
reference: steps([{ kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } }]),
|
|
602
|
+
wrong: steps([{ kind: 'set-cavity', part: 'bin', cavity: { wall: 10, start: 'open', end: 'open' } }])
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
id: 'pipe-extend',
|
|
606
|
+
title: '고치기: 관 끝에 아래로 꺾이는 굽힘과 직선 더하기 (ADR-0100 §16.2)',
|
|
607
|
+
covers: ['set-pipe path', 'bend direction'],
|
|
608
|
+
prompt: '이 관의 끝에서 아래쪽으로 90도 한 번 더 꺾고 300mm 더 뻗게 해 줘. 앞부분은 그대로',
|
|
609
|
+
base: elbowDraft,
|
|
610
|
+
check: (after, before) => {
|
|
611
|
+
const was = scenarioPartsOf(before).find(p => p.id === 'pipe');
|
|
612
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'pipe');
|
|
613
|
+
if (!now?.shape?.sweep)
|
|
614
|
+
return ['the pipe is gone'];
|
|
615
|
+
const misses = [];
|
|
616
|
+
const [p0, p1] = [was.shape.sweep.path, now.shape.sweep.path];
|
|
617
|
+
if (JSON.stringify(p1.slice(0, p0.length)) !== JSON.stringify(p0))
|
|
618
|
+
misses.push('the pipe as it was is changed');
|
|
619
|
+
if (p1.length !== p0.length + 2)
|
|
620
|
+
misses.push(`${p1.length - p0.length} pieces added, a bend and a run asked`);
|
|
621
|
+
else {
|
|
622
|
+
const [bend, run] = p1.slice(p0.length);
|
|
623
|
+
if (!(bend.kind === 'bend' && near(bend.angle, 90)))
|
|
624
|
+
misses.push(`it adds ${JSON.stringify(bend)}, a 90° bend asked`);
|
|
625
|
+
if (!(run.kind === 'line' && near(run.length, 300)))
|
|
626
|
+
misses.push(`it adds ${JSON.stringify(run)}, a 300 mm run asked`);
|
|
627
|
+
}
|
|
628
|
+
const heading = now.shape.sweep.stations.at(-1).n;
|
|
629
|
+
if (!(heading[1] < -0.999))
|
|
630
|
+
misses.push(`it ends heading ${heading.map(v => Math.round(v * 100) / 100).join(', ')}, down asked`);
|
|
631
|
+
return misses;
|
|
632
|
+
},
|
|
633
|
+
/* Heading +X after the first bend, its section's first axis is still +Z and its second is now -Y: 90 turns it down. */
|
|
634
|
+
reference: steps([{ kind: 'set-pipe', part: 'pipe', path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 400 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 300 }] }]),
|
|
635
|
+
wrong: steps([{ kind: 'set-pipe', part: 'pipe', path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 400 }, { bend: { radius: 120, angle: 90, toward: 270 } }, { line: 300 }] }])
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: 'pipe-to-duct',
|
|
639
|
+
title: '고치기: 원형 관을 사각 덕트로, 경로는 그대로',
|
|
640
|
+
covers: ['set-pipe section'],
|
|
641
|
+
prompt: '이 관을 가로 120, 세로 80 의 사각 덕트로 바꿔 줘. 경로는 그대로',
|
|
642
|
+
base: elbowDraft,
|
|
643
|
+
check: (after, before) => {
|
|
644
|
+
const was = scenarioPartsOf(before).find(p => p.id === 'pipe');
|
|
645
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'pipe');
|
|
646
|
+
if (!now?.shape?.sweep)
|
|
647
|
+
return ['the pipe is gone'];
|
|
648
|
+
const misses = [];
|
|
649
|
+
const s = now.shape.sweep.section;
|
|
650
|
+
if (s.kind !== 'rectangle')
|
|
651
|
+
return [`its section is a ${s.kind}, a rectangle asked`];
|
|
652
|
+
const sizes = [s.size.x, s.size.z].sort((a, b) => a - b);
|
|
653
|
+
if (!(near(sizes[0], 80) && near(sizes[1], 120)))
|
|
654
|
+
misses.push(`its section is ${s.size.x}×${s.size.z}, 120×80 asked`);
|
|
655
|
+
if (JSON.stringify(now.shape.sweep.path) !== JSON.stringify(was.shape.sweep.path))
|
|
656
|
+
misses.push('its path changed');
|
|
657
|
+
return misses;
|
|
658
|
+
},
|
|
659
|
+
reference: steps([{ kind: 'set-pipe', part: 'pipe', width: 120, depth: 80, round: 0 }]),
|
|
660
|
+
wrong: steps([{ kind: 'set-pipe', part: 'pipe', width: 120, depth: 80, round: 0, path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 90 } }, { line: 300 }] }])
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
id: 'pipe-to-solid-bar',
|
|
664
|
+
title: '고치기: 관을 굵은 속찬 막대로, 경로는 그대로',
|
|
665
|
+
covers: ['set-pipe radius', 'wall 0'],
|
|
666
|
+
prompt: '이 관을 지름 60 의 속이 찬 막대로 바꿔 줘',
|
|
667
|
+
base: elbowDraft,
|
|
668
|
+
check: (after, before) => {
|
|
669
|
+
const was = scenarioPartsOf(before).find(p => p.id === 'pipe');
|
|
670
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'pipe');
|
|
671
|
+
if (!now?.shape?.sweep)
|
|
672
|
+
return ['the pipe is gone'];
|
|
673
|
+
const misses = [];
|
|
674
|
+
const s = now.shape.sweep.section;
|
|
675
|
+
if (s.kind !== 'ellipse' || !near(s.radius.x, 30))
|
|
676
|
+
misses.push(`its section is ${JSON.stringify(s)}, a 60 mm circle asked`);
|
|
677
|
+
if (now.shape.sweep.wall !== undefined)
|
|
678
|
+
misses.push('it is still hollow');
|
|
679
|
+
if (JSON.stringify(now.shape.sweep.path) !== JSON.stringify(was.shape.sweep.path))
|
|
680
|
+
misses.push('its path changed');
|
|
681
|
+
return misses;
|
|
682
|
+
},
|
|
683
|
+
reference: steps([{ kind: 'set-pipe', part: 'pipe', radius: 30, wall: 0 }]),
|
|
684
|
+
wrong: steps([{ kind: 'set-pipe', part: 'pipe', radius: 30 }])
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: 'pipe-narrow',
|
|
688
|
+
title: '고치기: 관이 끝으로 갈수록 가늘어지게 (ADR-0100 §16.2.9)',
|
|
689
|
+
covers: ['set-pipe endRadius'],
|
|
690
|
+
prompt: '이 관이 끝으로 갈수록 가늘어져서 끝에서는 지름 40 이 되게 해 줘. 시작 굵기와 경로는 그대로',
|
|
691
|
+
base: elbowDraft,
|
|
692
|
+
check: (after, before) => {
|
|
693
|
+
const was = scenarioPartsOf(before).find(p => p.id === 'pipe');
|
|
694
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'pipe');
|
|
695
|
+
if (!now?.shape?.sweep)
|
|
696
|
+
return ['the pipe is gone'];
|
|
697
|
+
const w = now.shape.sweep;
|
|
698
|
+
const misses = [];
|
|
699
|
+
if (!w.to)
|
|
700
|
+
misses.push(`it does not narrow: one section, ${JSON.stringify(w.section)}, end to end`);
|
|
701
|
+
else if (!(w.to.kind === 'ellipse' && near(w.to.radius.x, 20)))
|
|
702
|
+
misses.push(`its end is ${JSON.stringify(w.to)}, a 40 mm circle asked`);
|
|
703
|
+
if (!near(w.section.radius?.x ?? 0, was.shape.sweep.section.radius.x))
|
|
704
|
+
misses.push('its start changed');
|
|
705
|
+
if (JSON.stringify(w.path) !== JSON.stringify(was.shape.sweep.path))
|
|
706
|
+
misses.push('its path changed');
|
|
707
|
+
return misses;
|
|
708
|
+
},
|
|
709
|
+
reference: steps([{ kind: 'set-pipe', part: 'pipe', endRadius: 20 }]),
|
|
710
|
+
wrong: steps([{ kind: 'set-pipe', part: 'pipe', radius: 20 }])
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
id: 'pipe-turn-other-way',
|
|
714
|
+
title: '고치기: 관이 반대쪽(-X)으로 꺾이게',
|
|
715
|
+
covers: ['set-pipe bend toward'],
|
|
716
|
+
prompt: '이 관이 오른쪽이 아니라 반대쪽(-X 방향)으로 꺾이게 해 줘. 길이와 굵기는 그대로',
|
|
717
|
+
base: elbowDraft,
|
|
718
|
+
check: after => {
|
|
719
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'pipe');
|
|
720
|
+
if (!now?.shape?.sweep)
|
|
721
|
+
return ['the pipe is gone'];
|
|
722
|
+
const misses = [];
|
|
723
|
+
const end = now.shape.sweep.stations.at(-1);
|
|
724
|
+
if (!(end.n[0] < -0.999))
|
|
725
|
+
misses.push(`it ends heading ${end.n.map((v) => Math.round(v * 100) / 100).join(', ')}, -X asked`);
|
|
726
|
+
const lengths = now.shape.sweep.path.map((p) => p.length ?? p.radius);
|
|
727
|
+
if (JSON.stringify(lengths) !== JSON.stringify([500, 120, 400]))
|
|
728
|
+
misses.push(`its runs and radius are ${lengths.join(', ')}, kept asked`);
|
|
729
|
+
return misses;
|
|
730
|
+
},
|
|
731
|
+
reference: steps([{ kind: 'set-pipe', part: 'pipe', path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 270 } }, { line: 400 }] }]),
|
|
732
|
+
wrong: steps([{ kind: 'set-pipe', part: 'pipe', path: [{ line: 500 }, { bend: { radius: 120, angle: 90, toward: 0 } }, { line: 400 }] }])
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
id: 'box-tapered-top',
|
|
736
|
+
title: '고치기: 상자 윗면을 절반 크기로 (ADR-0100 §16.1)',
|
|
737
|
+
covers: ['set-section top'],
|
|
738
|
+
prompt: '이 상자의 윗면을 가로세로 절반 크기로 줄여서 위로 갈수록 좁아지게 해 줘. 아랫면은 그대로',
|
|
739
|
+
base: boxDraft,
|
|
740
|
+
check: (after, before) => {
|
|
741
|
+
const was = scenarioPartsOf(before).find(p => p.id === 'bin');
|
|
742
|
+
const now = scenarioPartsOf(after).find(p => p.id === 'bin');
|
|
743
|
+
if (!now)
|
|
744
|
+
return ['the box is gone'];
|
|
745
|
+
if (now.kind !== 'tapered')
|
|
746
|
+
return [`it is a ${now.kind}, a tapered box asked`];
|
|
747
|
+
const misses = [];
|
|
748
|
+
const [from, to] = [now.shape.loft.section.size, now.shape.loft.to.size];
|
|
749
|
+
if (!(near(from.x, was.extent.x) && near(from.z, was.extent.z)))
|
|
750
|
+
misses.push(`its bottom is ${from.x}×${from.z}, kept asked`);
|
|
751
|
+
if (!(near(to.x, from.x / 2) && near(to.z, from.z / 2)))
|
|
752
|
+
misses.push(`its top is ${to.x}×${to.z}, half asked`);
|
|
753
|
+
return misses;
|
|
754
|
+
},
|
|
755
|
+
reference: steps([{ kind: 'set-section', part: 'bin', top: { across: { x: 300, z: 200 } } }]),
|
|
756
|
+
wrong: steps([{ kind: 'set-section', part: 'bin', across: { x: 300, z: 200 } }])
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: 'table-follows-width',
|
|
760
|
+
title: '고치기: 보드에서 폭을 바꾸면 상판이 따라가게',
|
|
761
|
+
covers: ['declare-instance-size', 'follow-size'],
|
|
762
|
+
prompt: '보드에서 이 테이블의 폭을 바꾸면 상판 폭이 따라 늘어나게 해 줘. 다리는 그대로',
|
|
763
|
+
base: tableDraft,
|
|
764
|
+
check: after => {
|
|
765
|
+
const inputs = after.document.model.inputs;
|
|
766
|
+
const width = inputs.find(i => i.id === 'size.x');
|
|
767
|
+
if (!width)
|
|
768
|
+
return ["the figure's size is not declared"];
|
|
769
|
+
const at = (x) => scenarioPartsOf({ ...after, designInputs: { ...after.designInputs, 'size.x': x } });
|
|
770
|
+
const was = Object.fromEntries(at(after.designInputs['size.x']).map(p => [p.id, p]));
|
|
771
|
+
const wider = Object.fromEntries(at(after.designInputs['size.x'] * 1.5).map(p => [p.id, p]));
|
|
772
|
+
const misses = [];
|
|
773
|
+
if (!(wider.top.extent.x > was.top.extent.x + 1))
|
|
774
|
+
misses.push('the top does not grow with the width');
|
|
775
|
+
if (!near(wider.leg.extent.x, was.leg.extent.x))
|
|
776
|
+
misses.push('the leg grows with the width');
|
|
777
|
+
return misses;
|
|
778
|
+
},
|
|
779
|
+
reference: steps([{ kind: 'declare-instance-size' }, { kind: 'follow-size', part: 'top', axes: ['x'] }]),
|
|
780
|
+
wrong: steps([{ kind: 'declare-instance-size' }])
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
id: 'status-lamp',
|
|
784
|
+
title: '고치기: 상태 값으로 램프 색 (데이터 바인딩)',
|
|
785
|
+
covers: ['set-data-schema', 'set-data-binding table'],
|
|
786
|
+
prompt: 'status 값이 ALARM 이면 램프를 빨간색으로 깜빡이고, NORMAL 이면 초록색으로 켜지게 해 줘',
|
|
787
|
+
base: lampDraft,
|
|
788
|
+
check: after => {
|
|
789
|
+
const bindings = (after.dataInterface?.bindings ?? []);
|
|
790
|
+
const table = bindings.find(b => b.rule === 'table' && (b.targets?.parts ?? []).includes('lamp'));
|
|
791
|
+
if (!table)
|
|
792
|
+
return ['no table binding on the lamp'];
|
|
793
|
+
const misses = [];
|
|
794
|
+
const red = (c) => !!c && /^#([ef][0-9a-f]|d[89a-f])[0-4][0-9a-f][0-4][0-9a-f]$/i.test(c);
|
|
795
|
+
const green = (c) => !!c && /^#[0-4][0-9a-f]([89a-f][0-9a-f])[0-6][0-9a-f]$/i.test(c);
|
|
796
|
+
if (!red(table.table?.ALARM?.color))
|
|
797
|
+
misses.push(`ALARM is ${table.table?.ALARM?.color ?? 'not there'}, red asked`);
|
|
798
|
+
if (table.table?.ALARM?.effect?.kind !== 'blink')
|
|
799
|
+
misses.push('ALARM does not blink');
|
|
800
|
+
if (!green(table.table?.NORMAL?.color))
|
|
801
|
+
misses.push(`NORMAL is ${table.table?.NORMAL?.color ?? 'not there'}, green asked`);
|
|
802
|
+
return misses;
|
|
803
|
+
},
|
|
804
|
+
reference: steps([
|
|
805
|
+
{ kind: 'set-data-schema', schema: [{ name: 'status', kind: 'text', values: ['NORMAL', 'ALARM'] }] },
|
|
806
|
+
{ kind: 'set-data-binding', binding: { id: 'lamp-status', accessor: 'status', rule: 'table', lookup: 'code', targets: { parts: ['lamp'] }, table: { ALARM: { color: '#ef4444', effect: { kind: 'blink' } }, NORMAL: { color: '#22c55e' } } } }
|
|
807
|
+
]),
|
|
808
|
+
wrong: steps([
|
|
809
|
+
{ kind: 'set-data-schema', schema: [{ name: 'status', kind: 'text', values: ['NORMAL', 'ALARM'] }] },
|
|
810
|
+
{ kind: 'set-data-binding', binding: { id: 'lamp-status', accessor: 'status', rule: 'table', lookup: 'code', targets: { parts: ['lamp'] }, table: { ALARM: { color: '#ef4444' }, NORMAL: { color: '#22c55e' } } } }
|
|
811
|
+
])
|
|
812
|
+
}
|
|
813
|
+
];
|
|
814
|
+
//# sourceMappingURL=figure-ai-scenarios.js.map
|