@things-factory/figure-service 10.1.206 → 10.1.208
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-context.js +0 -2
- package/dist-server/service/figure/figure-context.js.map +1 -1
- package/dist-server/service/figure/figure-format.d.ts +2 -2
- package/dist-server/service/figure/figure-format.js +11 -66
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-propose-steps.d.ts +1 -1
- package/dist-server/service/figure/figure-propose-steps.js +3 -5
- package/dist-server/service/figure/figure-propose-steps.js.map +1 -1
- package/dist-server/service/figure/figure-type.js +1 -1
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/service/figure/figure.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/scripts/templates.test.mts +2 -3
- package/server/service/figure/figure-category-db.test.ts +2 -2
- package/server/service/figure/figure-context.test.ts +1 -1
- package/server/service/figure/figure-context.ts +0 -2
- package/server/service/figure/figure-deprecation-db.test.ts +2 -2
- package/server/service/figure/figure-format.test.ts +25 -105
- package/server/service/figure/figure-format.ts +14 -69
- package/server/service/figure/figure-inspect-db.test.ts +6 -5
- package/server/service/figure/figure-llm-smoke.test.ts +1 -1
- package/server/service/figure/figure-namespace-db.test.ts +2 -2
- package/server/service/figure/figure-propose-steps.test.ts +15 -20
- package/server/service/figure/figure-propose-steps.ts +3 -5
- package/server/service/figure/figure-public-db.test.ts +2 -2
- package/server/service/figure/figure-release-check.test.ts +2 -2
- package/server/service/figure/figure-release-record-db.test.ts +7 -6
- package/server/service/figure/figure-release-size-db.test.ts +2 -2
- package/server/service/figure/figure-tools.test.ts +3 -3
- package/server/service/figure/figure-type.ts +1 -1
- package/server/service/figure/figure.ts +1 -1
- package/translations/en.json +4 -16
- package/translations/es.json +4 -16
- package/translations/ja.json +4 -16
- package/translations/ko.json +10 -22
- package/translations/ms.json +4 -16
- package/translations/zh.json +4 -16
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.208",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"test:templates": "node --test scripts/templates.test.mts"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@hatiolab/figure-model": "^0.
|
|
28
|
+
"@hatiolab/figure-model": "^0.10.0",
|
|
29
29
|
"@operato/typeorm-history": "^10.0.0",
|
|
30
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
31
|
-
"@things-factory/attachment-base": "^10.1.
|
|
32
|
-
"@things-factory/auth-base": "^10.1.
|
|
30
|
+
"@things-factory/ai-client-base": "^10.1.208",
|
|
31
|
+
"@things-factory/attachment-base": "^10.1.208",
|
|
32
|
+
"@things-factory/auth-base": "^10.1.208",
|
|
33
33
|
"@things-factory/env": "^10.1.149",
|
|
34
|
-
"@things-factory/setting-base": "^10.1.
|
|
35
|
-
"@things-factory/shell": "^10.1.
|
|
34
|
+
"@things-factory/setting-base": "^10.1.208",
|
|
35
|
+
"@things-factory/shell": "^10.1.208",
|
|
36
36
|
"graphql-scalars": "^1.22.4",
|
|
37
37
|
"graphql-upload": "^15.0.2",
|
|
38
38
|
"jszip": "^3.10.1",
|
|
39
39
|
"koa-router": "^7.4.0"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "ffc1ceaa9471b9f4a9b86f46c3021f2fed72d17d"
|
|
42
42
|
}
|
|
@@ -12,7 +12,7 @@ import assert from 'node:assert/strict'
|
|
|
12
12
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { applyFigureRecipe, checkFigureRecipe, compileFigureAsset, compileFigureDocument, inspectFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
16
16
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
17
17
|
|
|
18
18
|
import { buildTemplate, modelFileOf, RECIPES } from './build-templates.mts'
|
|
@@ -50,8 +50,7 @@ for (const name of names) {
|
|
|
50
50
|
const { models } = await buildTemplate(name)
|
|
51
51
|
for (const { values, asset } of models) {
|
|
52
52
|
const at = JSON.stringify(values)
|
|
53
|
-
|
|
54
|
-
assert.deepEqual(inspectFigureAsset(declared).violations, [], `${at}: the release gate`)
|
|
53
|
+
assert.deepEqual(inspectFigureAsset(asset).violations, [], `${at}: the release gate`)
|
|
55
54
|
for (const k of [0.5, 2]) assert.doesNotThrow(() => compileFigureAsset(withHost(asset, k)).evaluate(), `${at}: host × ${k}`)
|
|
56
55
|
for (const input of asset.document.model.inputs.filter(i => i.role === 'state'))
|
|
57
56
|
for (const v of [input.min!, input.max!]) assert.doesNotThrow(() => compileFigureAsset(asset).evaluate({ [input.id]: v }), `${at}: ${input.id} at ${v}`)
|
|
@@ -8,7 +8,7 @@ const { DataSource: TypeOrmDataSource } = require('typeorm')
|
|
|
8
8
|
const shell = require('@things-factory/shell')
|
|
9
9
|
const authBase = require('@things-factory/auth-base')
|
|
10
10
|
const settingBase = require('@things-factory/setting-base')
|
|
11
|
-
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring
|
|
11
|
+
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring } = require('@hatiolab/figure-model')
|
|
12
12
|
|
|
13
13
|
let dataSource: DataSource | null = null
|
|
14
14
|
let Figure: any
|
|
@@ -60,7 +60,7 @@ afterEach(() => categories.clearFigureCategories())
|
|
|
60
60
|
const releasable = () => {
|
|
61
61
|
let a = addFigureBox(createFigureAsset('Mixer'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
62
62
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
63
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
63
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
64
64
|
}
|
|
65
65
|
let seq = 0
|
|
66
66
|
async function context() {
|
|
@@ -196,7 +196,7 @@ describe('a proposal changes only what it was shown', () => {
|
|
|
196
196
|
{ kind: 'add-part', part: { id: 'lamp', kind: 'sphere', dimensions: { radius: 40, radiusY: 40 } } },
|
|
197
197
|
{ kind: 'attach', part: 'lamp', to: { part: 'shelf-9', face: 'top' } },
|
|
198
198
|
{ kind: 'link-dimension', part: 'lamp', axis: 'x', source: { of: 'part', part: 'shelf-7', axis: 'x' }, times: 0.1 },
|
|
199
|
-
{ kind: '
|
|
199
|
+
{ kind: 'set-placement', placement: 'floor' }
|
|
200
200
|
]
|
|
201
201
|
expect(unreadPartEdits(over, steps as never, shown)).toEqual([])
|
|
202
202
|
})
|
|
@@ -323,7 +323,6 @@ type Subject = string | undefined
|
|
|
323
323
|
*/
|
|
324
324
|
const SUBJECT: { [K in FigureProposalStep['kind']]: (step: StepOf<K>, base: FigureAsset) => Subject } = {
|
|
325
325
|
'add-part': () => undefined,
|
|
326
|
-
'fit-room': () => undefined,
|
|
327
326
|
'set-section-corner': step => step.part,
|
|
328
327
|
'set-cavity': step => step.part,
|
|
329
328
|
'set-section': step => step.part,
|
|
@@ -350,7 +349,6 @@ const SUBJECT: { [K in FigureProposalStep['kind']]: (step: StepOf<K>, base: Figu
|
|
|
350
349
|
'duplicate-part': step => step.target,
|
|
351
350
|
'move-part': step => step.target,
|
|
352
351
|
'set-placement': () => undefined,
|
|
353
|
-
'declare-occupancy': () => undefined,
|
|
354
352
|
'declare-instance-size': () => undefined,
|
|
355
353
|
'clear-mounting-face': () => undefined,
|
|
356
354
|
'scale-uniformly': () => undefined,
|
|
@@ -8,7 +8,7 @@ const { DataSource: TypeOrmDataSource } = require('typeorm')
|
|
|
8
8
|
const shell = require('@things-factory/shell')
|
|
9
9
|
const authBase = require('@things-factory/auth-base')
|
|
10
10
|
const settingBase = require('@things-factory/setting-base')
|
|
11
|
-
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring
|
|
11
|
+
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring } = require('@hatiolab/figure-model')
|
|
12
12
|
|
|
13
13
|
let FigureNamespace: any
|
|
14
14
|
let dataSource: DataSource | null = null
|
|
@@ -60,7 +60,7 @@ async function namespaced(domain: any) {
|
|
|
60
60
|
const releasable = () => {
|
|
61
61
|
let a = addFigureBox(createFigureAsset('Mixer'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
62
62
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
63
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
63
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
let seq = 0
|
|
@@ -6,22 +6,17 @@
|
|
|
6
6
|
* - a source that does not name the format is refused -- what cannot be identified is not stored;
|
|
7
7
|
* - the budget columns (`triangles`, `groups`) and the score are the kernel's.
|
|
8
8
|
*/
|
|
9
|
-
import { createFigureAsset, addFigureBox, addFigurePrimitive, serializeFigureAsset, applyFigureAuthoring,
|
|
9
|
+
import { createFigureAsset, addFigureBox, addFigurePrimitive, serializeFigureAsset, applyFigureAuthoring, placeholderType, figureScoreOf, inspectFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { FigureSourceError, consumablePayloadOf, inspectSource, parseSource, releaseRecordOf } from './figure-format'
|
|
13
13
|
|
|
14
|
-
/** 온전한 자산 하나: 바닥에 선
|
|
14
|
+
/** 온전한 자산 하나: 바닥에 선 상자. */
|
|
15
15
|
function figureAsset(): FigureAsset {
|
|
16
16
|
let asset = createFigureAsset('Mixer')
|
|
17
17
|
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
18
18
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
19
|
-
|
|
20
|
-
kind: 'declare-occupancy',
|
|
21
|
-
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
22
|
-
placement: 'floor'
|
|
23
|
-
})
|
|
24
|
-
return asset
|
|
19
|
+
return applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' })
|
|
25
20
|
}
|
|
26
21
|
const figureSource = () => serializeFigureAsset(figureAsset())
|
|
27
22
|
|
|
@@ -68,29 +63,13 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
68
63
|
})
|
|
69
64
|
|
|
70
65
|
describe('발행 판정', () => {
|
|
71
|
-
it('
|
|
66
|
+
it('온전한 자산은 막지 않는다', () => {
|
|
72
67
|
const inspection = inspectSource(figureSource())
|
|
73
68
|
expect(inspection.blocked).toBe(false)
|
|
74
69
|
})
|
|
75
70
|
|
|
76
|
-
it('
|
|
77
|
-
|
|
78
|
-
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
79
|
-
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
80
|
-
expect(inspection.blocked).toBe(true)
|
|
81
|
-
expect(inspection.findings.some(f => f.code === 'part-outside-occupancy' && f.at === 'arm')).toBe(true)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('부피를 선언하지 않은 자산은 막는다', () => {
|
|
85
|
-
let asset = createFigureAsset('Bare')
|
|
86
|
-
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
87
|
-
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
88
|
-
expect(inspection.blocked).toBe(true)
|
|
89
|
-
expect(inspection.findings.some(f => f.code === 'occupancy-undeclared')).toBe(true)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('전 범위를 증명하지 못한 판정은 막지 않고 그 사실을 말한다', () => {
|
|
93
|
-
/* 상태에 따라 도는 부품은 모서리만으로 증명되지 않는다 — 표본 검사였음을 발행 화면이 읽는다. */
|
|
71
|
+
it('표본으로 검사한 판정에도 다시 저작하라는 말은 없다', () => {
|
|
72
|
+
/* 상태에 따라 도는 부품은 표본으로 검사된다. 그 사이에서 넘을 선언된 공간이 없으니 화면이 할 말도 없다(ADR-0145). */
|
|
94
73
|
let asset = figureAsset()
|
|
95
74
|
asset = addFigurePrimitive(asset, { id: 'wheel', kind: 'cylinder', dimensions: { radius: 20, length: 20 }, position: { x: 0, y: 20, z: 0 } })
|
|
96
75
|
const m = asset.document.model as any
|
|
@@ -108,19 +87,11 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
108
87
|
pose.params = { ...pose.params, to: 'wheel.spun' }
|
|
109
88
|
m.nodes.push({ id: 'wheel.world', op: 'compose@1', args: ['wheel.turn.p', pose.outputs.pose], outputs: { pose: 'wheel.wp' } })
|
|
110
89
|
m.nodes.find((n: any) => n.id === 'wheel').args = [m.nodes.find((n: any) => n.id === 'wheel.shape').outputs.shape, 'wheel.wp']
|
|
111
|
-
asset = applyFigureAuthoring(asset, {
|
|
112
|
-
kind: 'declare-occupancy',
|
|
113
|
-
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
114
|
-
placement: 'floor'
|
|
115
|
-
})
|
|
116
90
|
|
|
91
|
+
expect(inspectFigureAsset(asset).coverage.method).toBe('sampled')
|
|
117
92
|
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
118
|
-
|
|
119
|
-
expect(
|
|
120
|
-
expect(sampled!.blocking).toBe(false)
|
|
121
|
-
/* 증명을 막은 움직임은 사람이 붙인 이름으로 — 연산 이름과 영어 설명은 화면에 나가지 않는다. */
|
|
122
|
-
expect(sampled!.at).toBe('wheel')
|
|
123
|
-
expect(`${sampled!.at}${sampled!.why}`).not.toMatch(/axis-turn@1|state-dependent|[a-z]+@\d/)
|
|
93
|
+
expect(inspection.blocked).toBe(false)
|
|
94
|
+
expect(inspection.findings).toEqual([])
|
|
124
95
|
})
|
|
125
96
|
|
|
126
97
|
it('정본이 없으면 전과 같이 그 말로 막는다', () => {
|
|
@@ -130,7 +101,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
130
101
|
it('바뀐 것 없는 재발행은 아무 말도 없이 지난다', () => {
|
|
131
102
|
const again = inspectSource(figureSource(), figureSource())
|
|
132
103
|
expect(again.blocked).toBe(false)
|
|
133
|
-
expect(again.findings
|
|
104
|
+
expect(again.findings).toEqual([])
|
|
134
105
|
})
|
|
135
106
|
|
|
136
107
|
it('놓인 인스턴스가 기대던 것을 없앤 재발행은 막는다 — 없어진 능력을 이름으로 댄다', () => {
|
|
@@ -163,28 +134,6 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
163
134
|
return text.replace(/\{(\w+)\}/g, (_, name) => String(options[name] ?? `{${name}}`))
|
|
164
135
|
}
|
|
165
136
|
|
|
166
|
-
const outside = () => {
|
|
167
|
-
let asset = figureAsset()
|
|
168
|
-
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
169
|
-
return serializeFigureAsset(asset)
|
|
170
|
-
}
|
|
171
|
-
const bare = () => {
|
|
172
|
-
let asset = createFigureAsset('Bare')
|
|
173
|
-
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
174
|
-
return serializeFigureAsset(asset)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
it('넘은 부품은 이름 · 축 · 넘은 양을 화면 말투로 말하고, 왜와 어떻게를 함께 낸다', () => {
|
|
178
|
-
const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
179
|
-
|
|
180
|
-
expect(finding.message).toMatch(/^도형이 차지하는 공간을 'arm' 부품이 X 로 [\d.]+mm 넘습니다\.$/)
|
|
181
|
-
expect(finding.why).toMatch(/니다\.$/)
|
|
182
|
-
expect(finding.how).toContain('부품에 맞추기')
|
|
183
|
-
expect(finding.how).toMatch(/세요\.$/)
|
|
184
|
-
/* 영어 로그 문장이 화면으로 새지 않는다. */
|
|
185
|
-
expect(`${finding.message}${finding.why}${finding.how}`).not.toMatch(/declared volume|leaves/)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
137
|
it('확장 속성의 키가 없어지면 발행을 막고 키 이름으로 말한다; 값이 바뀌면 알리기만 한다 (ADR-0123)', () => {
|
|
189
138
|
const carrier = { type: 'ROLL', dimensions: { width: 1000, length: 600, height: 200 } }
|
|
190
139
|
const withKey = applyFigureAuthoring(figureAsset(), { kind: 'set-extension', key: 'fmsim/carrier', value: carrier })
|
|
@@ -203,26 +152,9 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
203
152
|
})
|
|
204
153
|
|
|
205
154
|
/* 첫 15분 다시 재기(10-16 저녁): 판정 문구만 옛 말 「공간 선언」을 썼다. 화면 말은 ADR-0097 §9 Q2 A 의 표다. */
|
|
206
|
-
it('판정 문구는 화면 말을 쓴다 —
|
|
155
|
+
it('판정 문구는 화면 말을 쓴다 — 「공간 선언」은 쓰지 않는다', () => {
|
|
207
156
|
const said = Object.entries(KO).filter(([key]) => key.startsWith('figure.gate.'))
|
|
208
157
|
expect(said.filter(([, text]) => /공간 선언|공간을[^.]*선언/.test(text)).map(([key]) => key)).toEqual([])
|
|
209
|
-
const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
210
|
-
expect(`${finding.message}${finding.why}${finding.how}`).toContain('차지하는 공간')
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('차지하는 공간을 정하지 않은 도형은 무엇을 누르면 되는지 말한다', () => {
|
|
214
|
-
const finding = inspectSource(bare(), null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
|
|
215
|
-
|
|
216
|
-
expect(finding.message).toBe('이 도형은 차지하는 공간을 아직 정하지 않았습니다.')
|
|
217
|
-
expect(finding.how).toContain('부품에 맞추기')
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('부품이 없는 새 도형에는 도형을 먼저 추가하라고 한다 — 화면은 그때 맞추기 단추를 내지 않는다', () => {
|
|
221
|
-
const empty = serializeFigureAsset(createFigureAsset('Empty'))
|
|
222
|
-
const finding = inspectSource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
|
|
223
|
-
|
|
224
|
-
expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 차지하는 공간을 정하세요.')
|
|
225
|
-
expect(inspectSource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
|
|
226
158
|
})
|
|
227
159
|
|
|
228
160
|
/*
|
|
@@ -295,9 +227,9 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
295
227
|
})
|
|
296
228
|
|
|
297
229
|
it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
|
|
298
|
-
const finding = inspectSource(
|
|
230
|
+
const finding = inspectSource(splitting()).findings.find(f => f.code === 'parts-split-on-resize')!
|
|
299
231
|
|
|
300
|
-
expect(finding.message).
|
|
232
|
+
expect(finding.message).toBe("At 2x on Y, 'lamp' come away from the rest of the figure.")
|
|
301
233
|
expect(finding.message).not.toContain('figure.gate.')
|
|
302
234
|
})
|
|
303
235
|
|
|
@@ -321,8 +253,9 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
321
253
|
it('판정이 묻는 키는 ko · en 에 다 있다', () => {
|
|
322
254
|
const asked: string[] = []
|
|
323
255
|
const t = translatorOf(KO, asked)
|
|
324
|
-
inspectSource(
|
|
325
|
-
|
|
256
|
+
inspectSource(splitting(), null, t)
|
|
257
|
+
const raised = addFigureBox(createFigureAsset('Raised'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 150, z: 0 } })
|
|
258
|
+
inspectSource(serializeFigureAsset(applyFigureAuthoring(raised, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })), null, t)
|
|
326
259
|
inspectSource(figureSource(), figureSource(), t)
|
|
327
260
|
inspectSource(figureSource(), null, t, 'FIGURE_ABCDEF12')
|
|
328
261
|
|
|
@@ -352,22 +285,23 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
352
285
|
let asset = createFigureAsset('Mixer')
|
|
353
286
|
asset = addFigureBox(asset, { id: 'body', size: { x: 300, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
354
287
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
355
|
-
return serializeFigureAsset(applyFigureAuthoring(asset, { kind: '
|
|
288
|
+
return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' }))
|
|
356
289
|
}
|
|
357
290
|
it('a first release: its format, and nothing changed', () => {
|
|
358
291
|
const record = releaseRecordOf(serializeFigureAsset(figureAsset()))
|
|
359
292
|
expect([record.format, record.changes]).toEqual(['figure-asset-1', []])
|
|
360
|
-
/* Not sized from the instance: nothing measured across sizes, only
|
|
361
|
-
|
|
293
|
+
/* Not sized from the instance: nothing measured across sizes, only its body and the room it sweeps. Its plan is its one body from above. */
|
|
294
|
+
const box = { x: { min: -50, max: 50 }, y: { min: 0, max: 100 }, z: { min: -50, max: 50 } }
|
|
295
|
+
expect(record.metrics).toEqual({ body: box, motionRange: box })
|
|
362
296
|
expect(record.topView.shapes.map(s => s.id)).toEqual(['body'])
|
|
363
297
|
expect(record.topView.size).toBeNull()
|
|
364
298
|
})
|
|
365
|
-
it('the same again changes nothing; a wider
|
|
299
|
+
it('the same again changes nothing; a wider body is said and does not block', () => {
|
|
366
300
|
const v1 = serializeFigureAsset(figureAsset())
|
|
367
301
|
expect(releaseRecordOf(v1, v1).changes).toEqual([])
|
|
368
|
-
const
|
|
369
|
-
expect(
|
|
370
|
-
expect(String(
|
|
302
|
+
const size = releaseRecordOf(wider(), v1).changes!.find(c => c.code === 'size-changed')!
|
|
303
|
+
expect(size.blocking).toBe(false)
|
|
304
|
+
expect(String(size.values.after)).toMatch(/x -150…150/)
|
|
371
305
|
})
|
|
372
306
|
it('a last release that cannot be read is "not compared" (null), never "nothing changed" ([])', () => {
|
|
373
307
|
expect(releaseRecordOf(serializeFigureAsset(figureAsset()), '{ not json').changes).toBeNull()
|
|
@@ -375,7 +309,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
375
309
|
it('every new change has its sentence in both languages', () => {
|
|
376
310
|
const KO = require('../../../translations/ko.json') as Record<string, string>
|
|
377
311
|
const EN = require('../../../translations/en.json') as Record<string, string>
|
|
378
|
-
for (const code of ['
|
|
312
|
+
for (const code of ['size-changed', 'motion-range-changed', 'contact-face-changed', 'public-input-gone', 'public-input-range-narrowed', 'public-input-added', 'state-input-joints-changed', 'joint-axis-changed', 'joint-motion-unit-changed', 'joint-child-changed', 'joint-list-unread', 'extension-gone', 'extension-changed', 'extension-added'])
|
|
379
313
|
for (const dict of [KO, EN]) for (const field of ['message', 'why']) expect(dict[`figure.gate.contract.${code}.${field}`]).toBeTruthy()
|
|
380
314
|
})
|
|
381
315
|
it('the data changes have their sentences under figure-model 0.6.6\'s codes, in all six languages', () => {
|
|
@@ -417,7 +351,6 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
417
351
|
})
|
|
418
352
|
it('a figure that names its mounting face is checked on that face, not told twice', () => {
|
|
419
353
|
let a = conveyor(100)
|
|
420
|
-
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
|
|
421
354
|
a = applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'leg', face: 'bottom' })
|
|
422
355
|
const found = inspectSource(serializeFigureAsset(a), null, undefined, 'CONVEYOR').findings
|
|
423
356
|
expect(found.map(f => f.code)).not.toContain('off-mounting-plane')
|
|
@@ -431,17 +364,4 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
431
364
|
}
|
|
432
365
|
})
|
|
433
366
|
})
|
|
434
|
-
|
|
435
|
-
/* ADR-0104 decision 4: off the middle of its room is said at release, never refused. */
|
|
436
|
-
it('an origin off the middle of the room is a warning, and the release goes on', () => {
|
|
437
|
-
let asset = createFigureAsset('Door')
|
|
438
|
-
asset = addFigureBox(asset, { id: 'leaf', size: { x: 100, y: 200, z: 40 }, position: { x: 50, y: 100, z: 0 } })
|
|
439
|
-
asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset), placement: 'floor' })
|
|
440
|
-
const result = inspectSource(serializeFigureAsset(asset), null, undefined, 'DOOR')
|
|
441
|
-
const off = result.findings.find(f => f.code === 'origin-off-centre')!
|
|
442
|
-
expect(off.blocking).toBe(false)
|
|
443
|
-
expect(off.message).toMatch(/X 50 mm/)
|
|
444
|
-
expect(result.findings.filter(f => f.blocking).map(f => f.code)).not.toContain('origin-off-centre')
|
|
445
|
-
expect(inspectSource(serializeFigureAsset(figureAsset()), null, undefined, 'MIXER').findings.map(f => f.code)).not.toContain('origin-off-centre')
|
|
446
|
-
})
|
|
447
367
|
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`consumablePayloadOf`). The format is `figure-asset-1`, and the asset says so in its `version`; a source
|
|
5
5
|
* that does not is refused by name, because a source the server cannot identify is one it must not save.
|
|
6
6
|
*/
|
|
7
|
-
import { FIGURE_KERNEL_VERSION, compileFigureAsset, inspectFigureAsset, isPlaceholderType, parseFigureAsset, serializeFigureAsset, figureContractChanges, figureCostOf,
|
|
7
|
+
import { FIGURE_KERNEL_VERSION, compileFigureAsset, inspectFigureAsset, isPlaceholderType, parseFigureAsset, serializeFigureAsset, figureContractChanges, figureCostOf, figurePlaneGapOf, figureReleaseMetricsOf, figureScoreOf, figureTopViewOf, forgetRecordedInstanceSize, recordFigureInstanceSize } from '@hatiolab/figure-model'
|
|
8
8
|
import type { FigureAsset, FigureContractChange, FigureContractError, FigureReleaseMetrics, FigureTopView, FigureViolation } from '@hatiolab/figure-model'
|
|
9
9
|
|
|
10
10
|
import type { FigureFinding } from './figure-type.js'
|
|
@@ -62,8 +62,8 @@ export interface FigureReleaseCheckInput {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* A check an app adds to releasing (LV figure-server design G10): findings in the gate's own shape, blocking or said.
|
|
65
|
-
* The LV figure-server refuses an origin off the middle of the
|
|
66
|
-
*
|
|
65
|
+
* The LV figure-server refuses an origin off the middle of the floor, a receiving side's check (ADR-0104 decision 4),
|
|
66
|
+
* and says its carrier and budget advice.
|
|
67
67
|
*/
|
|
68
68
|
export type FigureReleaseCheck = (input: FigureReleaseCheckInput) => GateFinding[]
|
|
69
69
|
|
|
@@ -162,7 +162,7 @@ export type Translate = (key: string, options?: Record<string, unknown>) => stri
|
|
|
162
162
|
* The words are the modeller's (ADR-0097 §9 Q2 A): 「차지하는 공간」 for the volume a figure takes up, 「부품에 맞추기」 for
|
|
163
163
|
* setting it from the parts, and 「장착면」 for the face it stands on (`figure.text.mounting-face-explained`). A fact, then the way out.
|
|
164
164
|
*/
|
|
165
|
-
function figureWording(violation: FigureViolation, say: Say
|
|
165
|
+
function figureWording(violation: FigureViolation, say: Say): Pick<GateFinding, 'message' | 'why' | 'how'> {
|
|
166
166
|
const part = violation.part ?? ''
|
|
167
167
|
const axis = (violation.axis ?? '').toUpperCase()
|
|
168
168
|
const mm = violation.mm ?? 0
|
|
@@ -170,31 +170,6 @@ function figureWording(violation: FigureViolation, say: Say, hasParts: boolean):
|
|
|
170
170
|
const key = (field: string) => `figure.gate.${violation.code.toLowerCase().replace(/_/g, '-')}.${field}`
|
|
171
171
|
|
|
172
172
|
switch (violation.code) {
|
|
173
|
-
case 'PART_OUTSIDE_OCCUPANCY':
|
|
174
|
-
return {
|
|
175
|
-
message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the space the figure takes on ${axis}.`),
|
|
176
|
-
why: say(key('why'), {}, 'The space the figure takes sets the scale it is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),
|
|
177
|
-
how: say(key('how'), { part, axis, mm }, `Use "fit to parts" to fit the space the figure takes to the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)
|
|
178
|
-
}
|
|
179
|
-
case 'OCCUPANCY_UNDECLARED':
|
|
180
|
-
return violation.axis
|
|
181
|
-
? {
|
|
182
|
-
message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The space the figure takes is empty on ${axis}.`),
|
|
183
|
-
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
184
|
-
how: say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
185
|
-
}
|
|
186
|
-
: {
|
|
187
|
-
message: say(key('message'), {}, 'The space this figure takes is not set yet.'),
|
|
188
|
-
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
189
|
-
/*
|
|
190
|
-
A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —
|
|
191
|
-
the screen does not offer it. Telling the author to press it sent them looking for a button that was not
|
|
192
|
-
there (:3300, 2026-09-24).
|
|
193
|
-
*/
|
|
194
|
-
how: hasParts
|
|
195
|
-
? say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
196
|
-
: say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to set the space the figure takes from it.')
|
|
197
|
-
}
|
|
198
173
|
case 'PARTS_OFF_PLACEMENT_FACE':
|
|
199
174
|
return {
|
|
200
175
|
message: say(key('message'), { part, mm }, `The mounting face of '${part}' is ${mm} mm off the plane the figure stands on.`),
|
|
@@ -270,12 +245,9 @@ export function inspectSource(
|
|
|
270
245
|
|
|
271
246
|
let inspection
|
|
272
247
|
let asset: FigureAsset | undefined
|
|
273
|
-
let hasParts = true
|
|
274
248
|
try {
|
|
275
249
|
asset = parseFigureAsset(sourceJson)
|
|
276
250
|
inspection = inspectFigureAsset(asset)
|
|
277
|
-
const cost = figureCostOf(asset)
|
|
278
|
-
hasParts = cost.groups + cost.hidden > 0
|
|
279
251
|
} catch (e) {
|
|
280
252
|
const error = e as FigureContractError & { path?: string }
|
|
281
253
|
return {
|
|
@@ -287,7 +259,7 @@ export function inspectSource(
|
|
|
287
259
|
const say = sayerOf(t)
|
|
288
260
|
const findings: GateFinding[] = inspection.violations.map(violation => ({
|
|
289
261
|
code: violation.code.toLowerCase().replace(/_/g, '-'),
|
|
290
|
-
...figureWording(violation, say
|
|
262
|
+
...figureWording(violation, say),
|
|
291
263
|
at: violation.part,
|
|
292
264
|
blocking: true
|
|
293
265
|
}))
|
|
@@ -305,38 +277,6 @@ export function inspectSource(
|
|
|
305
277
|
blocking: true
|
|
306
278
|
})
|
|
307
279
|
}
|
|
308
|
-
if (inspection.coverage.method === 'sampled') {
|
|
309
|
-
const states = inspection.coverage.states
|
|
310
|
-
findings.push({
|
|
311
|
-
code: 'state-range-sampled',
|
|
312
|
-
/* Sizes as well as motion: the instance size range is swept for every figure (figure-model, designer 2026-09-27). */
|
|
313
|
-
message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of sizes and motion could not be proven, so ${states} combinations of size and pose were checked.`),
|
|
314
|
-
why: say('figure.gate.state-range-sampled.why', {}, 'Between the combinations that were checked, a part may still go past the space the figure takes.'),
|
|
315
|
-
how: say('figure.gate.state-range-sampled.how', {}, 'To cover the whole range, re-author what could not be proven.'),
|
|
316
|
-
/*
|
|
317
|
-
Which motions stopped the proof, by the name a person gave them: the kernel reports each operation it could
|
|
318
|
-
not prove (`j2-shoulder.chain: compose@1 turns …`), and the screen showed that English list as it was. The
|
|
319
|
-
name before the first dot is the joint or part; the operations stay in the logs.
|
|
320
|
-
*/
|
|
321
|
-
at: [...new Set((inspection.coverage.unprovable ?? []).map(entry => entry.split(':')[0]!.split('.')[0]!.trim()).filter(Boolean))].join(' · ') || undefined,
|
|
322
|
-
blocking: false
|
|
323
|
-
})
|
|
324
|
-
}
|
|
325
|
-
/*
|
|
326
|
-
Said, never refused (ADR-0104 decision 4): the origin is where the figure is fastened, and a door fastened at its
|
|
327
|
-
hinge is a right model. A drawing that places figures by the middle of their room reads this before it pins one.
|
|
328
|
-
*/
|
|
329
|
-
const origin = figureOriginCentredOf(asset!)
|
|
330
|
-
if (origin && !origin.centred) {
|
|
331
|
-
const off = origin.off.map(o => `${o.axis.toUpperCase()} ${o.mm} mm`).join(', ')
|
|
332
|
-
findings.push({
|
|
333
|
-
code: 'origin-off-centre',
|
|
334
|
-
message: say('figure.gate.origin-off-centre.message', { off }, `The origin is off the middle of the declared room: ${off}.`),
|
|
335
|
-
why: say('figure.gate.origin-off-centre.why', {}, 'A drawing that places figures by the middle of their room puts this one that far from where it is drawn.'),
|
|
336
|
-
how: say('figure.gate.origin-off-centre.how', {}, 'Keep it when the figure is fastened there, as a door at its hinge. Move the origin to the middle to place it by its room.'),
|
|
337
|
-
blocking: false
|
|
338
|
-
})
|
|
339
|
-
}
|
|
340
280
|
findings.push(...planeGapFindings(asset!, say))
|
|
341
281
|
if (releasedJson) findings.push(...figureContractFindings(asset!, releasedJson, say))
|
|
342
282
|
findings.push(...appFindings({ asset: asset!, type: rowType, category: rowCategory, t }))
|
|
@@ -374,7 +314,7 @@ const PLANE_ENGLISH = {
|
|
|
374
314
|
}
|
|
375
315
|
} as const
|
|
376
316
|
function planeGapFindings(asset: FigureAsset, say: Say): GateFinding[] {
|
|
377
|
-
if (asset.
|
|
317
|
+
if (asset.mountingFace) return []
|
|
378
318
|
const gap = figurePlaneGapOf(asset)
|
|
379
319
|
if (!gap || gap.touches) return []
|
|
380
320
|
const english = PLANE_ENGLISH[gap.placement]
|
|
@@ -550,10 +490,15 @@ function figureContractWording(change: FigureContractChange, say: Say): Pick<Gat
|
|
|
550
490
|
why: say(key('why'), {}, 'It adds what the figure can do. Placed instances stand as they are.')
|
|
551
491
|
}
|
|
552
492
|
/* Said, never refused (ADR-0104): a consumer that pins a release reads these before it moves to this one. */
|
|
553
|
-
case '
|
|
493
|
+
case 'size-changed':
|
|
494
|
+
return {
|
|
495
|
+
message: say(key('message'), v, `The figure's size changed from ${v.before || 'none'} to ${v.after || 'none'}.`),
|
|
496
|
+
why: say(key('why'), {}, 'A board gives the figure a box of this size. One that moves to this release sees its box change.')
|
|
497
|
+
}
|
|
498
|
+
case 'motion-range-changed':
|
|
554
499
|
return {
|
|
555
|
-
message: say(key('message'), v, `The room the figure
|
|
556
|
-
why: say(key('why'), {}, '
|
|
500
|
+
message: say(key('message'), v, `The room the figure sweeps as it moves changed from ${v.before || 'none'} to ${v.after || 'none'}.`),
|
|
501
|
+
why: say(key('why'), {}, 'It is what the figure reaches when it moves. One that moves to this release sees it reach differently.')
|
|
557
502
|
}
|
|
558
503
|
case 'contact-face-changed':
|
|
559
504
|
return {
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
applyFigureAuthoring,
|
|
14
14
|
createFigureAsset,
|
|
15
15
|
placeholderType,
|
|
16
|
-
proposeFigureOccupancy,
|
|
17
16
|
serializeFigureAsset
|
|
18
17
|
} from '@hatiolab/figure-model'
|
|
19
18
|
|
|
@@ -37,11 +36,11 @@ function loadCompiled(modulePath: string): any {
|
|
|
37
36
|
return require(resolved)
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
/** A figure that passes the gate on its own: a box standing on the floor
|
|
39
|
+
/** A figure that passes the gate on its own: a box standing on the floor. */
|
|
41
40
|
function sound(): string {
|
|
42
41
|
let asset = createFigureAsset('Probe')
|
|
43
42
|
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
44
|
-
asset = applyFigureAuthoring(asset, { kind: '
|
|
43
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' })
|
|
45
44
|
return serializeFigureAsset(asset)
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -102,12 +101,14 @@ describe('inspectFigureDraft — the verdict while editing', () => {
|
|
|
102
101
|
it('judges the unsaved source, not the stored one, and still reads the row type', async () => {
|
|
103
102
|
const figure = await save(placeholderType())
|
|
104
103
|
let edited = createFigureAsset('Probe')
|
|
105
|
-
edited = addFigureBox(edited, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y:
|
|
104
|
+
edited = addFigureBox(edited, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 150, z: 0 } })
|
|
105
|
+
/* Its bottom named as the face it is mounted on, 100 mm above the floor: a finding the stored one does not have. */
|
|
106
|
+
edited = applyFigureAuthoring(edited, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
|
|
106
107
|
|
|
107
108
|
const inspection = await new FigureQuery().inspectFigureDraft(serializeFigureAsset(edited), context(), figure.id)
|
|
108
109
|
const codes = inspection.findings.map((f: any) => f.code)
|
|
109
110
|
|
|
110
|
-
expect(codes).toContain('
|
|
111
|
+
expect(codes).toContain('parts-off-placement-face')
|
|
111
112
|
expect(codes).toContain('type-is-placeholder')
|
|
112
113
|
})
|
|
113
114
|
|
|
@@ -43,7 +43,7 @@ function mixerDraft(): FigureAsset {
|
|
|
43
43
|
{ kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } },
|
|
44
44
|
/* 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°). */
|
|
45
45
|
{ kind: 'attach', part: 'port', to: { part: 'body', surface: { around: 270, along: 'centre' } } },
|
|
46
|
-
{ kind: '
|
|
46
|
+
{ kind: 'set-placement', placement: 'floor' }
|
|
47
47
|
] as any)
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -10,7 +10,7 @@ const JSZip = require('jszip')
|
|
|
10
10
|
const shell = require('@things-factory/shell')
|
|
11
11
|
const authBase = require('@things-factory/auth-base')
|
|
12
12
|
const settingBase = require('@things-factory/setting-base')
|
|
13
|
-
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring,
|
|
13
|
+
const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, readFigurePackage } = require('@hatiolab/figure-model')
|
|
14
14
|
|
|
15
15
|
let dataSource: DataSource | null = null
|
|
16
16
|
let Figure: any
|
|
@@ -60,7 +60,7 @@ afterAll(async () => {
|
|
|
60
60
|
const releasable = (label = 'Conveyor') => {
|
|
61
61
|
let a = addFigureBox(createFigureAsset(label), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
62
62
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
|
|
63
|
-
return serializeFigureAsset(applyFigureAuthoring(a, { kind: '
|
|
63
|
+
return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
let seq = 0
|