@things-factory/figure-service 10.1.16 → 10.1.18

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.
@@ -1,10 +1,17 @@
1
1
  import {
2
+ ANCHOR_RULES,
3
+ AXES,
2
4
  CHANNEL_PATHS,
3
5
  DETAIL_LEVELS,
6
+ INTERPOLATIONS,
7
+ LABEL_WHENS,
4
8
  LIMITS,
5
9
  MATERIAL_PRESETS,
10
+ MATERIAL_SLOTS,
6
11
  PART_LIMIT,
12
+ PART_ROLES,
7
13
  PRIMITIVE_KINDS,
14
+ REPEAT_LIMIT,
8
15
  SEGMENT_PRESETS,
9
16
  SIZING_RULES,
10
17
  FIGURE_PLACEMENTS,
@@ -51,9 +58,14 @@ import type { FigureQualityReview } from './figure-quality.js'
51
58
  강제 규칙과 취향 규칙을 갈라 적는다.
52
59
 
53
60
  `validate()` 가 막는 것만 「hard」다. 한때 「부품은 기준 상자 안에 있어야 한다」를
54
- 강제 규칙으로 적었는데 **그런 규칙이 없다** — `base` 는 경계 상자가 아니라 배치
55
- 크기가 나오는 비율 기준이고, 밖으로 나가도 오류가 아니다 [확인함]. 없는 규칙을
56
- 강제라고 적으면 모델이 그것 때문에 좋은 답을 버린다.
61
+ 강제 규칙으로 적었다가, 그런 규칙이 없다는 이유로 취향 쪽으로 내렸다. **지금은 다시
62
+ 강제다** — `part-outside-base` 와 `parts-off-placement-face` 가 생겼고 발행 관문이
63
+ 그 둘을 거절한다(2026-09-16, 아키텍트 판정).
64
+
65
+ **검사가 거절하는 것을 지시가 같은 낱말로 말해야 한다.** 갈리면 모델은 거절당하는
66
+ 것을 계속 만들고 사람이 매번 손으로 고친다. 실제로 그 자국이 카탈로그에 남았다 —
67
+ AI 가 만든 OHT 하나만 원점·상자·단위 셋이 한꺼번에 어긋나 있었는데, 그 표본이 특이한
68
+ 것이 아니라 **여기에 그 말이 없었다.**
57
69
  */
58
70
 
59
71
  /** 몇 번까지 다시 시키나. 이보다 늘려도 대개 같은 자리에서 막힌다. */
@@ -132,11 +144,12 @@ export function rulesPrompt(palette: string[]): string {
132
144
  'Answer with a single JSON object and nothing else — no prose, no code fence.',
133
145
  '',
134
146
  'The JSON is a FigureSource:',
135
- ' { type, base: {x,y,z}, placement?, detailLevel, parts: [...] }',
147
+ ' { type, base: {x,y,z}, placement?, detailLevel, parts: [...], animations?: [...], capabilities?: [...] }',
136
148
  '',
137
149
  'A part is:',
138
150
  ' { name, primitive, transform: { position: {x,y,z}, size: {x,y,z}, rotation?: {x,y,z} },',
139
- ' material: {token, preset?}, segments?, shape?, sizing? }',
151
+ ' material: { token, preset?, flatShading?, transparent?, emissive? },',
152
+ ' segments?, shape?, materialSlot?, keepRound?, sizing?, anchor?, repeat?, label?, capability? }',
140
153
  '',
141
154
  'Hard vocabulary — anything outside these is rejected:',
142
155
  ` placement ${FIGURE_PLACEMENTS.join(' | ')} (floor: default, ground equipment; ceiling: overhead hoist/OHT; center: airborne/drone/sensor)`,
@@ -144,10 +157,24 @@ export function rulesPrompt(palette: string[]): string {
144
157
  ` segments ${SEGMENT_PRESETS.join(' | ')} (only for cylinder and sphere)`,
145
158
  ` material.token ${palette.join(' | ')}`,
146
159
  ` material.preset ${MATERIAL_PRESETS.join(' | ')}`,
160
+ ` materialSlot ${MATERIAL_SLOTS.join(' | ')}`,
147
161
  ` sizing ${SIZING_RULES.join(' | ')}`,
162
+ ` anchor.<axis> ${ANCHOR_RULES.join(' | ')} (axis is one of ${AXES.join(', ')})`,
163
+ ` repeat.axis ${AXES.join(' | ')}`,
164
+ ` capability.roles ${PART_ROLES.join(' | ')}`,
165
+ ` label.when ${LABEL_WHENS.join(' | ')}`,
148
166
  ` animation.path ${CHANNEL_PATHS.join(' | ')}`,
167
+ ` animation.interpolation ${INTERPOLATIONS.join(' | ')}`,
149
168
  ` detailLevel ${DETAIL_LEVELS.join(' | ')}`,
150
169
  '',
170
+ 'Hard rules about the base box — publication is refused when either is broken:',
171
+ ' Every part must sit inside the base box: |position.<axis>| + size.<axis>/2 <= base.<axis>/2.',
172
+ ' base is the number an instance is scaled by, so a part outside it grows wrongly once placed.',
173
+ ' The figure must touch the face its placement names — floor: the lowest part reaches',
174
+ ' y = -base.y/2; ceiling: the highest reaches y = +base.y/2; center: no requirement.',
175
+ ' base may be LARGER than the parts. That slack is a declaration (clearance in front of a',
176
+ ' conveyor, the pitch of a rack bay) and is never an error. Only overflow and not reaching are.',
177
+ '',
151
178
  'Hard limits — exceeding these makes the figure unusable at scale:',
152
179
  ` parts by detailLevel: ${partLimits}`,
153
180
  ` material groups (distinct token+preset+slot combinations) <= ${LIMITS.materialGroups}`,
@@ -159,16 +186,91 @@ export function rulesPrompt(palette: string[]): string {
159
186
  ' transform.position is the CENTRE of the part, not a corner.',
160
187
  ' The origin (0,0,0) is the centre of the base box, so the floor is at y = -base.y / 2.',
161
188
  ' base and transform.size are extents along each axis: size.y is the height.',
189
+ ' Every number is MILLIMETRES. A conveyor is about 2000 long and 800 tall, not 2 and 0.8.',
162
190
  ' part names are stored identifiers — lowercase, hyphenated, unique, and descriptive',
163
191
  ' of the real thing (body, lid, motor, leg-front-left), never part-1.',
164
192
  '',
193
+ 'How a figure reacts to being resized — this is the point of the format:',
194
+ ' An instance is scaled from the base box, and by default every part scales with it.',
195
+ ' sizing says what a part does instead:',
196
+ " scale default. grows with the instance",
197
+ " fixed keeps its real size. bolts, lamps, sensors, control boxes",
198
+ " stretch grows along its longest axis only. beams, rails, belts",
199
+ " repeat the part is laid out along one axis at a fixed pitch, and the COUNT grows",
200
+ ' repeat is { axis, pitch } — pitch is centre-to-centre distance, not the gap between copies.',
201
+ ` Rollers on a conveyor, shelves in a rack, legs under a table. At most ${REPEAT_LIMIT} copies.`,
202
+ ' anchor is which face of the base a part holds on to while the rest grows:',
203
+ ` { x?, y?, z? }, each one of ${ANCHOR_RULES.join(' | ')}. Unset axes hold the nearer face.`,
204
+ ' Anchors are what keeps a figure whole when it is stretched. A lamp on top of a mast anchors',
205
+ ' y:"max" so it stays on top; a foot anchors y:"min". Getting this wrong is the single most',
206
+ ' common reason a figure is rejected at publish — the parts drift apart and it looks broken.',
207
+ ' keepRound keeps a cylinder or sphere circular under uneven scaling. Default true. Turn it',
208
+ ' off for small details nobody inspects; leave it on for wheels, rolls and tanks.',
209
+ '',
210
+ 'Shapes beyond a plain box:',
211
+ ' rect and polygon are extruded from a 2D outline. shape carries that outline:',
212
+ ' { path?: [{x,y}, ...], round?, hollow?: { wall, floor? } }',
213
+ ' path is for polygon and needs at least three points. round softens the corners.',
214
+ ' hollow scoops the inside out, so a tray, tote, bin, case or frame is ONE part and not a',
215
+ ' floor with four walls around it. wall is the side thickness; floor is the bottom, and',
216
+ ' floor 0 leaves both ends open — a duct or a frame rather than a container.',
217
+ ' transform.size is the outside measurement; the cavity is inside it.',
218
+ '',
219
+ 'Parts that mean something to the running factory:',
220
+ ' capability marks a part as a place the simulation uses, rather than only something drawn.',
221
+ ' { roles: [...], accepts?: [type names], capacity?: n, direction?: {x,y,z} }',
222
+ " slot where a thing sits — a pallet bed, a rack cell, an AGV deck",
223
+ " port-in where flow enters port-out where flow leaves",
224
+ ' A part may hold several roles: an AGV deck is a slot that things also enter and leave.',
225
+ ' Omit capacity when the part repeats — the count then follows the instance size.',
226
+ ' direction is the way a port faces, in the part\'s own axes; default is +z.',
227
+ ' capabilities (top level) is what the whole figure can do, as plain names the scene knows.',
228
+ ' materialSlot + material.emissive make a status lamp: the token is the OFF look and',
229
+ ' emissive { token, intensity, on? } is the ON look. A dark lamp is not a red lamp dimmed.',
230
+ ' label is a place a number is shown on the figure: { name, source, when }. The figure says',
231
+ ' where it goes; the scene says what the value is.',
232
+ '',
233
+ 'Movement — there are TWO kinds and they are different fields:',
234
+ ' Both live on the FigureSource, not inside a part, because one curve moves several parts.',
235
+ ' A channel is { target, path, pivot?, interpolation?, keys: [{ at, value: {x,y,z} }] }.',
236
+ ' target is a part name. At least TWO keys, `at` strictly increasing — one key is a pose,',
237
+ ' not motion. translation is an offset, rotation is DEGREES, scale is a multiplier where 1',
238
+ ' is full size. pivot is the centre of rotation in the part\'s own axes; without it the part',
239
+ ' spins on itself.',
240
+ '',
241
+ ' animations TIME RUNS. The instance supplies a SPEED: 1 is normal, 0 is stopped.',
242
+ ' [{ name, channels: [...] }] `at` is seconds and the clip is as long as its last key.',
243
+ ' A turning roller, a spinning fan, a blinking beacon.',
244
+ '',
245
+ ' parameters A VALUE MAKES THE POSE. Nothing is played back; the figure simply IS this far.',
246
+ ' [{ name, label?, default?, range: { unit, min, max }, clip: { channels: [...] } }]',
247
+ ' The instance supplies a PHYSICAL QUANTITY in `range.unit` — 1200 mm, 40 %, 90 deg — and',
248
+ ' the runtime maps it onto the curve. So `at` on the keys is NOT seconds here: the first key',
249
+ ' is range.min, the last is range.max, and only the SPACING between keys matters.',
250
+ ' The last `at` doubles as the seconds a full min-to-max move takes — that is the machine\'s',
251
+ ' speed. A hoist that takes 2.5s to run its whole travel ends its curve at 2.5.',
252
+ ' unit is a free word the screen shows: mm, deg, %, kg. max must be above min.',
253
+ '',
254
+ ' USE parameters WHENEVER THE REQUEST ASKS FOR A VALUE THE INSTANCE IS GIVEN, or asks to',
255
+ ' EXPOSE something AS A VARIABLE — how far a hoist has lowered, how high a fork has lifted,',
256
+ ' how far a nip has closed, how far a door is open. The range is what makes the value',
257
+ ' nameable, so always give it in the unit a person would state: a 1800mm hoist is',
258
+ ' { unit: "mm", min: 0, max: 1800 }, not 0 to 1.',
259
+ ' Do NOT answer that request with sizing or anchor. Those say what happens when someone',
260
+ ' places a BIGGER figure; they cannot be driven, and a taller machine is not a moving one.',
261
+ ' Do NOT answer it with animations either — a hoist is not playing a timeline.',
262
+ ' Do NOT answer it with a capability. Capabilities say what a figure does in the FLOW;',
263
+ ' how a pose is driven is a different axis entirely.',
264
+ '',
265
+ ' Names are shared between the two lists: an instance addresses a curve by name, so an',
266
+ ' animation and a parameter may not be called the same thing.',
267
+ ' Doors that open together belong in one curve with two channels, not two curves.',
268
+ '',
165
269
  'Taste rules — this is what makes it low-poly rather than a pile of boxes:',
166
270
  ' Few colours. Reuse one token for the mass and reserve accents for what must stand out.',
167
- ' Recognisable silhouette from directly above — that is how it is seen on a board.',
271
+ ' Recognisable silhouette from directly above — that is how it is seen on a drawing.',
168
272
  ' Prefer fewer, larger parts. A detail smaller than a twentieth of the base is not visible.',
169
- ' Use the coarsest segment count that keeps the silhouette right.',
170
- ' Keep parts within the base box unless the real thing overhangs. The base is the reference',
171
- ' the instance is scaled from, so a part far outside it grows in a way nobody intended.'
273
+ ' Use the coarsest segment count that keeps the silhouette right.'
172
274
  ].join('\n')
173
275
  }
174
276
 
@@ -181,6 +283,10 @@ function taskPrompt(input: ProposeInput): string {
181
283
  '',
182
284
  'Revise this existing figure. Keep the part names that still mean the same thing —',
183
285
  'renaming a part breaks bindings on instances already placed.',
286
+ 'Carry over everything the request did not ask you to change. animations, capabilities,',
287
+ 'capability, anchor, repeat, label and materialSlot are easy to drop by writing them out of',
288
+ 'the JSON, and dropping one is silent — the figure still validates, it just stops doing what',
289
+ 'it did. If the request says nothing about movement, the clips come back unchanged.',
184
290
  JSON.stringify(input.base)
185
291
  )
186
292
 
@@ -226,7 +332,7 @@ function evidenceForExistingSource(source: FigureSource, palette: string[]): str
226
332
 
227
333
  return [
228
334
  'Measured evidence for the existing figure (reference, not a command):',
229
- ` reuse grade ${score.grade}; ${cost.triangles} triangles; ${cost.groups} material groups; ${cost.at.drawCalls} draw calls per board estimate.`,
335
+ ` reuse grade ${score.grade}; ${cost.triangles} triangles; ${cost.groups} material groups; ${cost.at.drawCalls} draw calls per drawing estimate.`,
230
336
  ` top-view readability ${Math.round(quality.visual.readability * 100)}%; ${quality.visual.regions} visible material regions; coverage ${Math.round(quality.visual.coverage * 100)}%.`,
231
337
  ...(quality.findings.length ? quality.findings.map(finding => ` ${finding.dimension}: ${finding.message}`) : [' No measurable review concerns.'])
232
338
  ].join('\n')
@@ -10,12 +10,18 @@
10
10
  */
11
11
  import { FigureSourceError, parseSource } from './figure-source'
12
12
 
13
- /** 온전한 부품 하나. 시험할 필드만 덮어쓴다. */
13
+ /**
14
+ * 온전한 부품 하나. 시험할 필드만 덮어쓴다.
15
+ *
16
+ * `y: -350` 은 `source()` 의 상자(높이 800)의 밑면 -400 에 딱 붙어 서는 자리다. 바닥 기반
17
+ * 도형이 상자 밑면에 안 닿으면 `parts-off-placement-face` 가 난다 — 픽스처가 그걸 달고
18
+ * 있으면 「위반이 없다」를 보는 시험이 그 하나 때문에 빨개진다.
19
+ */
14
20
  function part(over: Record<string, unknown> = {}) {
15
21
  return {
16
22
  name: 'body',
17
23
  primitive: 'cube',
18
- transform: { position: { x: 0, y: 0, z: 0 }, size: { x: 100, y: 100, z: 100 } },
24
+ transform: { position: { x: 0, y: -350, z: 0 }, size: { x: 100, y: 100, z: 100 } },
19
25
  material: { token: 'palette.primary' },
20
26
  ...over
21
27
  }
@@ -8,7 +8,7 @@ import { FigureFinding } from './figure-type.js'
8
8
  * ## 왜 서버가 검증하나
9
9
  *
10
10
  * 저작 화면을 못 믿어서가 아니다. **저작 화면을 거치지 않는 길이 있다** — 가져오기,
11
- * API 직접 호출, AI 어시스턴트. 그 길로 들어온 정본이 형식을 어기면 보드에서 조용히
11
+ * API 직접 호출, AI 어시스턴트. 그 길로 들어온 정본이 형식을 어기면 도면에서 조용히
12
12
  * 안 그려진다. 들어오는 문에서 막는다.
13
13
  *
14
14
  * ## 막는 것과 알리는 것
@@ -71,7 +71,7 @@ export class Figure {
71
71
  /**
72
72
  * 컴포넌트 타입 이름. **저장되는 식별자다.**
73
73
  *
74
- * 배치된 보드가 이 이름으로 타입을 찾는다. 바꾸면 그 보드는 타입을 못 찾아
74
+ * 배치된 도면이 이 이름으로 타입을 찾는다. 바꾸면 그 도면은 타입을 못 찾아
75
75
  * **오류 없이 빈 화면**이 된다. 그래서 만들 때 정하고 잠근다 — 고칠 수 있는 것은
76
76
  * 표시 이름(`name`)뿐이다. 수정 리졸버가 이 필드를 받지 않는다.
77
77
  *