@things-factory/figure-service 10.1.205 → 10.1.207
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/event-subscriber.js +5 -0
- package/dist-server/service/figure/event-subscriber.js.map +1 -1
- 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 +9 -2
- package/dist-server/service/figure/figure-format.js +36 -69
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.js +2 -0
- package/dist-server/service/figure/figure-mutation.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-query.js +3 -5
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-revert.js +1 -1
- package/dist-server/service/figure/figure-revert.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/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/recipes/base-frame/recipe.json +4 -8
- package/recipes/belt-surface/recipe.json +2 -4
- package/recipes/bin-row/recipe.json +1 -2
- package/recipes/bollard-row/recipe.json +1 -2
- package/recipes/cabinet-doors/recipe.json +5 -10
- package/recipes/cable-tray/recipe.json +3 -6
- package/recipes/canopy/recipe.json +2 -4
- package/recipes/cart-handle/recipe.json +1 -2
- package/recipes/ceiling-rail-carriage/recipe.json +1 -2
- package/recipes/chain-rails/recipe.json +2 -4
- package/recipes/column-row/recipe.json +1 -2
- package/recipes/conveyor-skirts/recipe.json +4 -8
- package/recipes/double-hinged-door/recipe.json +6 -12
- package/recipes/drawer-stack/recipe.json +13 -26
- package/recipes/drip-tray/recipe.json +2 -4
- package/recipes/end-covers/recipe.json +4 -8
- package/recipes/ffu-row/recipe.json +1 -2
- package/recipes/floor-zone/recipe.json +4 -8
- package/recipes/folding-door/recipe.json +3 -6
- package/recipes/gantry-frame/recipe.json +1 -2
- package/recipes/guard-cover/recipe.json +2 -4
- package/recipes/handrail/recipe.json +2 -4
- package/recipes/hatch/recipe.json +4 -8
- package/recipes/hinged-door/recipe.json +4 -8
- package/recipes/hopper/recipe.json +1 -2
- package/recipes/ladder/recipe.json +3 -6
- package/recipes/lift-updown/recipe.json +2 -4
- package/recipes/linear-guide/recipe.json +1 -2
- package/recipes/locker-row/recipe.json +1 -2
- package/recipes/observation-window/recipe.json +4 -8
- package/recipes/operator-panel/recipe.json +4 -8
- package/recipes/partition-wall/recipe.json +3 -6
- package/recipes/peg-board/facts.md +1 -1
- package/recipes/peg-board/recipe.json +1 -2
- package/recipes/pipe-run/recipe.json +1 -2
- package/recipes/platform/recipe.json +2 -4
- package/recipes/pull-cord/recipe.json +1 -2
- package/recipes/roll-up-shutter/recipe.json +3 -6
- package/recipes/roller-row/recipe.json +2 -4
- package/recipes/room-shell/recipe.json +6 -12
- package/recipes/safety-fence/recipe.json +2 -4
- package/recipes/safety-mat/recipe.json +1 -2
- package/recipes/shelf-tiers/recipe.json +5 -10
- package/recipes/side-guides/recipe.json +2 -4
- package/recipes/sign-board/recipe.json +1 -2
- package/recipes/sliding-door/recipe.json +7 -14
- package/recipes/stairs/recipe.json +1 -2
- package/recipes/stopper/recipe.json +1 -2
- package/recipes/suction-pads/recipe.json +1 -2
- package/recipes/tank/recipe.json +2 -4
- package/recipes/toe-board/recipe.json +1 -2
- package/recipes/traffic-cones/recipe.json +1 -2
- package/recipes/transfer-belts/recipe.json +2 -4
- package/recipes/trough/recipe.json +1 -2
- package/recipes/turntable/recipe.json +1 -2
- package/recipes/workbench-top/recipe.json +2 -4
- package/scripts/templates.test.mts +2 -3
- package/server/service/figure/event-subscriber.ts +4 -0
- package/server/service/figure/figure-blueprint-query.test.ts +3 -0
- package/server/service/figure/figure-category-db.test.ts +2 -2
- package/server/service/figure/figure-context.test.ts +2 -2
- 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 +40 -73
- 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-mutation.ts +2 -0
- 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-query.ts +3 -6
- 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 +156 -0
- package/server/service/figure/figure-revert.ts +2 -2
- package/server/service/figure/figure-tools.test.ts +2 -2
- package/server/service/figure/figure-type.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 +4 -16
- package/translations/ms.json +4 -16
- package/translations/zh.json +4 -16
|
@@ -65,8 +65,7 @@
|
|
|
65
65
|
"of": "part",
|
|
66
66
|
"part": "{target}",
|
|
67
67
|
"axis": "z"
|
|
68
|
-
}
|
|
69
|
-
"narrowRange": true
|
|
68
|
+
}
|
|
70
69
|
},
|
|
71
70
|
{
|
|
72
71
|
"kind": "link-dimension",
|
|
@@ -76,8 +75,7 @@
|
|
|
76
75
|
"of": "part",
|
|
77
76
|
"part": "{target}",
|
|
78
77
|
"axis": "z"
|
|
79
|
-
}
|
|
80
|
-
"narrowRange": true
|
|
78
|
+
}
|
|
81
79
|
},
|
|
82
80
|
{
|
|
83
81
|
"kind": "set-joint",
|
|
@@ -55,8 +55,7 @@
|
|
|
55
55
|
"of": "part",
|
|
56
56
|
"part": "{target}",
|
|
57
57
|
"axis": "x"
|
|
58
|
-
}
|
|
59
|
-
"narrowRange": true
|
|
58
|
+
}
|
|
60
59
|
},
|
|
61
60
|
{
|
|
62
61
|
"kind": "link-dimension",
|
|
@@ -66,8 +65,7 @@
|
|
|
66
65
|
"of": "part",
|
|
67
66
|
"part": "{target}",
|
|
68
67
|
"axis": "z"
|
|
69
|
-
}
|
|
70
|
-
"narrowRange": true
|
|
68
|
+
}
|
|
71
69
|
},
|
|
72
70
|
{
|
|
73
71
|
"kind": "set-joint",
|
|
@@ -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}`)
|
|
@@ -12,6 +12,8 @@ import { FigureHistory } from './figure-history.js'
|
|
|
12
12
|
* 그 전부를 남기면 판본 목록이 「내가 오늘 몇 번 눌렀나」가 된다. 사람이 판이라고 부르는 것은
|
|
13
13
|
* **발행한 것**이므로 그때만 베낀다. board 가 같은 규칙으로 돈다(`board-service` 의 같은 이름 파일).
|
|
14
14
|
*
|
|
15
|
+
* 발행판의 정본(`__releaseSource`)은 크기 범위를 적은 발행판 사본이다 — Figure 행에는 작성자가 좁힌 끝만 남는다.
|
|
16
|
+
*
|
|
15
17
|
* 발행 메모는 Figure 의 칸이 아니다. `releaseFigure` 가 저장할 엔티티에 `__releaseComment` 로 얹어
|
|
16
18
|
* 두면 여기서 판본의 `comment` 로 옮긴다 — 메모는 그 판의 것이지 도형의 것이 아니다. 정본 형식
|
|
17
19
|
* (`__releaseFormat`), 잰 커널의 판(`__releaseKernel`, ADR-0109 C1), 직전 판에 견준 변경(`__releaseChanges`), 발행 때 잰 값(`__releaseMetrics`)과
|
|
@@ -20,6 +22,8 @@ import { FigureHistory } from './figure-history.js'
|
|
|
20
22
|
/** What `releaseFigure` laid on the entity for this release, moved onto the history row. */
|
|
21
23
|
function releaseRecordOnto(history: FigureHistory, entity: Figure): FigureHistory {
|
|
22
24
|
const e = entity as any
|
|
25
|
+
/* What a board reads of this release: its own copy, with the size range written down (ADR-0087 decision 6). */
|
|
26
|
+
if (e.__releaseSource) history.source = e.__releaseSource
|
|
23
27
|
;(history as any).comment = e.__releaseComment ?? null
|
|
24
28
|
history.assetFormat = e.__releaseFormat ?? null
|
|
25
29
|
history.kernelVersion = e.__releaseKernel ?? null
|
|
@@ -44,6 +44,8 @@ beforeAll(async () => {
|
|
|
44
44
|
Figure = loadCompiled('service/figure/figure').Figure
|
|
45
45
|
FigureHistory = loadCompiled('service/figure/figure-history').FigureHistory
|
|
46
46
|
FigureQuery = loadCompiled('service/figure/figure-query').FigureQuery
|
|
47
|
+
/* A released row is saved with its history row, as releasing does: a board reads the release's own copy there (ADR-0087 decision 6). */
|
|
48
|
+
const Subscriber = loadCompiled('service/figure/event-subscriber').FigureHistoryEntitySubscriber
|
|
47
49
|
|
|
48
50
|
dataSource = new TypeOrmDataSource({
|
|
49
51
|
type: 'sqlite',
|
|
@@ -52,6 +54,7 @@ beforeAll(async () => {
|
|
|
52
54
|
dropSchema: true,
|
|
53
55
|
namingStrategy: new shell.NamingStrategy(),
|
|
54
56
|
entities: [...shell.entities, ...authBase.entities, Figure, FigureHistory],
|
|
57
|
+
subscribers: [Subscriber],
|
|
55
58
|
logging: false
|
|
56
59
|
})
|
|
57
60
|
|
|
@@ -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() {
|
|
@@ -193,10 +193,10 @@ describe('a proposal changes only what it was shown', () => {
|
|
|
193
193
|
it('a shown part may change, a new part may be fastened to any part, the figure as a whole may change', () => {
|
|
194
194
|
const steps = [
|
|
195
195
|
{ kind: 'set-material', target: 'shelf-2', material: { token: 'palette.primary' } },
|
|
196
|
-
{ kind: 'add-part', part: { id: 'lamp', kind: 'sphere', dimensions: {
|
|
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
|
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The doors a stored source goes through: saving (`parseSource`), publishing (`inspectSource
|
|
3
|
-
*
|
|
2
|
+
* The doors a stored source goes through: saving (`parseSource`), publishing (`inspectSource`, and the release's own
|
|
3
|
+
* copy in `releaseRecordOf`), bringing a release back as a draft (`draftSourceOfRelease`) and handing it to a board
|
|
4
|
+
* (`consumablePayloadOf`). The format is `figure-asset-1`, and the asset says so in its `version`; a source
|
|
4
5
|
* that does not is refused by name, because a source the server cannot identify is one it must not save.
|
|
5
6
|
*/
|
|
6
|
-
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'
|
|
7
8
|
import type { FigureAsset, FigureContractChange, FigureContractError, FigureReleaseMetrics, FigureTopView, FigureViolation } from '@hatiolab/figure-model'
|
|
8
9
|
|
|
9
10
|
import type { FigureFinding } from './figure-type.js'
|
|
@@ -61,8 +62,8 @@ export interface FigureReleaseCheckInput {
|
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* A check an app adds to releasing (LV figure-server design G10): findings in the gate's own shape, blocking or said.
|
|
64
|
-
* The LV figure-server refuses an origin off the middle of the
|
|
65
|
-
*
|
|
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.
|
|
66
67
|
*/
|
|
67
68
|
export type FigureReleaseCheck = (input: FigureReleaseCheckInput) => GateFinding[]
|
|
68
69
|
|
|
@@ -161,7 +162,7 @@ export type Translate = (key: string, options?: Record<string, unknown>) => stri
|
|
|
161
162
|
* The words are the modeller's (ADR-0097 §9 Q2 A): 「차지하는 공간」 for the volume a figure takes up, 「부품에 맞추기」 for
|
|
162
163
|
* setting it from the parts, and 「장착면」 for the face it stands on (`figure.text.mounting-face-explained`). A fact, then the way out.
|
|
163
164
|
*/
|
|
164
|
-
function figureWording(violation: FigureViolation, say: Say
|
|
165
|
+
function figureWording(violation: FigureViolation, say: Say): Pick<GateFinding, 'message' | 'why' | 'how'> {
|
|
165
166
|
const part = violation.part ?? ''
|
|
166
167
|
const axis = (violation.axis ?? '').toUpperCase()
|
|
167
168
|
const mm = violation.mm ?? 0
|
|
@@ -169,31 +170,6 @@ function figureWording(violation: FigureViolation, say: Say, hasParts: boolean):
|
|
|
169
170
|
const key = (field: string) => `figure.gate.${violation.code.toLowerCase().replace(/_/g, '-')}.${field}`
|
|
170
171
|
|
|
171
172
|
switch (violation.code) {
|
|
172
|
-
case 'PART_OUTSIDE_OCCUPANCY':
|
|
173
|
-
return {
|
|
174
|
-
message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the space the figure takes on ${axis}.`),
|
|
175
|
-
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.'),
|
|
176
|
-
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}.`)
|
|
177
|
-
}
|
|
178
|
-
case 'OCCUPANCY_UNDECLARED':
|
|
179
|
-
return violation.axis
|
|
180
|
-
? {
|
|
181
|
-
message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The space the figure takes is empty on ${axis}.`),
|
|
182
|
-
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
183
|
-
how: say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
184
|
-
}
|
|
185
|
-
: {
|
|
186
|
-
message: say(key('message'), {}, 'The space this figure takes is not set yet.'),
|
|
187
|
-
why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
|
|
188
|
-
/*
|
|
189
|
-
A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —
|
|
190
|
-
the screen does not offer it. Telling the author to press it sent them looking for a button that was not
|
|
191
|
-
there (:3300, 2026-09-24).
|
|
192
|
-
*/
|
|
193
|
-
how: hasParts
|
|
194
|
-
? say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
|
|
195
|
-
: say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to set the space the figure takes from it.')
|
|
196
|
-
}
|
|
197
173
|
case 'PARTS_OFF_PLACEMENT_FACE':
|
|
198
174
|
return {
|
|
199
175
|
message: say(key('message'), { part, mm }, `The mounting face of '${part}' is ${mm} mm off the plane the figure stands on.`),
|
|
@@ -269,12 +245,9 @@ export function inspectSource(
|
|
|
269
245
|
|
|
270
246
|
let inspection
|
|
271
247
|
let asset: FigureAsset | undefined
|
|
272
|
-
let hasParts = true
|
|
273
248
|
try {
|
|
274
249
|
asset = parseFigureAsset(sourceJson)
|
|
275
250
|
inspection = inspectFigureAsset(asset)
|
|
276
|
-
const cost = figureCostOf(asset)
|
|
277
|
-
hasParts = cost.groups + cost.hidden > 0
|
|
278
251
|
} catch (e) {
|
|
279
252
|
const error = e as FigureContractError & { path?: string }
|
|
280
253
|
return {
|
|
@@ -286,7 +259,7 @@ export function inspectSource(
|
|
|
286
259
|
const say = sayerOf(t)
|
|
287
260
|
const findings: GateFinding[] = inspection.violations.map(violation => ({
|
|
288
261
|
code: violation.code.toLowerCase().replace(/_/g, '-'),
|
|
289
|
-
...figureWording(violation, say
|
|
262
|
+
...figureWording(violation, say),
|
|
290
263
|
at: violation.part,
|
|
291
264
|
blocking: true
|
|
292
265
|
}))
|
|
@@ -304,38 +277,6 @@ export function inspectSource(
|
|
|
304
277
|
blocking: true
|
|
305
278
|
})
|
|
306
279
|
}
|
|
307
|
-
if (inspection.coverage.method === 'sampled') {
|
|
308
|
-
const states = inspection.coverage.states
|
|
309
|
-
findings.push({
|
|
310
|
-
code: 'state-range-sampled',
|
|
311
|
-
/* Sizes as well as motion: the instance size range is swept for every figure (figure-model, designer 2026-09-27). */
|
|
312
|
-
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.`),
|
|
313
|
-
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.'),
|
|
314
|
-
how: say('figure.gate.state-range-sampled.how', {}, 'To cover the whole range, re-author what could not be proven.'),
|
|
315
|
-
/*
|
|
316
|
-
Which motions stopped the proof, by the name a person gave them: the kernel reports each operation it could
|
|
317
|
-
not prove (`j2-shoulder.chain: compose@1 turns …`), and the screen showed that English list as it was. The
|
|
318
|
-
name before the first dot is the joint or part; the operations stay in the logs.
|
|
319
|
-
*/
|
|
320
|
-
at: [...new Set((inspection.coverage.unprovable ?? []).map(entry => entry.split(':')[0]!.split('.')[0]!.trim()).filter(Boolean))].join(' · ') || undefined,
|
|
321
|
-
blocking: false
|
|
322
|
-
})
|
|
323
|
-
}
|
|
324
|
-
/*
|
|
325
|
-
Said, never refused (ADR-0104 decision 4): the origin is where the figure is fastened, and a door fastened at its
|
|
326
|
-
hinge is a right model. A drawing that places figures by the middle of their room reads this before it pins one.
|
|
327
|
-
*/
|
|
328
|
-
const origin = figureOriginCentredOf(asset!)
|
|
329
|
-
if (origin && !origin.centred) {
|
|
330
|
-
const off = origin.off.map(o => `${o.axis.toUpperCase()} ${o.mm} mm`).join(', ')
|
|
331
|
-
findings.push({
|
|
332
|
-
code: 'origin-off-centre',
|
|
333
|
-
message: say('figure.gate.origin-off-centre.message', { off }, `The origin is off the middle of the declared room: ${off}.`),
|
|
334
|
-
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.'),
|
|
335
|
-
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.'),
|
|
336
|
-
blocking: false
|
|
337
|
-
})
|
|
338
|
-
}
|
|
339
280
|
findings.push(...planeGapFindings(asset!, say))
|
|
340
281
|
if (releasedJson) findings.push(...figureContractFindings(asset!, releasedJson, say))
|
|
341
282
|
findings.push(...appFindings({ asset: asset!, type: rowType, category: rowCategory, t }))
|
|
@@ -373,7 +314,7 @@ const PLANE_ENGLISH = {
|
|
|
373
314
|
}
|
|
374
315
|
} as const
|
|
375
316
|
function planeGapFindings(asset: FigureAsset, say: Say): GateFinding[] {
|
|
376
|
-
if (asset.
|
|
317
|
+
if (asset.mountingFace) return []
|
|
377
318
|
const gap = figurePlaneGapOf(asset)
|
|
378
319
|
if (!gap || gap.touches) return []
|
|
379
320
|
const english = PLANE_ENGLISH[gap.placement]
|
|
@@ -549,10 +490,15 @@ function figureContractWording(change: FigureContractChange, say: Say): Pick<Gat
|
|
|
549
490
|
why: say(key('why'), {}, 'It adds what the figure can do. Placed instances stand as they are.')
|
|
550
491
|
}
|
|
551
492
|
/* Said, never refused (ADR-0104): a consumer that pins a release reads these before it moves to this one. */
|
|
552
|
-
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':
|
|
553
499
|
return {
|
|
554
|
-
message: say(key('message'), v, `The room the figure
|
|
555
|
-
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.')
|
|
556
502
|
}
|
|
557
503
|
case 'contact-face-changed':
|
|
558
504
|
return {
|
|
@@ -711,9 +657,20 @@ function figureContractWording(change: FigureContractChange, say: Say): Pick<Gat
|
|
|
711
657
|
export function releaseRecordOf(
|
|
712
658
|
sourceJson: string,
|
|
713
659
|
releasedJson?: string | null
|
|
714
|
-
): { format: string; kernel: string; changes: FigureContractChange[] | null; metrics: FigureReleaseMetrics; topView: FigureTopView } {
|
|
660
|
+
): { source: string; format: string; kernel: string; changes: FigureContractChange[] | null; metrics: FigureReleaseMetrics; topView: FigureTopView } {
|
|
715
661
|
const asset = parseFigureAsset(sourceJson)
|
|
716
|
-
|
|
662
|
+
/*
|
|
663
|
+
The release's own copy carries its size range as it is now: the author's ends, and the rest as far as the parts
|
|
664
|
+
allow (ADR-0087 decision 6). A board reads the ends from it and does not measure; the figure's row keeps only what
|
|
665
|
+
the author narrowed.
|
|
666
|
+
*/
|
|
667
|
+
const measured = {
|
|
668
|
+
source: serializeFigureAsset(recordFigureInstanceSize(asset)),
|
|
669
|
+
format: asset.version,
|
|
670
|
+
kernel: FIGURE_KERNEL_VERSION,
|
|
671
|
+
metrics: figureReleaseMetricsOf(asset),
|
|
672
|
+
topView: figureTopViewOf(asset, null)
|
|
673
|
+
}
|
|
717
674
|
if (!releasedJson) return { ...measured, changes: [] }
|
|
718
675
|
let before: FigureAsset
|
|
719
676
|
try {
|
|
@@ -724,6 +681,16 @@ export function releaseRecordOf(
|
|
|
724
681
|
return { ...measured, changes: figureContractChanges(before, asset) }
|
|
725
682
|
}
|
|
726
683
|
|
|
684
|
+
/**
|
|
685
|
+
* A release's source made a draft's again (a revert): the size ends its release wrote down, as far as the parts allow,
|
|
686
|
+
* are opened, and the author's own are kept (ADR-0087 decision 6). An author's end that equals the computed one is
|
|
687
|
+
* opened with them.
|
|
688
|
+
*/
|
|
689
|
+
export function draftSourceOfRelease(releasedJson: string): string {
|
|
690
|
+
assertFormat(releasedJson)
|
|
691
|
+
return serializeFigureAsset(forgetRecordedInstanceSize(parseFigureAsset(releasedJson)))
|
|
692
|
+
}
|
|
693
|
+
|
|
727
694
|
/**
|
|
728
695
|
* What a consumer is handed for a released figure: the asset itself, because what draws it -- `registerFigure` in
|
|
729
696
|
* things-scene -- takes the asset and compiles it with the kernel.
|