@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.
Files changed (105) hide show
  1. package/dist-server/service/figure/event-subscriber.js +5 -0
  2. package/dist-server/service/figure/event-subscriber.js.map +1 -1
  3. package/dist-server/service/figure/figure-context.js +0 -2
  4. package/dist-server/service/figure/figure-context.js.map +1 -1
  5. package/dist-server/service/figure/figure-format.d.ts +9 -2
  6. package/dist-server/service/figure/figure-format.js +36 -69
  7. package/dist-server/service/figure/figure-format.js.map +1 -1
  8. package/dist-server/service/figure/figure-mutation.js +2 -0
  9. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  10. package/dist-server/service/figure/figure-propose-steps.d.ts +1 -1
  11. package/dist-server/service/figure/figure-propose-steps.js +3 -5
  12. package/dist-server/service/figure/figure-propose-steps.js.map +1 -1
  13. package/dist-server/service/figure/figure-query.js +3 -5
  14. package/dist-server/service/figure/figure-query.js.map +1 -1
  15. package/dist-server/service/figure/figure-revert.js +1 -1
  16. package/dist-server/service/figure/figure-revert.js.map +1 -1
  17. package/dist-server/service/figure/figure-type.js +1 -1
  18. package/dist-server/service/figure/figure-type.js.map +1 -1
  19. package/dist-server/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +8 -8
  21. package/recipes/base-frame/recipe.json +4 -8
  22. package/recipes/belt-surface/recipe.json +2 -4
  23. package/recipes/bin-row/recipe.json +1 -2
  24. package/recipes/bollard-row/recipe.json +1 -2
  25. package/recipes/cabinet-doors/recipe.json +5 -10
  26. package/recipes/cable-tray/recipe.json +3 -6
  27. package/recipes/canopy/recipe.json +2 -4
  28. package/recipes/cart-handle/recipe.json +1 -2
  29. package/recipes/ceiling-rail-carriage/recipe.json +1 -2
  30. package/recipes/chain-rails/recipe.json +2 -4
  31. package/recipes/column-row/recipe.json +1 -2
  32. package/recipes/conveyor-skirts/recipe.json +4 -8
  33. package/recipes/double-hinged-door/recipe.json +6 -12
  34. package/recipes/drawer-stack/recipe.json +13 -26
  35. package/recipes/drip-tray/recipe.json +2 -4
  36. package/recipes/end-covers/recipe.json +4 -8
  37. package/recipes/ffu-row/recipe.json +1 -2
  38. package/recipes/floor-zone/recipe.json +4 -8
  39. package/recipes/folding-door/recipe.json +3 -6
  40. package/recipes/gantry-frame/recipe.json +1 -2
  41. package/recipes/guard-cover/recipe.json +2 -4
  42. package/recipes/handrail/recipe.json +2 -4
  43. package/recipes/hatch/recipe.json +4 -8
  44. package/recipes/hinged-door/recipe.json +4 -8
  45. package/recipes/hopper/recipe.json +1 -2
  46. package/recipes/ladder/recipe.json +3 -6
  47. package/recipes/lift-updown/recipe.json +2 -4
  48. package/recipes/linear-guide/recipe.json +1 -2
  49. package/recipes/locker-row/recipe.json +1 -2
  50. package/recipes/observation-window/recipe.json +4 -8
  51. package/recipes/operator-panel/recipe.json +4 -8
  52. package/recipes/partition-wall/recipe.json +3 -6
  53. package/recipes/peg-board/facts.md +1 -1
  54. package/recipes/peg-board/recipe.json +1 -2
  55. package/recipes/pipe-run/recipe.json +1 -2
  56. package/recipes/platform/recipe.json +2 -4
  57. package/recipes/pull-cord/recipe.json +1 -2
  58. package/recipes/roll-up-shutter/recipe.json +3 -6
  59. package/recipes/roller-row/recipe.json +2 -4
  60. package/recipes/room-shell/recipe.json +6 -12
  61. package/recipes/safety-fence/recipe.json +2 -4
  62. package/recipes/safety-mat/recipe.json +1 -2
  63. package/recipes/shelf-tiers/recipe.json +5 -10
  64. package/recipes/side-guides/recipe.json +2 -4
  65. package/recipes/sign-board/recipe.json +1 -2
  66. package/recipes/sliding-door/recipe.json +7 -14
  67. package/recipes/stairs/recipe.json +1 -2
  68. package/recipes/stopper/recipe.json +1 -2
  69. package/recipes/suction-pads/recipe.json +1 -2
  70. package/recipes/tank/recipe.json +2 -4
  71. package/recipes/toe-board/recipe.json +1 -2
  72. package/recipes/traffic-cones/recipe.json +1 -2
  73. package/recipes/transfer-belts/recipe.json +2 -4
  74. package/recipes/trough/recipe.json +1 -2
  75. package/recipes/turntable/recipe.json +1 -2
  76. package/recipes/workbench-top/recipe.json +2 -4
  77. package/scripts/templates.test.mts +2 -3
  78. package/server/service/figure/event-subscriber.ts +4 -0
  79. package/server/service/figure/figure-blueprint-query.test.ts +3 -0
  80. package/server/service/figure/figure-category-db.test.ts +2 -2
  81. package/server/service/figure/figure-context.test.ts +2 -2
  82. package/server/service/figure/figure-context.ts +0 -2
  83. package/server/service/figure/figure-deprecation-db.test.ts +2 -2
  84. package/server/service/figure/figure-format.test.ts +25 -105
  85. package/server/service/figure/figure-format.ts +40 -73
  86. package/server/service/figure/figure-inspect-db.test.ts +6 -5
  87. package/server/service/figure/figure-llm-smoke.test.ts +1 -1
  88. package/server/service/figure/figure-mutation.ts +2 -0
  89. package/server/service/figure/figure-namespace-db.test.ts +2 -2
  90. package/server/service/figure/figure-propose-steps.test.ts +15 -20
  91. package/server/service/figure/figure-propose-steps.ts +3 -5
  92. package/server/service/figure/figure-public-db.test.ts +2 -2
  93. package/server/service/figure/figure-query.ts +3 -6
  94. package/server/service/figure/figure-release-check.test.ts +2 -2
  95. package/server/service/figure/figure-release-record-db.test.ts +7 -6
  96. package/server/service/figure/figure-release-size-db.test.ts +156 -0
  97. package/server/service/figure/figure-revert.ts +2 -2
  98. package/server/service/figure/figure-tools.test.ts +2 -2
  99. package/server/service/figure/figure-type.ts +1 -1
  100. package/translations/en.json +4 -16
  101. package/translations/es.json +4 -16
  102. package/translations/ja.json +4 -16
  103. package/translations/ko.json +4 -16
  104. package/translations/ms.json +4 -16
  105. package/translations/zh.json +4 -16
@@ -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, with its space declared. */
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: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }), placement: 'floor' })
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: 50, z: 0 } })
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('occupancy-undeclared')
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: 'fit-room', placement: 'floor' }
46
+ { kind: 'set-placement', placement: 'floor' }
47
47
  ] as any)
48
48
  }
49
49
 
@@ -447,6 +447,8 @@ export class FigureMutation {
447
447
  state: 'released',
448
448
  updater: user,
449
449
  /* 판본 구독자가 이것들을 판본의 comment · assetFormat · kernelVersion · changes 로 옮긴다. Figure 의 칸이 아니다. */
450
+ /* The release's own copy, its size range written down; the figure keeps the author's (ADR-0087 decision 6). */
451
+ __releaseSource: record.source,
450
452
  __releaseComment: comment ?? null,
451
453
  __releaseFormat: record.format,
452
454
  __releaseKernel: record.kernel,
@@ -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, proposeFigureOccupancy, readFigurePackage } = require('@hatiolab/figure-model')
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: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), placement: 'floor' }))
63
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
64
64
  }
65
65
 
66
66
  let seq = 0
@@ -5,7 +5,7 @@
5
5
  * the step the kernel refuses, and the gate findings the steps bring in, go back to the model; after three attempts
6
6
  * the proposer says it could not make it, and nothing half made comes back.
7
7
  */
8
- import { FIGURE_PROPOSAL_GRAMMAR, addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, inspectFigureAsset, proposeFigureOccupancy } from '@hatiolab/figure-model'
8
+ import { FIGURE_PROPOSAL_GRAMMAR, addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, inspectFigureAsset } from '@hatiolab/figure-model'
9
9
  import { AIAnswerNotJSON, type AIClient } from '@things-factory/ai-client-base'
10
10
 
11
11
  import { ProposeFigureFailure, WORKED_EXAMPLE, judgeFigureAnswer, proposalFailureForPerson, proposeFigureSteps, rulesPromptFigure } from './figure-propose-steps'
@@ -38,10 +38,10 @@ const liftTable = [
38
38
  { kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } },
39
39
  { kind: 'attach', part: 'column', to: { part: 'base', face: 'top' } },
40
40
  { kind: 'attach', part: 'top', to: { part: 'column', face: 'top' } },
41
- { kind: 'set-material', target: 'top', material: { token: 'palette.accent' }, palette: { 'palette.accent': '#d08a2c' } },
42
- { kind: 'fit-room', placement: 'floor' }
41
+ { kind: 'set-material', target: 'top', material: { token: 'palette.accent' }, palette: { 'palette.accent': '#d08a2c' } }
43
42
  ]
44
- const withoutRoom = liftTable.slice(0, -1)
43
+ /* The table's top named as the face it is mounted on: a finding the draft has, its face 740 mm off the plane. */
44
+ const offFace = [...liftTable, { kind: 'set-mounting-face', part: 'top', face: 'top' }]
45
45
  const wrongFace = liftTable.map((s, i) => (i === 4 ? { ...s, face: 'left' } : s))
46
46
 
47
47
  describe('the model is told the kernel\'s commands and the open draft', () => {
@@ -74,7 +74,6 @@ describe('the model is told the kernel\'s commands and the open draft', () => {
74
74
  expect(judged.source!.appearance.map(a => a.target)).toEqual(['frame', 'support', 'roller'])
75
75
  const kinds = WORKED_EXAMPLE.map(step => step.kind)
76
76
  expect(kinds).toContain('make-repeat')
77
- expect(kinds.at(-1)).toBe('fit-room')
78
77
  expect(WORKED_EXAMPLE.filter(step => step.kind === 'attach' && 'plane' in (step as any).to).every(step => !('facing' in step))).toBe(true)
79
78
  })
80
79
 
@@ -117,15 +116,9 @@ describe('the steps are held to the kernel and the gate', () => {
117
116
  expect(said).toMatch(/SCHEMA: steps\[4\]: attach: column: face is a word attach does not take/)
118
117
  })
119
118
 
120
- it('a new figure must come out whole: a list that leaves the room undeclared is sent back', async () => {
121
- const client = stubClient([{ steps: withoutRoom }, { steps: liftTable }])
122
- const made = await proposeFigureSteps({ client, prompt: '리프트 테이블', name: 'Lift table' })
123
- expect(made.attempts).toBe(2)
124
- expect(client.sent[1]!.at(-1)!.content).toMatch(/OCCUPANCY_UNDECLARED/)
125
- })
126
-
127
119
  it('a revision is not blamed for what the draft already had', async () => {
128
- const draft = applyFigureProposal(createFigureAsset('Lift table'), withoutRoom as any)
120
+ const draft = applyFigureProposal(createFigureAsset('Lift table'), offFace as any)
121
+ expect(inspectFigureAsset(draft).violations.map(v => v.code)).toEqual(['PARTS_OFF_PLACEMENT_FACE'])
129
122
  const client = stubClient([{ steps: [{ kind: 'rename', name: 'Lift' }] }])
130
123
  const made = await proposeFigureSteps({ client, prompt: '이름', base: draft })
131
124
  expect(made.attempts).toBe(1)
@@ -227,28 +220,30 @@ describe('reference pictures go with the task', () => {
227
220
 
228
221
  /*
229
222
  A revision is not blamed for a finding the draft already had -- but the same finding made worse is the answer's.
230
- Matching by code, part and axis alone excused a part pushed further out of the declared room.
223
+ Matching by code, part and axis alone excused a part pushed further out of the declared room; the room is computed
224
+ now (ADR-0145), and a mounting face further off its plane is the same case.
231
225
  */
232
226
  describe('a finding the draft had is excused only while it is no worse', () => {
233
227
  const outside = (() => {
234
228
  let asset = addFigureBox(createFigureAsset('Mixer'), { id: 'body', size: { x: 300, y: 400, z: 300 }, position: { x: 0, y: 200, z: 0 } })
235
- asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset), placement: 'floor' })
236
- return addFigureBox(asset, { id: 'sign', size: { x: 100, y: 100, z: 40 }, position: { x: 300, y: 200, z: 0 } })
229
+ asset = applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' })
230
+ asset = addFigureBox(asset, { id: 'sign', size: { x: 100, y: 100, z: 40 }, position: { x: 300, y: 250, z: 0 } })
231
+ return applyFigureAuthoring(asset, { kind: 'set-mounting-face', part: 'sign', face: 'bottom' })
237
232
  })()
238
233
 
239
234
  it('the draft has the finding to begin with', () => {
240
- expect(inspectFigureAsset(outside).violations.map(v => `${v.code} ${v.part} ${v.axis} ${v.mm}`)).toEqual(['PART_OUTSIDE_OCCUPANCY sign x 200'])
235
+ expect(inspectFigureAsset(outside).violations.map(v => `${v.code} ${v.part} ${v.axis} ${v.mm}`)).toEqual(['PARTS_OFF_PLACEMENT_FACE sign y 200'])
241
236
  })
242
237
 
243
238
  it('left as it was, or made smaller, it is excused', () => {
244
239
  expect(judgeFigureAnswer(outside, { steps: [{ kind: 'rename', name: 'Mixer 2' }] }, true).reasons).toEqual([])
245
- expect(judgeFigureAnswer(outside, { steps: [{ kind: 'nudge-part', part: 'sign', by: { x: -20 } }] }, true).reasons).toEqual([])
240
+ expect(judgeFigureAnswer(outside, { steps: [{ kind: 'nudge-part', part: 'sign', by: { y: -20 } }] }, true).reasons).toEqual([])
246
241
  })
247
242
 
248
243
  it('pushed further out, it is sent back as the answer\'s', () => {
249
- const judged = judgeFigureAnswer(outside, { steps: [{ kind: 'nudge-part', part: 'sign', by: { x: 100 } }] }, true)
244
+ const judged = judgeFigureAnswer(outside, { steps: [{ kind: 'nudge-part', part: 'sign', by: { y: 100 } }] }, true)
250
245
  expect(judged.source).toBeUndefined()
251
- expect(judged.reasons).toEqual([expect.stringMatching(/^PART_OUTSIDE_OCCUPANCY \(sign\): .*worse than the 200 mm the figure already had/)])
246
+ expect(judged.reasons).toEqual([expect.stringMatching(/^PARTS_OFF_PLACEMENT_FACE \(sign\): .*worse than the 200 mm the figure already had/)])
252
247
  })
253
248
  })
254
249
 
@@ -92,8 +92,7 @@ export const WORKED_EXAMPLE: FigureProposalStep[] = [
92
92
  { kind: 'add-part', part: { id: 'roller', kind: 'box', dimensions: { width: 60, height: 60, depth: 560 } } },
93
93
  { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } },
94
94
  { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 160 },
95
- { kind: 'set-material', target: 'frame', material: { token: 'palette.frame' }, palette: { 'palette.frame': '#5b6770' } },
96
- { kind: 'fit-room', placement: 'floor' }
95
+ { kind: 'set-material', target: 'frame', material: { token: 'palette.frame' }, palette: { 'palette.frame': '#5b6770' } }
97
96
  ] as FigureProposalStep[]
98
97
 
99
98
  /** The rules and the words, all from figure-model's constants. */
@@ -114,8 +113,7 @@ export function rulesPromptFigure(): string {
114
113
  '{ kind: "set-material", target, material: { token }, palette: { [token]: "#rrggbb" } }.',
115
114
  'When the request says a measure is the same as, matches or follows another part or the figure, write link-dimension:',
116
115
  'a copied number stays the same when the other one is resized.',
117
- 'Declare the sizes the figure may be placed at before any measure follows the figure. End with fit-room when parts,',
118
- 'their fastenings or motions changed.',
116
+ 'Declare the sizes the figure may be placed at before any measure follows the figure.',
119
117
  'When revising, write only the commands the request needs. Everything not touched stays as it is.',
120
118
  '',
121
119
  'The commands (shape, then what it means):',
@@ -216,7 +214,7 @@ function partsAtRefusedStep(base: FigureAsset, steps: FigureProposalStep[], erro
216
214
  /**
217
215
  * Applies an answer on a copy of the base and returns the figure, or the reasons it was not accepted: the step the
218
216
  * kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the
219
- * answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).
217
+ * answer's; a new figure has none to excuse, so it must come out whole.
220
218
  */
221
219
  export function judgeFigureAnswer(
222
220
  base: FigureAsset,
@@ -12,7 +12,7 @@ const shell = require('@things-factory/shell')
12
12
  const authBase = require('@things-factory/auth-base')
13
13
  const settingBase = require('@things-factory/setting-base')
14
14
  const attachmentBase = require('@things-factory/attachment-base')
15
- const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, proposeFigureOccupancy, readFigurePackage } = require('@hatiolab/figure-model')
15
+ const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, readFigurePackage } = require('@hatiolab/figure-model')
16
16
 
17
17
  let dataSource: DataSource | null = null
18
18
  let Figure: any
@@ -65,7 +65,7 @@ afterAll(async () => {
65
65
  const releasable = (label = 'Mixer') => {
66
66
  let a = addFigureBox(createFigureAsset(label), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
67
67
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
68
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), placement: 'floor' }))
68
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
69
69
  }
70
70
 
71
71
  let seq = 0
@@ -442,8 +442,9 @@ async function releaseNumbered(figure: Figure, version: number): Promise<{ figur
442
442
  /**
443
443
  * Finds the released source of a type in one domain, or null.
444
444
  *
445
- * A released row gives its own source; a draft row gives the source of its latest released history.
446
- * A figure that was never released gives null, because drafts never reach consumers.
445
+ * Always the latest release's own copy, from history: it carries the size range written down at release (ADR-0087
446
+ * decision 6), and the figure's row -- released or a draft -- keeps only the ends its author narrowed. A figure that
447
+ * was never released gives null, because drafts never reach consumers.
447
448
  */
448
449
  async function releasedSourceOf(
449
450
  domainId: string,
@@ -452,10 +453,6 @@ async function releasedSourceOf(
452
453
  const figure = await getRepository(Figure).findOne({ where: { domain: { id: domainId }, type } })
453
454
  if (!figure) return null
454
455
 
455
- if (figure.state === 'released' && figure.source) {
456
- return { figure, source: figure.source, version: figure.version }
457
- }
458
-
459
456
  const latestReleased = await getRepository(FigureHistory).findOne({
460
457
  where: { domain: { id: domainId }, originalId: figure.id, state: 'released' },
461
458
  order: { version: 'DESC' }
@@ -3,14 +3,14 @@
3
3
  * reader's words, and their findings join the gate's -- a blocking one blocks, a said one does not; a check that
4
4
  * throws blocks by name rather than being skipped; one taken off is gone.
5
5
  */
6
- import { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, proposeFigureOccupancy } from '@hatiolab/figure-model'
6
+ import { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring } from '@hatiolab/figure-model'
7
7
 
8
8
  import { inspectSource, registerFigureReleaseCheck } from './figure-format'
9
9
 
10
10
  const source = () => {
11
11
  let asset = addFigureBox(createFigureAsset('Mixer'), { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
12
12
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
13
- return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }), placement: 'floor' }))
13
+ return serializeFigureAsset(applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' }))
14
14
  }
15
15
  const codes = (inspection: { findings: { code: string; blocking: boolean }[] }) => inspection.findings.map(f => [f.code, f.blocking])
16
16
 
@@ -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, proposeFigureOccupancy, palletizerRobotFigure, FIGURE_KERNEL_VERSION } = require('@hatiolab/figure-model')
11
+ const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, palletizerRobotFigure, FIGURE_KERNEL_VERSION } = require('@hatiolab/figure-model')
12
12
 
13
13
  let FigureNamespace: any
14
14
  let dataSource: DataSource | null = null
@@ -69,7 +69,7 @@ async function release(extra: Record<string, unknown>) {
69
69
 
70
70
  describe('a release record on its history row', () => {
71
71
  it('keeps the format and the changes as they were', async () => {
72
- const changes = [{ code: 'occupancy-changed', blocking: false, values: { before: 'x -50…50', after: 'x -150…150' }, detail: 'declared room changed' }]
72
+ const changes = [{ code: 'size-changed', blocking: false, values: { before: 'x -50…50', after: 'x -150…150' }, detail: 'size changed' }]
73
73
  const row = await release({ __releaseFormat: 'figure-asset-1', __releaseChanges: changes })
74
74
  expect(row.assetFormat).toBe('figure-asset-1')
75
75
  expect(JSON.parse(row.changes)).toEqual(changes)
@@ -90,9 +90,9 @@ describe('releaseFigure records it', () => {
90
90
  const releasable = (width: number) => {
91
91
  let a = addFigureBox(createFigureAsset('Mixer'), { id: 'body', size: { x: width, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
92
92
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' } })
93
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), placement: 'floor' }))
93
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
94
94
  }
95
- it('first release: [], the second: the wider room', async () => {
95
+ it('first release: [], the second: the wider body', async () => {
96
96
  const domain = await namespaced(await dataSource!.getRepository(shell.Domain).save({ name: 'release-door', subdomain: 'release-door' }))
97
97
  const tx = dataSource!.manager
98
98
  const context = { state: { domain, user: undefined, tx } }
@@ -106,9 +106,10 @@ describe('releaseFigure records it', () => {
106
106
  /* Each release names the kernel that measured it (ADR-0109 C1). */
107
107
  expect(rows.map((r: any) => r.kernelVersion)).toEqual([FIGURE_KERNEL_VERSION, FIGURE_KERNEL_VERSION])
108
108
  const codes = JSON.parse(rows[1].changes).map((c: any) => c.code)
109
- expect(codes).toContain('occupancy-changed')
109
+ expect(codes).toEqual(expect.arrayContaining(['size-changed', 'motion-range-changed']))
110
110
  /* What each release measured, and its plan from above, are on its own row. */
111
- expect(JSON.parse(rows[0].metrics)).toEqual({ origin: { centred: true, off: [] } })
111
+ const box = { x: { min: -50, max: 50 }, y: { min: 0, max: 100 }, z: { min: -50, max: 50 } }
112
+ expect(JSON.parse(rows[0].metrics)).toEqual({ body: box, motionRange: box })
112
113
  expect(JSON.parse(rows[1].topView).shapes.map((s: any) => s.id)).toEqual(['body'])
113
114
  })
114
115
  })
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The size range a board reads is the release's own copy (ADR-0087 decision 10, user 2026-10-21). The figure's row keeps
3
+ * only the ends its author narrowed; a release writes the rest down as far as the parts allow, on its history row. Every
4
+ * door a release leaves by -- one figure for a board, several by full name, the package another server imports -- hands
5
+ * out that copy, never the row. A release made a draft again opens what the release wrote down.
6
+ */
7
+ import type { DataSource } from 'typeorm'
8
+
9
+ const { DataSource: TypeOrmDataSource } = require('typeorm')
10
+ const { createHash } = require('crypto')
11
+ const JSZip = require('jszip')
12
+ const shell = require('@things-factory/shell')
13
+ const authBase = require('@things-factory/auth-base')
14
+ const settingBase = require('@things-factory/setting-base')
15
+ const { createFigureAsset, addFigureBox, serializeFigureAsset, applyFigureAuthoring, readFigurePackage } = require('@hatiolab/figure-model')
16
+
17
+ let dataSource: DataSource | null = null
18
+ let Figure: any
19
+ let FigureMutation: any
20
+ let FigureQuery: any
21
+ let figurePackageOf: any
22
+ let revertKeepingDraft: any
23
+ let FigureHistory: any
24
+ let FigureNamespace: any
25
+
26
+ function loadCompiled(modulePath: string): any {
27
+ const { existsSync } = require('fs')
28
+ const { join } = require('path')
29
+ const resolved = join(__dirname, '..', '..', '..', 'dist-server', `${modulePath}.js`)
30
+ if (!existsSync(resolved))
31
+ throw new Error(`figure-service 산출물이 없다: ${resolved}\n먼저 빌드할 것:\n yarn workspace @things-factory/figure-service build:server`)
32
+ return require(resolved)
33
+ }
34
+
35
+ beforeAll(async () => {
36
+ Figure = loadCompiled('service/figure/figure').Figure
37
+ FigureHistory = loadCompiled('service/figure/figure-history').FigureHistory
38
+ FigureNamespace = loadCompiled('service/figure/figure-namespace-entity').FigureNamespace
39
+ const FigureReplacedDraft = loadCompiled('service/figure/figure-replaced-draft').FigureReplacedDraft
40
+ const Subscriber = loadCompiled('service/figure/event-subscriber').FigureHistoryEntitySubscriber
41
+ ;({ FigureMutation } = loadCompiled('service/figure/figure-mutation'))
42
+ ;({ FigureQuery } = loadCompiled('service/figure/figure-query'))
43
+ ;({ figurePackageOf } = loadCompiled('service/figure/figure-package-export'))
44
+ ;({ revertKeepingDraft } = loadCompiled('service/figure/figure-revert'))
45
+ dataSource = new TypeOrmDataSource({
46
+ type: 'sqlite',
47
+ database: ':memory:',
48
+ synchronize: true,
49
+ dropSchema: true,
50
+ namingStrategy: new shell.NamingStrategy(),
51
+ entities: [...shell.entities, ...authBase.entities, ...settingBase.entities, Figure, FigureHistory, FigureNamespace, FigureReplacedDraft],
52
+ subscribers: [Subscriber],
53
+ logging: false
54
+ })
55
+ await dataSource.initialize()
56
+ shell.addDataSource('default', dataSource)
57
+ shell.addDataSource('tx', dataSource)
58
+ })
59
+
60
+ afterAll(async () => {
61
+ await dataSource?.destroy()
62
+ dataSource = null
63
+ })
64
+
65
+ /*
66
+ The user's figure (2026-10-21): a box that follows the figure's size, with a cavity ticked on it. Below twice its wall it
67
+ has no inside, so its width has a smallest size its parts set; nothing limits how large it grows. The height is
68
+ narrowed by the author, 50 to 400 mm.
69
+ */
70
+ const buttonBox = () => {
71
+ let a = addFigureBox(createFigureAsset('Button box'), { id: 'box', size: { x: 120, y: 60, z: 80 }, position: { x: 0, y: 30, z: 0 } })
72
+ a = applyFigureAuthoring(a, { kind: 'attach', part: 'box', to: { plane: 'mounting-plane' } })
73
+ a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'box' })
74
+ a = applyFigureAuthoring(a, { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: { closed: 10 }, end: 'open' } })
75
+ a = applyFigureAuthoring(a, { kind: 'declare-instance-size', range: { y: { min: 50, max: 400 } }, replace: true })
76
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
77
+ }
78
+ const sizeOf = (source: string | object, axis: 'x' | 'y' | 'z') => {
79
+ const asset = typeof source === 'string' ? JSON.parse(source) : source
80
+ const { min, max } = asset.document.model.inputs.find((i: any) => i.id === `size.${axis}`)
81
+ return { min, max }
82
+ }
83
+ /** The width's smallest size as the parts set it: twice the wall, and the kernel's numerical step above it. */
84
+ const widthFloor = (min: number | undefined) => typeof min === 'number' && min > 20 && min < 20.001
85
+
86
+ let seq = 0
87
+ async function domainWith(namespace: string) {
88
+ const name = `size-${++seq}`
89
+ const domain = await dataSource!.getRepository(shell.Domain).save({ name, subdomain: name })
90
+ await dataSource!.getRepository(FigureNamespace).save({ domain, name: namespace, state: 'active', isDefault: true })
91
+ return { domain, context: { state: { domain, user: undefined, tx: dataSource!.manager } } }
92
+ }
93
+ const creationId = () => require('crypto').randomUUID()
94
+
95
+ async function released(type: string) {
96
+ const ns = `size-${++seq}.hatiolab.com`
97
+ const { domain, context } = await domainWith(ns)
98
+ const saved = await new FigureMutation().saveFigure(buttonBox(), context, undefined, undefined, creationId(), undefined, type, undefined, undefined)
99
+ await new FigureMutation().releaseFigure(saved.id, 'first', context)
100
+ const figure = await dataSource!.getRepository(Figure).findOneOrFail({ where: { id: saved.id } })
101
+ const history = await dataSource!.getRepository(FigureHistory).findOneOrFail({ where: { originalId: saved.id, version: 1 } })
102
+ return { ns, domain, context, figure, history }
103
+ }
104
+
105
+ describe('the size range a release carries', () => {
106
+ it('is written on the release, and the figure keeps only what its author narrowed', async () => {
107
+ const { figure, history } = await released('button-box')
108
+ expect(figure.state).toBe('released')
109
+ expect(sizeOf(figure.source, 'x')).toEqual({ min: undefined, max: undefined })
110
+ expect(sizeOf(figure.source, 'y')).toEqual({ min: 50, max: 400 })
111
+ const x = sizeOf(history.source, 'x')
112
+ expect(widthFloor(x.min)).toBe(true)
113
+ expect(x.max).toBeUndefined()
114
+ expect(sizeOf(history.source, 'y')).toEqual({ min: 50, max: 400 })
115
+ })
116
+
117
+ it('is what every door a release leaves by hands out: one for a board, several by full name, a package', async () => {
118
+ const { ns, domain, context, history } = await released('button-box')
119
+ const one = await new FigureQuery().figureBlueprint('button-box', context)
120
+ expect(widthFloor(sizeOf(one.asset, 'x').min)).toBe(true)
121
+ const pinned = await new FigureQuery().figureBlueprint('button-box', context, 1)
122
+ expect(widthFloor(sizeOf(pinned.asset, 'x').min)).toBe(true)
123
+ const several = await new FigureQuery().figureBlueprints([`${ns}/button-box`], context)
124
+ expect(widthFloor(sizeOf(several.items[0].asset, 'x').min)).toBe(true)
125
+
126
+ const { bytes } = await figurePackageOf({
127
+ refs: [`${ns}/button-box@1`],
128
+ domain,
129
+ exportedBy: 'author',
130
+ sourceUrl: 'https://figure.hatiolab.com',
131
+ files: { put: async () => ({}), send: async () => {}, read: async () => Buffer.alloc(0) }
132
+ })
133
+ const zip = await JSZip.loadAsync(bytes)
134
+ const files = new Map<string, Uint8Array>()
135
+ for (const path of Object.keys(zip.files)) if (!zip.files[path].dir) files.set(path, await zip.files[path].async('uint8array'))
136
+ const read = readFigurePackage(files, (b: Uint8Array) => createHash('sha256').update(b).digest('hex'))
137
+ expect(read.releases[0].source).toBe(history.source)
138
+ })
139
+
140
+ it('released again unchanged, nothing is said to have narrowed', async () => {
141
+ const { figure, context } = await released('button-box')
142
+ await dataSource!.getRepository(Figure).save({ ...figure, state: 'draft' })
143
+ await new FigureMutation().releaseFigure(figure.id, 'again', context)
144
+ const second = await dataSource!.getRepository(FigureHistory).findOneOrFail({ where: { originalId: figure.id, version: 2 } })
145
+ expect(JSON.parse(second.changes)).toEqual([])
146
+ })
147
+
148
+ it('made a draft again by a revert, what the release wrote down is opened and the author keeps theirs', async () => {
149
+ const { figure, domain } = await released('button-box')
150
+ const draft = await dataSource!.getRepository(Figure).save({ ...figure, state: 'draft' })
151
+ await dataSource!.transaction(tx => revertKeepingDraft(tx, domain, undefined, { id: figure.id, version: 1, requestId: creationId(), expectedSource: draft.source }))
152
+ const back = await dataSource!.getRepository(Figure).findOneOrFail({ where: { id: figure.id } })
153
+ expect(sizeOf(back.source, 'x')).toEqual({ min: undefined, max: undefined })
154
+ expect(sizeOf(back.source, 'y')).toEqual({ min: 50, max: 400 })
155
+ })
156
+ })
@@ -5,7 +5,7 @@ import { GraphQLError } from 'graphql'
5
5
  import { Figure } from './figure.js'
6
6
  import { FigureHistory } from './figure-history.js'
7
7
  import { FigureReplacedDraft } from './figure-replaced-draft.js'
8
- import { parseSource } from './figure-format.js'
8
+ import { draftSourceOfRelease, parseSource } from './figure-format.js'
9
9
  import { sourceConflict } from './figure-write.js'
10
10
 
11
11
  /*
@@ -77,7 +77,7 @@ export async function revertKeepingDraft(
77
77
  })
78
78
  if (!history) throw refused('FIGURE_VERSION_NOT_FOUND', `Figure '${figure.type}' 에 ${request.version} 판이 없습니다.`)
79
79
  /* The format is checked again: an old version may break today's rules, and is not let in quietly. */
80
- const { source, derived, violations } = parseSource(history.source)
80
+ const { source, derived, violations } = parseSource(draftSourceOfRelease(history.source!))
81
81
 
82
82
  const record = await keep(kept, {
83
83
  domain: { id: domain.id },
@@ -42,11 +42,11 @@ function stubRepository(rows: any[]) {
42
42
  const MINE = { id: 'domain-mine' }
43
43
  const THEIRS = { id: 'domain-theirs' }
44
44
 
45
- /** A stored figure: one body on the floor, its room declared. */
45
+ /** A stored figure: one body on the floor. */
46
46
  const SOURCE = (() => {
47
47
  const model = require('@hatiolab/figure-model')
48
48
  let asset = model.addFigureBox(model.createFigureAsset('Mixer'), { id: 'body', size: { x: 300, y: 400, z: 300 }, position: { x: 0, y: 200, z: 0 } })
49
- asset = model.applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: model.proposeFigureOccupancy(asset), placement: 'floor' })
49
+ asset = model.applyFigureAuthoring(asset, { kind: 'set-placement', placement: 'floor' })
50
50
  return asset
51
51
  })()
52
52
 
@@ -176,7 +176,7 @@ export class FigureRevertResult {
176
176
  */
177
177
  @ObjectType({ description: 'One finding from the release gate.' })
178
178
  export class FigureGateFinding {
179
- @Field({ description: 'Stable code of the gate finding, for example part-outside-occupancy or parts-split-on-resize.' })
179
+ @Field({ description: 'Stable code of the gate finding, for example parts-off-placement-face or parts-split-on-resize.' })
180
180
  code: string
181
181
 
182
182
  @Field({ description: 'Human-readable message.' })
@@ -1,14 +1,6 @@
1
1
  {
2
2
  "error.figure-proposal-failed": "The figure could not be made.",
3
3
  "error.figure-release-does-not-compile": "the released source of {type} (version {version}) does not compile. open it in the modeller, fix it and release it again before it draws on a board.",
4
- "figure.gate.occupancy-empty-axis.message": "The space the figure takes is empty on {axis}.",
5
- "figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to set the space the figure takes from the parts as they are now.",
6
- "figure.gate.occupancy-undeclared.how-no-parts": "Add a shape first, then use \"fit to parts\" to set the space the figure takes from it.",
7
- "figure.gate.occupancy-undeclared.message": "The space this figure takes is not set yet.",
8
- "figure.gate.occupancy-undeclared.why": "The space a figure takes sets the scale it is placed at on a board.",
9
- "figure.gate.part-outside-occupancy.how": "Use \"fit to parts\" to fit the space the figure takes to the parts again, or move '{part}' {mm} mm inward on {axis}.",
10
- "figure.gate.part-outside-occupancy.message": "'{part}' goes {mm} mm past the space the figure takes on {axis}.",
11
- "figure.gate.part-outside-occupancy.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.",
12
4
  "figure.gate.parts-off-placement-face.how": "Move '{part}' {mm} mm on Y so the face meets the plane, or choose a different mounting face.",
13
5
  "figure.gate.parts-off-placement-face.message": "The mounting face of '{part}' is {mm} mm off the plane the figure stands on.",
14
6
  "figure.gate.parts-off-placement-face.why": "The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.",
@@ -21,9 +13,6 @@
21
13
  "figure.gate.placement-face-tilted.how": "Choose a mounting face parallel to the plane, or turn '{part}' upright.",
22
14
  "figure.gate.placement-face-tilted.message": "The mounting face of '{part}' is tilted {degrees}° from the plane the figure stands on.",
23
15
  "figure.gate.placement-face-tilted.why": "A tilted face cannot lie on the plane, so how the figure stands is not settled.",
24
- "figure.gate.state-range-sampled.how": "To cover the whole range, re-author what could not be proven.",
25
- "figure.gate.state-range-sampled.message": "The whole range of sizes and motion could not be proven, so {states} combinations of size and pose were checked.",
26
- "figure.gate.state-range-sampled.why": "Between the combinations that were checked, a part may still go past the space the figure takes.",
27
16
  "figure.gate.type-is-placeholder.how": "Set the type in the type field, then publish.",
28
17
  "figure.gate.type-is-placeholder.message": "'{type}' is a temporary type given when the figure was first saved.",
29
18
  "figure.gate.type-is-placeholder.why": "Boards call a figure by its type, and it cannot change once the figure is published.",
@@ -66,8 +55,10 @@
66
55
  "figure.gate.contract.capability-gone.why": "Scenes attach behaviour by this name. Placed instances lose what it did.",
67
56
  "figure.gate.contract.capability-added.message": "The capability '{name}' is new.",
68
57
  "figure.gate.contract.capability-added.why": "It adds what the figure can do. Placed instances stand as they are.",
69
- "figure.gate.contract.occupancy-changed.message": "The room the figure takes changed: {before} → {after}.",
70
- "figure.gate.contract.occupancy-changed.why": "A consumer places the figure by this room. One that moves to this release sees it take the new room.",
58
+ "figure.gate.contract.size-changed.message": "The figure's size changed: {before} → {after}.",
59
+ "figure.gate.contract.size-changed.why": "A board gives the figure a box of this size. One that moves to this release sees its box change.",
60
+ "figure.gate.contract.motion-range-changed.message": "The room the figure sweeps as it moves changed: {before} → {after}.",
61
+ "figure.gate.contract.motion-range-changed.why": "It is what the figure reaches when it moves. One that moves to this release sees it reach differently.",
71
62
  "figure.gate.contract.contact-face-changed.message": "The face the figure is mounted by changed.",
72
63
  "figure.gate.contract.contact-face-changed.why": "A consumer that moves to this release sees it stand on the new face.",
73
64
  "figure.gate.contract.public-input-gone.message": "'{input}' is no longer set on a board.",
@@ -94,9 +85,6 @@
94
85
  "figure.gate.contract.joint-list-unread.message": "The last release's joint list could not be read, so its joints were not compared: {reason}",
95
86
  "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.",
96
87
  "error.figure-release-not-found": "{type} has no release {version}. its latest release is {latest}.",
97
- "figure.gate.origin-off-centre.message": "The origin is off the middle of the declared room: {off}.",
98
- "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.",
99
- "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.",
100
88
  "figure.gate.off-mounting-plane.floor.clear.message": "'{part}' is {mm} mm above the floor.",
101
89
  "figure.gate.off-mounting-plane.floor.through.message": "'{part}' goes {mm} mm below the floor.",
102
90
  "figure.gate.off-mounting-plane.floor.beyond.message": "Every part is below the floor.",
@@ -1,14 +1,6 @@
1
1
  {
2
2
  "error.figure-proposal-failed": "No se pudo crear la figura.",
3
3
  "error.figure-release-does-not-compile": "el código fuente publicado de {type} (versión {version}) no compila. Ábralo en el modelador, corríjalo y vuelva a publicarlo para que se dibuje en un tablero.",
4
- "figure.gate.occupancy-empty-axis.message": "El espacio declarado está vacío en {axis}.",
5
- "figure.gate.occupancy-undeclared.how": "Use «ajustar a las piezas» para declarar el espacio a partir de las piezas tal como están ahora.",
6
- "figure.gate.occupancy-undeclared.how-no-parts": "Añada primero una forma y luego use «ajustar a las piezas» para declarar el espacio a partir de ella.",
7
- "figure.gate.occupancy-undeclared.message": "Esta figura no ha declarado el espacio que ocupa.",
8
- "figure.gate.occupancy-undeclared.why": "Sin un espacio declarado no se puede fijar la escala con la que se coloca una figura.",
9
- "figure.gate.part-outside-occupancy.how": "Use «ajustar a las piezas» para volver a declarar el espacio a partir de las piezas, o mueva '{part}' {mm} mm hacia dentro en {axis}.",
10
- "figure.gate.part-outside-occupancy.message": "'{part}' sobrepasa {mm} mm el espacio declarado en {axis}.",
11
- "figure.gate.part-outside-occupancy.why": "El espacio declarado fija la escala con la que se coloca una figura. Lo que queda fuera no entra en esa escala, así que en un tablero esta pieza queda fuera del lugar asignado a la figura.",
12
4
  "figure.gate.parts-off-placement-face.how": "Mueva '{part}' {mm} mm en Y para que la cara toque el plano, o elija otra cara de montaje.",
13
5
  "figure.gate.parts-off-placement-face.message": "La cara de montaje de '{part}' está a {mm} mm del plano sobre el que se apoya la figura.",
14
6
  "figure.gate.parts-off-placement-face.why": "La figura está declarada para apoyarse sobre esa cara. Si la cara no llega al plano, la figura flota esa distancia en el tablero, y lo que se coloque sobre ella también flota.",
@@ -21,9 +13,6 @@
21
13
  "figure.gate.placement-face-tilted.how": "Elija una cara de montaje paralela al plano o ponga '{part}' en posición vertical.",
22
14
  "figure.gate.placement-face-tilted.message": "La cara de montaje de '{part}' está inclinada {degrees}° respecto al plano sobre el que se apoya la figura.",
23
15
  "figure.gate.placement-face-tilted.why": "Una cara inclinada no puede apoyarse sobre el plano, por lo que no queda definida la postura de la figura.",
24
- "figure.gate.state-range-sampled.how": "Para cubrir todo el rango, vuelva a diseñar lo que no se pudo demostrar.",
25
- "figure.gate.state-range-sampled.message": "No se pudo demostrar todo el rango de tamaños y movimientos, así que se comprobaron {states} combinaciones de tamaño y postura.",
26
- "figure.gate.state-range-sampled.why": "Entre las combinaciones comprobadas, una pieza aún podría sobrepasar el espacio declarado.",
27
16
  "figure.gate.type-is-placeholder.how": "Defina el tipo en el campo de tipo y luego publique.",
28
17
  "figure.gate.type-is-placeholder.message": "'{type}' es un tipo provisional asignado cuando se guardó la figura por primera vez.",
29
18
  "figure.gate.type-is-placeholder.why": "Los tableros llaman a una figura por su tipo, y no se puede cambiar una vez publicada la figura.",
@@ -66,8 +55,10 @@
66
55
  "figure.gate.contract.capability-gone.why": "Las escenas asocian comportamientos mediante este nombre. Las instancias colocadas pierden lo que hacía.",
67
56
  "figure.gate.contract.capability-added.message": "La capacidad '{name}' es nuevo.",
68
57
  "figure.gate.contract.capability-added.why": "Amplía lo que puede hacer la figura. Las instancias colocadas se quedan como están.",
69
- "figure.gate.contract.occupancy-changed.message": "Cambió el espacio que ocupa la figura: {before} → {after}.",
70
- "figure.gate.contract.occupancy-changed.why": "Quien usa la figura la coloca según este espacio. Quien pase a esta publicación verá que ocupa el nuevo espacio.",
58
+ "figure.gate.contract.size-changed.message": "El tamaño de la figura cambió: {before} → {after}.",
59
+ "figure.gate.contract.size-changed.why": "Un tablero le da a la figura una caja de este tamaño. Al pasar a esta versión, su caja cambia.",
60
+ "figure.gate.contract.motion-range-changed.message": "El espacio que la figura recorre al moverse cambió: {before} → {after}.",
61
+ "figure.gate.contract.motion-range-changed.why": "Es lo que la figura alcanza al moverse. Al pasar a esta versión, alcanza otro espacio.",
71
62
  "figure.gate.contract.contact-face-changed.message": "Cambió la cara por la que se monta la figura.",
72
63
  "figure.gate.contract.contact-face-changed.why": "Quien pase a esta publicación verá que se apoya sobre la nueva cara.",
73
64
  "figure.gate.contract.public-input-gone.message": "'{input}' ya no se define en un tablero.",
@@ -94,9 +85,6 @@
94
85
  "figure.gate.contract.joint-list-unread.message": "No se pudo leer la lista de articulaciones de la última publicación, por lo que no se compararon sus articulaciones: {reason}",
95
86
  "figure.gate.contract.joint-list-unread.why": "No se ha comprobado si esta publicación cambia lo que mueven los valores de un controlador. La última publicación se generó con un núcleo que no medía las articulaciones.",
96
87
  "error.figure-release-not-found": "{type} no tiene la publicación {version}. Su última publicación es {latest}.",
97
- "figure.gate.origin-off-centre.message": "El origen está desplazado del centro del espacio declarado: {off}.",
98
- "figure.gate.origin-off-centre.why": "Un plano que coloca las figuras por el centro de su espacio sitúa esta a esa distancia de donde está dibujada.",
99
- "figure.gate.origin-off-centre.how": "Manténgalo si la figura se fija en ese punto, como una puerta por su bisagra. Mueva el origen al centro para colocarla por su espacio.",
100
88
  "figure.gate.off-mounting-plane.floor.clear.message": "'{part}' está {mm} mm por encima del suelo.",
101
89
  "figure.gate.off-mounting-plane.floor.through.message": "'{part}' baja {mm} mm por debajo del suelo.",
102
90
  "figure.gate.off-mounting-plane.floor.beyond.message": "Todas las piezas están por debajo del suelo.",