@things-factory/figure-service 10.1.47 → 10.1.49
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/dist-server/service/figure/figure-format.js +12 -0
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +46 -1
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/figure/figure-format.test.ts +16 -0
- package/server/service/figure/figure-format.ts +12 -0
- package/server/service/figure/figure-tools.test.ts +39 -1
- package/server/service/figure/figure-tools.ts +49 -1
- package/translations/en.json +3 -0
- package/translations/ko.json +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.49",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@hatiolab/figure-model": "^0.1.
|
|
26
|
+
"@hatiolab/figure-model": "^0.1.55",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
28
|
"@things-factory/ai-client-base": "^10.1.39",
|
|
29
29
|
"@things-factory/auth-base": "^10.1.47",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"graphql-upload": "^15.0.2",
|
|
34
34
|
"koa-router": "^7.4.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "f9b29dcbdfecbfdd72e33bbcf74aea05331da75f"
|
|
37
37
|
}
|
|
@@ -276,6 +276,22 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
276
276
|
expect(`${finding!.message}${finding!.why}${finding!.how}`).not.toMatch(/come away|piece/)
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
+
/* 한 비율로 커진다고 선언한 도형에서 제 숫자를 가진 부품(수석 아키텍트 판정 2026-09-24 조건 ②). */
|
|
280
|
+
it('한 비율로 커진다고 선언한 도형에서 따라 커지지 않는 부품을 막고 이름을 댄다', () => {
|
|
281
|
+
let asset = createV3Asset('Uniform')
|
|
282
|
+
asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
283
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
284
|
+
asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
285
|
+
asset.designInputs[`size.${axis}`] = 100
|
|
286
|
+
}
|
|
287
|
+
asset = applyV3Authoring(asset, { kind: 'scale-uniformly' } as never)
|
|
288
|
+
const finding = inspectAnySource(serializeV3Asset(asset), null, translatorOf(KO)).findings.find(f => f.code === 'sizing-not-uniform')
|
|
289
|
+
|
|
290
|
+
expect(finding?.blocking).toBe(true)
|
|
291
|
+
expect(finding!.message).toBe("이 도형은 한 비율로 커지는데, 크기를 X 로 2배로 하면 'body' 부품은 같이 커지지 않습니다.")
|
|
292
|
+
expect(finding!.how).toContain('크기에 비례')
|
|
293
|
+
})
|
|
294
|
+
|
|
279
295
|
/* The server's translator is i18next with its defaults, which escape values for HTML. The panel draws text. */
|
|
280
296
|
it('부품 이름의 따옴표가 HTML 로 바뀌지 않는다 — 서버와 같은 i18next 로', async () => {
|
|
281
297
|
const i18next = require('i18next').createInstance()
|
|
@@ -177,6 +177,18 @@ function v3Wording(violation: V3Violation, say: Say, hasParts: boolean): Pick<Ga
|
|
|
177
177
|
how: say(key('how'), { parts }, `Fasten ${parts} to a face of the part they rest on, or press "fix here" under placement, so they stay put when the figure is resized.`)
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
+
case 'SIZING_NOT_UNIFORM': {
|
|
181
|
+
/* The figure declares it grows by one ratio; these parts keep a size or place of their own (ruling 2026-09-24). */
|
|
182
|
+
const resize = violation.resize!
|
|
183
|
+
const parts = (violation.parts ?? []).map(one => `'${one}'`).join(', ')
|
|
184
|
+
const axes = resize.axes.map(one => one.toUpperCase()).join('·')
|
|
185
|
+
const factor = resize.factor
|
|
186
|
+
return {
|
|
187
|
+
message: say(key('message'), { parts, axes, factor }, `The figure grows by one ratio, but at ${factor}x on ${axes} ${parts} do not grow with it.`),
|
|
188
|
+
why: say(key('why'), {}, 'A figure that grows by one ratio keeps its proportions at any size. A part with a size or place of its own comes apart from the rest.'),
|
|
189
|
+
how: say(key('how'), { parts }, `Let ${parts} follow the figure's size ("in proportion to size" under size and placement), or remove their own fixed numbers.`)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
@@ -79,7 +79,10 @@ function install() {
|
|
|
79
79
|
costOf: model.costOf,
|
|
80
80
|
scoreOf: model.scoreOf,
|
|
81
81
|
validate: model.validate,
|
|
82
|
-
isPlaceholderType: model.isPlaceholderType
|
|
82
|
+
isPlaceholderType: model.isPlaceholderType,
|
|
83
|
+
parseAnySource: require('./figure-format').parseAnySource,
|
|
84
|
+
v3CostOf: model.v3CostOf,
|
|
85
|
+
v3ScoreOf: model.v3ScoreOf
|
|
83
86
|
})
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -505,3 +508,38 @@ describe('후보가 카드가 된다', () => {
|
|
|
505
508
|
expect(proposed[0].refine).toBe(false)
|
|
506
509
|
})
|
|
507
510
|
})
|
|
511
|
+
|
|
512
|
+
/*
|
|
513
|
+
V3 도형이 열려 있을 때(사용자 2026-09-24: 도크가 V3 초안에 「type 이 없다 · base 가 없다」 로 답했다). 읽기는 V3 로 읽고
|
|
514
|
+
잰다. 후보 제안은 아직 V2 만 쓰므로 지어내지 않고 그렇다고 말한다.
|
|
515
|
+
*/
|
|
516
|
+
describe('V3 도형이 열려 있을 때', () => {
|
|
517
|
+
const v3Draft = () => {
|
|
518
|
+
const model = require('@hatiolab/figure-model')
|
|
519
|
+
let asset = model.createV3Asset('Probe')
|
|
520
|
+
asset = model.addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
521
|
+
return asset
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
it('저장 전 V3 초안을 V3 로 읽고, V3 셈으로 잰다', async () => {
|
|
525
|
+
const draft = v3Draft()
|
|
526
|
+
const got = await toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: draft, palette: ['palette.primary'] }))
|
|
527
|
+
expect(got.format).toBe('v3')
|
|
528
|
+
expect(got.draft).toBe(true)
|
|
529
|
+
expect(got.cost.triangles).toBe(12)
|
|
530
|
+
expect(got.score.grade).toBe('A')
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('형식을 어긴 V3 초안은 V3 의 말로 거절한다 — V2 파서의 말이 아니다', async () => {
|
|
534
|
+
const broken = { ...v3Draft(), appearance: [] }
|
|
535
|
+
await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken, palette: ['palette.primary'] }))).rejects.toThrow(/V3 draft/)
|
|
536
|
+
await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken, palette: ['palette.primary'] }))).rejects.not.toThrow(/base 가 없다/)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('후보 제안은 V3 도형에 V2 후보를 지어내지 않고, 아직 못 한다고 말한다', async () => {
|
|
540
|
+
const got = await toolNamed('proposeFigureCandidate').builder({ prompt: '다리를 길게' }, ctx({ figureId: 'fig-1', draftSource: v3Draft(), palette: ['palette.primary'] }))
|
|
541
|
+
expect(got.error).toBe('V3_PROPOSAL_UNAVAILABLE')
|
|
542
|
+
expect(proposed).toHaveLength(0)
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
@@ -62,6 +62,9 @@ function getDeps(): any {
|
|
|
62
62
|
complaint. The tool's own contract is unchanged: it reads, it never saves (ADR-0087).
|
|
63
63
|
*/
|
|
64
64
|
inspectSource: format.inspectAnySource,
|
|
65
|
+
parseAnySource: format.parseAnySource,
|
|
66
|
+
v3CostOf: model.v3CostOf,
|
|
67
|
+
v3ScoreOf: model.v3ScoreOf,
|
|
65
68
|
proposeFigure: propose.proposeFigure,
|
|
66
69
|
compile: model.compile,
|
|
67
70
|
costOf: model.costOf,
|
|
@@ -153,6 +156,23 @@ async function resolveFigureTarget(figureId: string | undefined, domainId: strin
|
|
|
153
156
|
* 찾은 뒤에만 보며, Figure의 불변 type과 형식 검증을 다시 확인한다. 검증되지 않은 큰 객체를 LLM
|
|
154
157
|
* 프롬프트로 옮기면 비용만 태우고, DB에 있던 Figure와 다른 것을 고친다고 잘못 말할 수 있다.
|
|
155
158
|
*/
|
|
159
|
+
/** A V3 asset names its format in `version`; a V2 FigureSource carries a number there. */
|
|
160
|
+
const isV3 = (source: any) => source?.version === 'figure-v3-asset-1'
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The V2 proposer writes V2 figures; a V3 figure open in the modeller is not something it can revise or add to. Said as
|
|
164
|
+
* what is true, with what the assistant can do instead -- not a V2 parser's complaint about a V3 asset (user,
|
|
165
|
+
* 2026-09-24: the dock answered 「type 이 없다 · base 가 없다」 for a V3 draft).
|
|
166
|
+
*/
|
|
167
|
+
function v3ProposalUnavailable() {
|
|
168
|
+
return {
|
|
169
|
+
error: 'V3_PROPOSAL_UNAVAILABLE',
|
|
170
|
+
message: '이 도형은 V3 형식입니다. AI 후보 제안은 아직 V2 도형만 만들 수 있어 이 도형을 고치는 후보를 낼 수 없습니다.',
|
|
171
|
+
nextStep:
|
|
172
|
+
'Tell the person plainly that candidate proposals do not support V3 figures yet. You can still read the figure (getFigureSource) and explain whether it may be released and why (inspectFigure). Do not produce a V2 candidate for a V3 figure.'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
156
176
|
function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
|
|
157
177
|
const isMatch = host?.figureId === row.id || (!row.id && host?.editorMode === 'new') || (!row.id && !host?.figureId)
|
|
158
178
|
const draft = isMatch ? host?.draftSource : undefined
|
|
@@ -169,6 +189,17 @@ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
|
|
|
169
189
|
throw new Error('the unsaved draft is too large to safely review')
|
|
170
190
|
}
|
|
171
191
|
|
|
192
|
+
/* A V3 draft is checked as V3 -- the same door a save goes through. It names no type; the row does. */
|
|
193
|
+
if (isV3(draft)) {
|
|
194
|
+
try {
|
|
195
|
+
getDeps().parseAnySource(serialized)
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
const findings = Array.isArray(e?.findings) ? e.findings.map((one: any) => one.message).join(' · ') : String(e?.message ?? e)
|
|
198
|
+
throw new Error(`the unsaved V3 draft is invalid: ${findings}`)
|
|
199
|
+
}
|
|
200
|
+
return { source: draft, draft: true }
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
const { validate, isPlaceholderType } = getDeps()
|
|
173
204
|
const checked = validate(draft)
|
|
174
205
|
if (checked.errors.length > 0) {
|
|
@@ -306,6 +337,22 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
306
337
|
return { id: row.id, type: row.type, name: row.name, source: null, cost: null }
|
|
307
338
|
}
|
|
308
339
|
|
|
340
|
+
if (isV3(current.source)) {
|
|
341
|
+
/* A V3 asset is measured as V3 -- triangles, draw groups, and its grade against one screen. */
|
|
342
|
+
const { v3CostOf, v3ScoreOf } = getDeps()
|
|
343
|
+
return {
|
|
344
|
+
id: row.id,
|
|
345
|
+
type: row.type,
|
|
346
|
+
name: row.name,
|
|
347
|
+
state: row.state,
|
|
348
|
+
format: 'v3',
|
|
349
|
+
source: current.source,
|
|
350
|
+
draft: current.draft,
|
|
351
|
+
cost: v3CostOf(current.source),
|
|
352
|
+
score: v3ScoreOf(current.source) ?? null
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
309
356
|
const { compile, costOf, scoreOf } = getDeps()
|
|
310
357
|
const blueprint = compile(current.source)
|
|
311
358
|
|
|
@@ -339,7 +386,7 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
339
386
|
|
|
340
387
|
const current = sourceForTool(row, ctx?.host)
|
|
341
388
|
const { inspectSource } = getDeps()
|
|
342
|
-
return { ...inspectSource(current.source ? JSON.stringify(current.source) : null), draft: current.draft }
|
|
389
|
+
return { ...inspectSource(current.source ? JSON.stringify(current.source) : null, null, undefined, row.type), draft: current.draft }
|
|
343
390
|
}
|
|
344
391
|
},
|
|
345
392
|
{
|
|
@@ -391,6 +438,7 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
391
438
|
base = sourceForTool(row, ctx?.host).source ?? undefined
|
|
392
439
|
}
|
|
393
440
|
}
|
|
441
|
+
if (isV3(base) || (args?.create !== true && isV3(ctx?.host?.draftSource))) return v3ProposalUnavailable()
|
|
394
442
|
|
|
395
443
|
const palette = Array.isArray(ctx?.host?.palette) ? ctx.host.palette : []
|
|
396
444
|
if (palette.length === 0) {
|
package/translations/en.json
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"figure.gate.parts-split-on-resize.how": "Fasten {parts} to a face of the part they rest on, or press “fix here” under placement, so they stay put when the figure is resized.",
|
|
15
15
|
"figure.gate.parts-split-on-resize.message": "At {factor}x on {axes}, {parts} come away from the rest of the figure.",
|
|
16
16
|
"figure.gate.parts-split-on-resize.why": "A figure placed at another size shows those parts hanging in the air — to a person, it looks broken.",
|
|
17
|
+
"figure.gate.sizing-not-uniform.how": "Let {parts} follow the figure's size (“in proportion to size” under size and placement), or remove their own fixed numbers.",
|
|
18
|
+
"figure.gate.sizing-not-uniform.message": "The figure grows by one ratio, but at {factor}x on {axes} {parts} do not grow with it.",
|
|
19
|
+
"figure.gate.sizing-not-uniform.why": "A figure that grows by one ratio keeps its proportions at any size. A part with a size or place of its own comes apart from the rest.",
|
|
17
20
|
"figure.gate.placement-face-tilted.how": "Choose a mounting face parallel to the plane, or turn '{part}' upright.",
|
|
18
21
|
"figure.gate.placement-face-tilted.message": "The mounting face of '{part}' is tilted {degrees}° from the plane the figure stands on.",
|
|
19
22
|
"figure.gate.placement-face-tilted.why": "A tilted face cannot lie on the plane, so how the figure stands is not settled.",
|
package/translations/ko.json
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"figure.gate.parts-split-on-resize.how": "{parts} 부품을 올려 둔 부품의 면에 붙이거나, 부품의 「위치와 크기」에서 「여기에 고정」을 누르면 크기를 바꿔도 제자리에 있습니다.",
|
|
15
15
|
"figure.gate.parts-split-on-resize.message": "크기를 {axes} 로 {factor}배로 하면 {parts} 부품이 나머지에서 떨어집니다.",
|
|
16
16
|
"figure.gate.parts-split-on-resize.why": "다른 크기로 놓으면 그 부품이 공중에 떠 보입니다. 보는 사람에게는 부서진 도형입니다.",
|
|
17
|
+
"figure.gate.sizing-not-uniform.how": "{parts} 부품이 도형 크기를 따르게 하세요(「위치와 크기」의 「크기에 비례」). 제 고정 숫자를 가진 부품은 한 비율로 커지지 않습니다.",
|
|
18
|
+
"figure.gate.sizing-not-uniform.message": "이 도형은 한 비율로 커지는데, 크기를 {axes} 로 {factor}배로 하면 {parts} 부품은 같이 커지지 않습니다.",
|
|
19
|
+
"figure.gate.sizing-not-uniform.why": "한 비율로 커지는 도형은 어떤 크기에서도 비례를 지킵니다. 제 크기나 자리를 따로 가진 부품은 나머지와 어긋납니다.",
|
|
17
20
|
"figure.gate.placement-face-tilted.how": "평면과 나란한 면을 장착면으로 다시 정하거나, '{part}' 부품을 바로 세우세요.",
|
|
18
21
|
"figure.gate.placement-face-tilted.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {degrees}° 기울어 있습니다.",
|
|
19
22
|
"figure.gate.placement-face-tilted.why": "기운 면은 평면에 닿을 수 없어서, 도형이 어떤 자세로 놓이는지 정해지지 않습니다.",
|