@things-factory/figure-service 10.1.115 → 10.1.116

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.115",
3
+ "version": "10.1.116",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -25,16 +25,16 @@
25
25
  "test:templates": "node --test scripts/templates.test.mts"
26
26
  },
27
27
  "dependencies": {
28
- "@hatiolab/figure-model": "^0.2.15",
28
+ "@hatiolab/figure-model": "^0.3.2",
29
29
  "@operato/typeorm-history": "^10.0.0",
30
30
  "@things-factory/ai-client-base": "^10.1.114",
31
- "@things-factory/attachment-base": "^10.1.113",
32
- "@things-factory/auth-base": "^10.1.113",
31
+ "@things-factory/attachment-base": "^10.1.116",
32
+ "@things-factory/auth-base": "^10.1.116",
33
33
  "@things-factory/env": "^10.1.20",
34
34
  "@things-factory/shell": "^10.1.106",
35
35
  "graphql-scalars": "^1.22.4",
36
36
  "graphql-upload": "^15.0.2",
37
37
  "koa-router": "^7.4.0"
38
38
  },
39
- "gitHead": "3d1f480028b680a6da3d0ae6a73c3121d644b64a"
39
+ "gitHead": "49fa935cc703be70200eef4d29f473de98f871ce"
40
40
  }
@@ -350,7 +350,7 @@ describe('figure-format — 판을 알아보는 문', () => {
350
350
  it('every new change has its sentence in both languages', () => {
351
351
  const KO = require('../../../translations/ko.json') as Record<string, string>
352
352
  const EN = require('../../../translations/en.json') as Record<string, string>
353
- for (const code of ['occupancy-changed', 'contact-face-changed', 'public-input-gone', 'public-input-range-narrowed', 'public-input-added'])
353
+ for (const code of ['occupancy-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'])
354
354
  for (const dict of [KO, EN]) for (const field of ['message', 'why']) expect(dict[`figure.gate.contract.${code}.${field}`]).toBeTruthy()
355
355
  })
356
356
  })
@@ -461,6 +461,37 @@ function figureContractWording(change: FigureContractChange, say: Say): Pick<Gat
461
461
  message: say(key('message'), { ...v, input: change.subject ?? '' }, `'${change.subject}' can now be set on a board.`),
462
462
  why: say(key('why'), {}, 'Placed instances that give no value stand at its default.')
463
463
  }
464
+ /* The joints a controller drives (ADR-0107 decision 7): what writing a state input moves, compared release to release. */
465
+ case 'state-input-joints-changed': {
466
+ const after = v.after || say('figure.gate.contract.joints-none', {}, 'nothing')
467
+ return {
468
+ message: say(key('message'), { ...v, after }, `The control '${v.id}' moved ${v.before}. It now moves ${after}.`),
469
+ why: say(key('why'), {}, 'A controller writes this value to drive those joints. After this release the same value moves other joints, or by other amounts.'),
470
+ ...gone
471
+ }
472
+ }
473
+ case 'joint-axis-changed':
474
+ return {
475
+ message: say(key('message'), v, `The axis of the joint '${v.joint}' changed from ${v.before} to ${v.after}.`),
476
+ why: say(key('why'), {}, "A controller's stored values turn or slide the joint about this axis. After this release the same value moves it another way."),
477
+ ...gone
478
+ }
479
+ case 'joint-motion-unit-changed':
480
+ return {
481
+ message: say(key('message'), v, `The joint '${v.joint}' now moves in ${v.after}, not ${v.before}.`),
482
+ why: say(key('why'), {}, "A controller's stored values were written for the old unit. After this release the same value means another motion."),
483
+ ...gone
484
+ }
485
+ case 'joint-child-changed':
486
+ return {
487
+ message: say(key('message'), v, `The joint '${v.joint}' now moves '${v.after}' instead of '${v.before}'.`),
488
+ why: say(key('why'), {}, "A controller's values keep their meaning. On screen the joint now moves the new part.")
489
+ }
490
+ case 'joint-list-unread':
491
+ return {
492
+ message: say(key('message'), v, `The last release's joint list could not be read, so its joints were not compared: ${v.reason}`),
493
+ why: say(key('why'), {}, "Whether this release changes what a controller's values move has not been checked. The last release was made by a kernel that did not measure joints.")
494
+ }
464
495
  }
465
496
  }
466
497
 
@@ -141,7 +141,7 @@ export class FigureHistory implements HistoryEntityInterface<Figure> {
141
141
  @Field({
142
142
  nullable: true,
143
143
  description:
144
- 'What this version measured once, as JSON (figure-model figureReleaseMetricsOf): { atLargest?: { size, triangles, groups }, linear?: { scalable, parts, probe } }. Only for a figure that takes its size from the instance; {} otherwise.'
144
+ 'What this version measured once, as JSON (figure-model figureReleaseMetricsOf): { atLargest?: { size, triangles, groups }, linear?: { scalable, parts, probe }, origin?: { centred, off }, joints?: { schema, derivation, joints, unlisted } }. atLargest and linear only for a figure that takes its size from the instance. joints is the joint list a controller reads (schema 2, figure-model figureJointsOf), present when anything moves.'
145
145
  })
146
146
  metrics?: string
147
147
 
@@ -7,13 +7,14 @@ import type { DataSource } from 'typeorm'
7
7
  const { DataSource: TypeOrmDataSource } = require('typeorm')
8
8
  const shell = require('@things-factory/shell')
9
9
  const authBase = require('@things-factory/auth-base')
10
- const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, proposeFigureOccupancy, FIGURE_KERNEL_VERSION } = require('@hatiolab/figure-model')
10
+ const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, proposeFigureOccupancy, palletizerRobotFigure, FIGURE_KERNEL_VERSION } = require('@hatiolab/figure-model')
11
11
 
12
12
  let dataSource: DataSource | null = null
13
13
  let Figure: any
14
14
  let FigureHistory: any
15
15
  let Subscriber: any
16
16
  let FigureMutation: any
17
+ let releaseRecordOf: any
17
18
 
18
19
  function loadCompiled(modulePath: string): any {
19
20
  const { existsSync } = require('fs')
@@ -29,6 +30,7 @@ beforeAll(async () => {
29
30
  FigureHistory = loadCompiled('service/figure/figure-history').FigureHistory
30
31
  Subscriber = loadCompiled('service/figure/event-subscriber').FigureHistoryEntitySubscriber
31
32
  FigureMutation = loadCompiled('service/figure/figure-mutation').FigureMutation
33
+ releaseRecordOf = loadCompiled('service/figure/figure-format').releaseRecordOf
32
34
  dataSource = new TypeOrmDataSource({
33
35
  type: 'sqlite',
34
36
  database: ':memory:',
@@ -101,3 +103,41 @@ describe('releaseFigure records it', () => {
101
103
  expect(JSON.parse(rows[1].topView).shapes.map((s: any) => s.id)).toEqual(['body'])
102
104
  })
103
105
  })
106
+
107
+ /*
108
+ The joint list a controller reads rides the release's metrics (ADR-0107 decision 7): measured once, at release, and
109
+ read back from the history row. A re-release that turns a joint about another axis is refused with the sentence.
110
+ */
111
+ describe('a release of a figure that moves keeps its joint list', () => {
112
+ const palletizer = (j4Axis?: string) => {
113
+ const { asset } = palletizerRobotFigure()
114
+ if (!j4Axis) return serializeFigureAsset(asset)
115
+ const j4 = asset.joints.find((j: any) => j.id === 'j4')
116
+ return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j4, axis: j4Axis } }))
117
+ }
118
+ it('schema 2, the wrist following -(j2 + j3), controls with both units; another axis is refused', async () => {
119
+ const domain = await dataSource!.getRepository(shell.Domain).save({ name: 'release-joints', subdomain: 'release-joints' })
120
+ const context = { state: { domain, user: undefined, tx: dataSource!.manager } }
121
+ const figure = await dataSource!.getRepository(Figure).save({ type: 'PALLETIZER', name: 'palletizer', source: palletizer(), state: 'draft', version: 0, domain })
122
+ await new FigureMutation().releaseFigure(figure.id, 'one', context)
123
+
124
+ const [row] = await dataSource!.getRepository(FigureHistory).find({ where: { originalId: figure.id } })
125
+ const list = JSON.parse(row.metrics).joints
126
+ expect(list.schema).toBe(2)
127
+ const byId = new Map(list.joints.map((j: any) => [j.id, j]))
128
+ expect(byId.get('wrist')).toMatchObject({
129
+ type: 'revolute',
130
+ follows: { sum: [{ joint: 'j2', multiplier: -1 }, { joint: 'j3', multiplier: -1 }], offset: 0 }
131
+ })
132
+ expect((byId.get('wrist') as any).control).toBeUndefined()
133
+ for (const id of ['j1', 'j2', 'j3', 'j4']) expect((byId.get(id) as any).control).toMatchObject({ inputUnit: 'deg', motionUnit: 'deg' })
134
+ expect(list.unlisted).toEqual([])
135
+
136
+ /* The same figure with J4 turning about X: the kernel's comparison blocks it, and the door refuses with its sentence. */
137
+ const changes = releaseRecordOf(palletizer('X'), row.source).changes
138
+ expect(changes).toContainEqual(expect.objectContaining({ code: 'joint-axis-changed', blocking: true, subject: 'j4' }))
139
+ await dataSource!.getRepository(Figure).save({ ...(await dataSource!.getRepository(Figure).findOneOrFail({ where: { id: figure.id } })), domain, source: palletizer('X'), state: 'draft' })
140
+ await expect(new FigureMutation().releaseFigure(figure.id, 'two', context)).rejects.toThrow(/The axis of the joint 'j4' changed/)
141
+ expect(await dataSource!.getRepository(FigureHistory).count({ where: { originalId: figure.id } })).toBe(1)
142
+ })
143
+ })
@@ -75,6 +75,17 @@
75
75
  "figure.gate.contract.public-input-range-narrowed.why": "An instance may have set a value outside the new range. Which values are placed is not recorded.",
76
76
  "figure.gate.contract.public-input-added.message": "'{input}' can now be set on a board.",
77
77
  "figure.gate.contract.public-input-added.why": "Placed instances that give no value stand at its default.",
78
+ "figure.gate.contract.joints-none": "nothing",
79
+ "figure.gate.contract.state-input-joints-changed.message": "The control '{id}' moved {before}. It now moves {after}.",
80
+ "figure.gate.contract.state-input-joints-changed.why": "A controller writes this value to drive those joints. After this release the same value moves other joints, or by other amounts.",
81
+ "figure.gate.contract.joint-axis-changed.message": "The axis of the joint '{joint}' changed from {before} to {after}.",
82
+ "figure.gate.contract.joint-axis-changed.why": "A controller's stored values turn or slide the joint about this axis. After this release the same value moves it another way.",
83
+ "figure.gate.contract.joint-motion-unit-changed.message": "The joint '{joint}' now moves in {after}, not {before}.",
84
+ "figure.gate.contract.joint-motion-unit-changed.why": "A controller's stored values were written for the old unit. After this release the same value means another motion.",
85
+ "figure.gate.contract.joint-child-changed.message": "The joint '{joint}' now moves '{after}' instead of '{before}'.",
86
+ "figure.gate.contract.joint-child-changed.why": "A controller's values keep their meaning. On screen the joint now moves the new part.",
87
+ "figure.gate.contract.joint-list-unread.message": "The last release's joint list could not be read, so its joints were not compared: {reason}",
88
+ "figure.gate.contract.joint-list-unread.why": "Whether this release changes what a controller's values move has not been checked. The last release was made by a kernel that did not measure joints.",
78
89
  "error.figure-release-not-found": "{type} has no release {version}. its latest release is {latest}.",
79
90
  "figure.gate.origin-off-centre.message": "The origin is off the middle of the declared room: {off}.",
80
91
  "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.",
@@ -75,6 +75,17 @@
75
75
  "figure.gate.contract.public-input-range-narrowed.why": "새 범위 밖의 값을 준 인스턴스가 있을 수 있습니다. 어떤 값이 놓였는지는 기록하지 않습니다.",
76
76
  "figure.gate.contract.public-input-added.message": "'{input}' 값을 이제 보드에서 정할 수 있습니다.",
77
77
  "figure.gate.contract.public-input-added.why": "값을 주지 않은 인스턴스는 기본값으로 섭니다.",
78
+ "figure.gate.contract.joints-none": "없음",
79
+ "figure.gate.contract.state-input-joints-changed.message": "'{id}' 조작값이 움직이던 관절이 {before} 에서 {after} 로 바뀌었습니다.",
80
+ "figure.gate.contract.state-input-joints-changed.why": "컨트롤러는 이 값을 써서 그 관절들을 움직입니다. 이 발행 뒤에는 같은 값이 다른 관절을 움직이거나 다른 양만큼 움직입니다.",
81
+ "figure.gate.contract.joint-axis-changed.message": "'{joint}' 관절의 축이 {before} 에서 {after} 로 바뀌었습니다.",
82
+ "figure.gate.contract.joint-axis-changed.why": "컨트롤러가 저장한 값은 이 축을 기준으로 관절을 회전하거나 이동합니다. 이 발행 뒤에는 같은 값이 다른 방향으로 움직입니다.",
83
+ "figure.gate.contract.joint-motion-unit-changed.message": "'{joint}' 관절의 움직임 단위가 {before} 에서 {after} 로 바뀌었습니다.",
84
+ "figure.gate.contract.joint-motion-unit-changed.why": "컨트롤러가 저장한 값은 옛 단위에 맞춰 쓰였습니다. 이 발행 뒤에는 같은 값이 다른 움직임이 됩니다.",
85
+ "figure.gate.contract.joint-child-changed.message": "'{joint}' 관절이 '{before}' 대신 '{after}' 부품을 움직입니다.",
86
+ "figure.gate.contract.joint-child-changed.why": "컨트롤러의 값은 뜻이 그대로입니다. 화면에서는 그 관절이 새 부품을 움직입니다.",
87
+ "figure.gate.contract.joint-list-unread.message": "직전 발행본의 관절 목록을 읽지 못해 관절을 비교하지 않았습니다: {reason}",
88
+ "figure.gate.contract.joint-list-unread.why": "이 발행이 컨트롤러의 값이 움직이는 것을 바꾸는지는 확인하지 않았습니다. 직전 발행본은 관절을 재지 않던 커널로 발행되었습니다.",
78
89
  "error.figure-release-not-found": "{type} 에는 {version} 번 발행판이 없습니다. 최신 발행판은 {latest} 번입니다.",
79
90
  "figure.gate.origin-off-centre.message": "원점이 차지 공간 가운데에서 비켜 있습니다: {off}.",
80
91
  "figure.gate.origin-off-centre.why": "공간 가운데로 도형을 놓는 도면에서는 그린 자리에서 그만큼 떨어져 놓입니다.",