@things-factory/figure-ui 10.1.206 → 10.1.208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/client/modeller/editor.ts +11 -97
  2. package/client/modeller/figure-facts.ts +1 -1
  3. package/client/modeller/figure-history-panel.ts +1 -29
  4. package/client/modeller/figure-preview.ts +5 -7
  5. package/client/modeller/figure-verdict.ts +2 -17
  6. package/client/modeller/maturity.ts +11 -27
  7. package/client/modeller/refusals.ts +1 -7
  8. package/client/pages/figure-modeller-page.ts +7 -22
  9. package/dist-client/modeller/editor.d.ts +1 -8
  10. package/dist-client/modeller/editor.js +12 -98
  11. package/dist-client/modeller/editor.js.map +1 -1
  12. package/dist-client/modeller/figure-facts.js +1 -1
  13. package/dist-client/modeller/figure-facts.js.map +1 -1
  14. package/dist-client/modeller/figure-fasten-dialog.d.ts +1 -1
  15. package/dist-client/modeller/figure-history-panel.d.ts +0 -16
  16. package/dist-client/modeller/figure-history-panel.js +1 -27
  17. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  18. package/dist-client/modeller/figure-links.d.ts +1 -1
  19. package/dist-client/modeller/figure-preview.js +5 -8
  20. package/dist-client/modeller/figure-preview.js.map +1 -1
  21. package/dist-client/modeller/figure-verdict.d.ts +0 -5
  22. package/dist-client/modeller/figure-verdict.js +2 -21
  23. package/dist-client/modeller/figure-verdict.js.map +1 -1
  24. package/dist-client/modeller/maturity.d.ts +2 -4
  25. package/dist-client/modeller/maturity.js +12 -26
  26. package/dist-client/modeller/maturity.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -3
  28. package/dist-client/modeller/refusals.js +1 -7
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/pages/figure-modeller-page.d.ts +1 -3
  31. package/dist-client/pages/figure-modeller-page.js +7 -23
  32. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  33. package/dist-client/route.d.ts +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +8 -8
  36. package/test/browser/acceptance-page.js +3 -3
  37. package/test/browser/detail-page.js +1 -3
  38. package/test/browser/detail-tabs-page.js +1 -2
  39. package/test/browser/links.check.mjs +2 -37
  40. package/test/browser/modeller.check.mjs +1 -3
  41. package/test/browser/parts-page.js +2 -2
  42. package/test/browser/parts.check.mjs +2 -12
  43. package/test/browser/product-page.check.mjs +6 -9
  44. package/test/browser/product-page.js +1 -2
  45. package/test/browser/screen-page.js +2 -6
  46. package/test/browser/screen.check.mjs +8 -65
  47. package/test/first-minute.test.ts +2 -3
  48. package/test/fixtures/joint-column.json +1 -66
  49. package/test/fixtures/joint-roller.json +1 -66
  50. package/test/fixtures/robot-arm-6dof.json +2 -67
  51. package/test/harness-serve.test.ts +1 -1
  52. package/test/heading-scale-guard.test.ts +3 -3
  53. package/test/links.test.ts +1 -1
  54. package/test/parts-wiring.test.ts +1 -1
  55. package/test/properties.test.ts +9 -12
  56. package/test/refusals.test.ts +3 -21
  57. package/test/resize-with-placement.test.ts +13 -15
  58. package/test/revision.test.ts +1 -2
  59. package/test/verdict.test.ts +1 -15
  60. package/translations/en.json +0 -19
  61. package/translations/es.json +0 -19
  62. package/translations/ja.json +0 -19
  63. package/translations/ko.json +9 -28
  64. package/translations/ms.json +0 -19
  65. package/translations/zh.json +0 -19
  66. package/client/modeller/occupancy.ts +0 -51
  67. package/dist-client/modeller/occupancy.d.ts +0 -31
  68. package/dist-client/modeller/occupancy.js +0 -36
  69. package/dist-client/modeller/occupancy.js.map +0 -1
  70. package/test/release-panel.test.ts +0 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.206",
3
+ "version": "10.1.208",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.9.6",
31
- "@hatiolab/things-scene": "^10.1.95",
30
+ "@hatiolab/figure-model": "^0.10.0",
31
+ "@hatiolab/things-scene": "^10.1.99",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.29.43",
34
34
  "@operato/graphql": "^10.29.43",
@@ -42,11 +42,11 @@
42
42
  "@operato/slot-layout-editor": "^10.29.43",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.43",
45
- "@things-factory/ai-assistant": "^10.1.206",
46
- "@things-factory/figure-service": "^10.1.206",
47
- "@things-factory/figure-viewer": "^10.1.206",
48
- "@things-factory/setting-base": "^10.1.206",
45
+ "@things-factory/ai-assistant": "^10.1.208",
46
+ "@things-factory/figure-service": "^10.1.208",
47
+ "@things-factory/figure-viewer": "^10.1.208",
48
+ "@things-factory/setting-base": "^10.1.208",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "18588a058a4a6cb21c379b2da5517e077c6442ba"
51
+ "gitHead": "ffc1ceaa9471b9f4a9b86f46c3021f2fed72d17d"
52
52
  }
@@ -10,7 +10,7 @@ import 'figure-ui-client/modeller/figure-properties.js'
10
10
  import { createFigureProductSession } from 'figure-ui-client/modeller/product-session.js'
11
11
  import { runFigureProposal, takeFigureOffer } from 'figure-ui-client/modeller/ai-proposal.js'
12
12
  import { i18next } from '@operato/i18n'
13
- import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
13
+ import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
14
14
 
15
15
  const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
16
16
 
@@ -19,7 +19,7 @@ function box(placement, position) {
19
19
  let a = createFigureAsset(`Probe ${placement}`)
20
20
  a.palette = { ...a.palette, ...PALETTE }
21
21
  a = addFigureBox(a, { id: 'box', size: { x: 400, y: 400, z: 400 }, position })
22
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement }))
22
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement }))
23
23
  }
24
24
 
25
25
  function turned(rotation, placement = 'floor', position = { x: 0, y: 400, z: 0 }) {
@@ -91,7 +91,7 @@ export const MODELS = {
91
91
  a.palette = { ...a.palette, ...PALETTE }
92
92
  a = addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -400, y: 350, z: 0 } })
93
93
  a = addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 720, z: 0 } })
94
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' }))
94
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
95
95
  },
96
96
  /* A ceiling figure's part off its ceiling, for the one-off align (to the ceiling, by its top). */
97
97
  'ceiling-off': () => box('ceiling', { x: 0, y: -600, z: 0 }),
@@ -1,7 +1,7 @@
1
1
  /* Isolated product detail: fixtures are in memory, never saved to the running app. */
2
2
  import 'figure-ui-client/viewparts/figure-detail-dock.js'
3
3
  import { i18next } from '@operato/i18n'
4
- import { addFigureBox, createFigureAsset, editFigurePart, applyFigureAuthoring, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
4
+ import { addFigureBox, createFigureAsset, editFigurePart, applyFigureAuthoring, serializeFigureAsset } from '@hatiolab/figure-model'
5
5
 
6
6
  async function start() {
7
7
  i18next.addResourceBundle('ko', 'translations', await fetch('/ko.json').then(r => r.json()), true, true)
@@ -14,8 +14,6 @@ async function start() {
14
14
  id: 'block', type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: 'block' }, axis: 'X', lowerLimit: 0, upperLimit: 200, state: { id: 'shift', start: 0 }
15
15
  } })
16
16
  asset = editFigurePart(asset, { kind: 'set-driver', state: 'shift', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 2, value: 200 }, { seconds: 4, value: 0 }] })
17
- /* Its room declared from its parts over the whole motion, as the modeller does before a release. */
18
- asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }) })
19
17
  const source = serializeFigureAsset(asset)
20
18
  const dock = document.createElement('figure-detail-dock')
21
19
  let fail = true
@@ -4,7 +4,7 @@
4
4
  * are in memory; nothing is saved.
5
5
  */
6
6
  import 'figure-ui-client/viewparts/figure-detail-dock.js'
7
- import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, proposeFigureOccupancy, robotDogFigure, serializeFigureAsset } from '@hatiolab/figure-model'
7
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, robotDogFigure, serializeFigureAsset } from '@hatiolab/figure-model'
8
8
 
9
9
  const PARTS = 12
10
10
 
@@ -17,7 +17,6 @@ for (let i = 0; i < PARTS; i++) {
17
17
  } })
18
18
  asset = editFigurePart(asset, { kind: 'set-driver', state: `${id}-z`, clip: `cycle-${i}`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 100 }, { seconds: 2, value: 0 }] })
19
19
  }
20
- asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }) })
21
20
  const source = serializeFigureAsset(asset)
22
21
 
23
22
  const dock = document.createElement('figure-detail-dock')
@@ -17,7 +17,6 @@ import {
17
17
  applyFigureProposal,
18
18
  createFigureAsset,
19
19
  planFigureResize,
20
- proposeFigureOccupancy,
21
20
  serializeFigureAsset
22
21
  } from '@hatiolab/figure-model'
23
22
 
@@ -683,52 +682,18 @@ describe('a part fastened twisted is sized by a drag: its size alone, its fasten
683
682
  describe('a box made to follow the figure size on the floor', () => {
684
683
  before(async () => {
685
684
  let a = box(createFigureAsset('Floor'), 'box', [100, 100, 100], [20, 50, 30])
686
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
685
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
687
686
  await mount('floor', applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps))
688
687
  await pick('box')
689
688
  })
690
689
 
691
- test('its height is the room\'s only, shown apart; its row shows the number the floor gives; a size drag is drawn by the modeller', async () => {
692
- const roomOnly = await all('input[aria-label="box.position.y"]')
693
- assert.equal(roomOnly.length, 1, 'one field for it, apart from the position row')
694
- assert.equal(roomOnly[0].disabled, true, 'not typed into')
695
- assert.equal((await all('section[room-only] input[aria-label="box.position.y"]')).length, 1, 'in the room\'s section')
690
+ test('its row shows the number the floor gives; a size drag is drawn by the modeller', async () => {
696
691
  const held = (await all('div[vector-row] input[readonly]')).filter(i => i.label?.startsWith('position.y'))
697
692
  assert.deepEqual(held.map(i => i.value), ['50'], 'the height the floor gives, read-only')
698
693
  assert.equal(await read(() => look.scalePlacement('box')), 'owner', 'its place in a size drag is the modeller\'s (ADR-0140 4.4)')
699
694
  })
700
-
701
- test('the room is declared again only when the author confirms', async () => {
702
- const prompt = id =>
703
- read(id => {
704
- const deep = (root = document) => [...root.querySelectorAll('ox-prompt')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
705
- const button = deep().at(-1)?.shadowRoot?.querySelector(`#${id}`)
706
- if (!button) return null
707
- const r = button.getBoundingClientRect()
708
- return r.width ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null
709
- }, id)
710
- const start = await state()
711
- await click(await at('button[room-refit]'), 'declare the room again')
712
- await until(() => !!document.querySelector('ox-prompt'), [], 'asked')
713
- await click(await poll(() => prompt('cancel')), 'cancel')
714
- await until(() => !document.querySelector('ox-prompt'), [], 'the question gone')
715
- assert.equal((await state()).revision, start.revision, 'cancelled: nothing written')
716
- await click(await at('button[room-refit]'), 'declare the room again')
717
- await click(await poll(() => prompt('confirm')), 'confirm')
718
- await until(r => probe.state().revision === r + 1, [start.revision], 'confirmed: one step')
719
- })
720
695
  })
721
696
 
722
- /** Asks until the answer is something, for a few seconds. */
723
- async function poll(ask, ms = 5000) {
724
- const deadline = Date.now() + ms
725
- for (;;) {
726
- const answer = await ask()
727
- if (answer || Date.now() > deadline) return answer
728
- await sleep(50)
729
- }
730
- }
731
-
732
697
  /*
733
698
  * The fastening window writes once (user 2026-10-17): whatever is tried in it is a preview, 「적용」 is one step of the
734
699
  * history with all of it, and 「취소」 or Esc leave the history as it was.
@@ -769,9 +769,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
769
769
  assert.equal(after.revision, start.revision + 1, 'one step')
770
770
  assert.deepEqual(await read(() => probe.fields('size.')), { 'size.x': 400, 'size.y': 400, 'size.z': 400 }, 'the size is the box as it stands')
771
771
  const notice = await read(() => (window.__told ?? []).filter(n => n.level === 'info').at(-1)?.message ?? null)
772
- /* The box had no space declared either: the same step declared it, and the one line says both (ruling 2026-10-16, Q1 A). */
773
- assert.ok(notice?.includes('figure.text.size-and-space-declared-to-follow'), `a line says so: ${notice}`)
774
- assert.equal(await read(() => !!document.querySelector('figure-editor').session.revisionNow().asset.occupancy), true, 'the space declared by the same step')
772
+ assert.ok(notice?.includes('figure.text.size-declared-to-follow'), `a line says so: ${notice}`)
775
773
  const grown = await read(() => {
776
774
  const editor = document.querySelector('figure-editor')
777
775
  editor.session.apply([{ kind: 'set-design', input: 'size.x', unit: 'mm', value: 800 }])
@@ -5,7 +5,7 @@
5
5
  * Only reading: every change the check makes goes through the mouse and the keyboard.
6
6
  */
7
7
  import './editing-page.js'
8
- import { addFigureBox, applyFigureAuthoring, createFigureAsset, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
8
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
9
9
 
10
10
  const PALETTE = {
11
11
  'palette.dark': '#2b2b2b',
@@ -42,7 +42,7 @@ Object.assign(window.figures, {
42
42
  a = addFigureBox(a, { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 500, z: 0 } })
43
43
  a = addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 600, y: 50, z: 0 } })
44
44
  for (const part of ['housing', 'stray']) a = applyFigureAuthoring(a, { kind: 'attach', part, to: { plane: 'mounting-plane' } })
45
- return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' }))
45
+ return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' }))
46
46
  },
47
47
  rounded: () => {
48
48
  let a = figure('Rounded')
@@ -288,25 +288,15 @@ describe('a part renamed in its name field: one step, still selected; a name ref
288
288
  })
289
289
  })
290
290
 
291
- describe('a part in the declared space removed: at once, the space fitted to the parts left, and said so (2026-09-27)', () => {
292
- test('one step, nothing asked, the space still declared, and one notice that it was fitted again', async () => {
291
+ describe('a part on the plane beside another removed: at once (2026-09-27)', () => {
292
+ test('one step, nothing asked', async () => {
293
293
  await mount('box', 'gate')
294
294
  await pick('stray')
295
295
  const before = await revision()
296
- const space = () => read(() => JSON.parse(document.querySelector('figure-editor').session.source()).occupancy ?? null)
297
- const was = await space()
298
- assert.ok(was, 'declared to begin with')
299
296
  await click(await read(() => parts.op('remove-part')), 'remove')
300
297
  assert.deepEqual(await ids(), ['housing'])
301
298
  assert.equal(await revision(), before + 1, 'one step')
302
299
  assert.equal(await read(() => editing.prompt()), null, 'nothing asked')
303
- const now = await space()
304
- assert.ok(now, 'the space is still declared')
305
- assert.notDeepEqual(now, was, 'fitted to the part left')
306
- const told = await read(() => editing.told())
307
- assert.deepEqual(told.map(t => t.level), ['info'], JSON.stringify(told))
308
- /* The page loads no translations: the words are the key. */
309
- assert.ok(told[0].message.includes('figure.text.removal-space-refitted'), told[0].message)
310
300
  })
311
301
  })
312
302
 
@@ -285,7 +285,7 @@ describe('the history panel on the page', () => {
285
285
  product.answer('figureVersions', () => [VERSION_1])
286
286
  product.answer('inspectFigure', () => ({
287
287
  blocked: true,
288
- findings: ['occupancy-undeclared', 'part-outside-occupancy', 'type-is-placeholder'].map(code => ({ code, message: code, why: null, how: null, at: null, blocking: true }))
288
+ findings: ['off-mounting-plane', 'parts-off-placement-face', 'type-is-placeholder'].map(code => ({ code, message: code, why: null, how: null, at: null, blocking: true }))
289
289
  }))
290
290
  }, VERSION_1)
291
291
  await open('h')
@@ -305,13 +305,12 @@ describe('the history panel on the page', () => {
305
305
  assert.deepEqual(link, { href: '/figure-package?refs=acme%2Fagv%401', download: true })
306
306
  })
307
307
 
308
- test('fitting to the parts is offered for the two findings it answers, and declares the room as one step', async () => {
309
- const offered = await until(p => { const found = product.deep(`${p} >>> li[finding]`); return found.length === 3 && found.filter(li => li.querySelector('button[fit-base]')).map(li => li.querySelector('[msg]').textContent).sort() }, 'the findings', PANEL)
310
- assert.deepEqual(offered, ['occupancy-undeclared', 'part-outside-occupancy'])
308
+ test('the findings stand in the history panel; an edit beside it is one step', async () => {
309
+ const shown = await until(p => { const found = product.deep(`${p} >>> li[finding]`); return found.length === 3 && found.map(li => li.querySelector('[msg]').textContent).sort() }, 'the findings', PANEL)
310
+ assert.deepEqual(shown, ['off-mounting-plane', 'parts-off-placement-face', 'type-is-placeholder'])
311
311
  const before = await read(() => product.view())
312
- await press(`${PANEL} >>> button[fit-base]`, 'fit')
312
+ await rename('H2')
313
313
  await until(r => product.view().revision === r + 1, 'one step', before.revision)
314
- await until(() => !!JSON.parse(product.view().source).occupancy, 'the room declared')
315
314
  await openHistory()
316
315
  })
317
316
 
@@ -557,7 +556,6 @@ describe('the page around the figure: the shell, the dock, the properties', () =
557
556
  })
558
557
  await until(r => product.view().revision === r + 1, 'one step', before.revision)
559
558
  assert.equal(JSON.parse((await read(() => product.view())).source).placement, 'ceiling')
560
- assert.equal(JSON.parse((await read(() => product.view())).source).occupancy, undefined, 'no room declared on the way')
561
559
  })
562
560
 
563
561
  test('unsaved: closing or reloading the tab is asked about', async () => {
@@ -806,8 +804,7 @@ describe('a mounting face named already', () => {
806
804
  const before = await read(() => product.view())
807
805
  await press('figure-properties >>> button[clear-mounting-face]', 'take the mounting face off')
808
806
  await until(r => product.view().revision === r + 1, 'one step', before.revision)
809
- assert.equal(JSON.parse((await read(() => product.view())).source).occupancy.contact, undefined, 'taken off')
810
- assert.ok(JSON.parse((await read(() => product.view())).source).occupancy.bounds, 'the room stays')
807
+ assert.equal(JSON.parse((await read(() => product.view())).source).mountingFace, undefined, 'taken off')
811
808
  await until(() => !product.deep('figure-modeller-page >>> figure-properties')[0].shadowRoot.querySelector('div[mounting-face]'), 'the line gone')
812
809
  })
813
810
 
@@ -20,7 +20,7 @@ import { store } from '@operato/shell'
20
20
  import { addressController } from '@operato/utils/address.js'
21
21
  import './popup-stand-in.js'
22
22
  import { html, render } from 'lit'
23
- import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, proposeFigureOccupancy, serializeFigureAsset } from '@hatiolab/figure-model'
23
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, serializeFigureAsset } from '@hatiolab/figure-model'
24
24
 
25
25
  const PALETTE = { 'palette.dark': '#2b2b2b', 'palette.neutral': '#9a9a9a', 'palette.primary': '#477b9e', 'palette.accent': '#f79c40', 'palette.light': '#dddddd' }
26
26
 
@@ -35,7 +35,6 @@ export function mountedSource(placement = 'floor') {
35
35
  let a = applyFigureAuthoring(createFigureAsset('Mounted'), { kind: 'set-placement', placement })
36
36
  a.palette = { ...a.palette, ...PALETTE }
37
37
  a = addFigureBox(a, { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } })
38
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
39
38
  return serializeFigureAsset(applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'box', face: 'bottom' }))
40
39
  }
41
40
 
@@ -16,7 +16,6 @@ import {
16
16
  compileFigureAsset,
17
17
  createFigureAsset,
18
18
  editFigurePart,
19
- proposeFigureOccupancy,
20
19
  robotArmFigure,
21
20
  serializeFigureAsset
22
21
  } from '@hatiolab/figure-model'
@@ -67,18 +66,15 @@ const FIGURES = {
67
66
  let a = addFigureBox(named('Stack'), { id: 'base', size: { x: 1000, y: 200, z: 600 }, position: { x: 0, y: 100, z: 0 } })
68
67
  a = addFigureBox(a, { id: 'top', size: { x: 200, y: 200, z: 200 }, position: { x: 0, y: 300, z: 0 } })
69
68
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'top', to: { part: 'base', face: 'top' } })
70
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a) })
71
69
  return applyFigureAuthoring(a, { kind: 'set-mounting-face', part: 'base', face: 'bottom' })
72
70
  },
73
- /* A conveyor that follows its width, a roller every 200 mm, and the space it takes declared: the one figure stretched. */
71
+ /* A conveyor that follows its width, a roller every 200 mm: the one figure stretched. */
74
72
  stretch: () => {
75
73
  let a = addFigureBox(named('Stretch'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
76
74
  a = addFigurePrimitive(a, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
77
75
  a = applyFigureAuthoring(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
78
76
  a = applyFigureAuthoring(a, { kind: 'follow-size', part: 'frame', axes: ['x'] })
79
- a = applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
80
- /* A space wider than the parts, as a spin swept or a padding gives: the box is the space, not the size (2026-10-16). */
81
- return applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a, { over: 'range' }), padding: { x: 100, y: 0, z: 0 } })
77
+ return applyFigureAuthoring(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
82
78
  },
83
79
  /* The robot arm with a clip that turns its base: controls and a clip in the preview. */
84
80
  'arm-clip': () => editFigurePart(robotArmFigure(), { kind: 'set-driver', state: 'j1-base', clip: 'cycle', duration: 4, keys: [{ seconds: 0, value: 0 }, { seconds: 4, value: 60 }] })
@@ -129,31 +129,10 @@ describe('the verdict tab over a figure, as the modeller page mounts it', () =>
129
129
  await until('the answer is shown', () => !!alone.verdict.shadowRoot.querySelector('p[clear]'))
130
130
  })
131
131
 
132
- test('no 「부품에 맞추기」 over a figure with no parts; over one with parts, it is offered for the same finding', async () => {
133
- const undeclared = await read(() => [alone.finding('occupancy-undeclared')])
134
- await read((text, findings) => alone.mount(text, { findings }), await read(() => figureText('empty')), undeclared)
135
- await until('the finding is shown', () => !!alone.verdict.shadowRoot.querySelector('li[finding]'))
136
- assert.equal(await read(() => alone.verdict.shadowRoot.querySelectorAll('button[fit]').length), 0, 'nothing to measure, nothing offered')
137
- await read((text, findings) => alone.mount(text, { findings }), await read(() => figureText('two')), undeclared)
138
- await until('the finding is shown', () => !!alone.verdict.shadowRoot.querySelector('li[finding]'))
139
- assert.equal(await read(() => alone.verdict.shadowRoot.querySelectorAll('li[finding] button[fit]').length), 1)
140
- })
141
-
142
- test('the verdict tab and the release history offer the fit for the same findings, from questions that ask the same fields', async () => {
143
- const findings = await read(() => ['part-outside-occupancy', 'occupancy-undeclared', 'mounting-face-floating'].map(code => alone.finding(code)))
132
+ test('the verdict tab and the release history show the same findings, from questions that ask the same fields', async () => {
133
+ const findings = await read(() => ['off-mounting-plane', 'type-is-placeholder', 'mounting-face-floating'].map(code => alone.finding(code)))
144
134
  await read((text, findings) => alone.mount(text, { figureId: 'fig-1', findings, history: true }), await read(() => figureText('two')), findings)
145
- const offered = (host, button) =>
146
- read(
147
- (host, button) => {
148
- const root = document.querySelector(host)?.shadowRoot
149
- return root ? [...root.querySelectorAll('li[finding]')].filter(li => li.querySelector(button)).map(li => li.querySelector('span[msg]').textContent.trim()).sort() : null
150
- },
151
- host,
152
- button
153
- )
154
135
  await until('both judged', () => ['figure-verdict', 'figure-history-panel'].every(host => document.querySelector(host)?.shadowRoot?.querySelectorAll('li[finding]').length === 3))
155
- assert.deepEqual(await offered('figure-verdict', 'button[fit]'), ['occupancy-undeclared', 'part-outside-occupancy'])
156
- assert.deepEqual(await offered('figure-history-panel', 'button[fit-base]'), await offered('figure-verdict', 'button[fit]'), 'the same two, in both places')
157
136
  const draft = await read(() => alone.fieldsAsked('inspectFigureDraft'))
158
137
  assert.ok(draft?.includes('why') && draft.includes('how'), `the draft's question asks why and how: ${draft}`)
159
138
  assert.equal(draft, await read(() => alone.fieldsAsked('inspectFigure')), 'the saved figure\'s question asks the same fields')
@@ -783,8 +762,8 @@ describe('the preview stands one figure the author stretches (first 15 minutes,
783
762
  slider.dispatchEvent(new Event('input'))
784
763
  slider.dispatchEvent(new Event('change'))
785
764
  })
786
- /* The box is the space the figure takes at 6 m: the frame and its 100 mm padding either side. */
787
- await until('the figure 6 m wide, in its space', () => Math.round(screenCheck.deep('figure-preview')[0].scene.root.components[0].model.width) === 6200)
765
+ /* The box is the figure's body at 6 m: the frame, as wide as asked (ADR-0145). */
766
+ await until('the figure 6 m wide, its body', () => Math.round(screenCheck.deep('figure-preview')[0].scene.root.components[0].model.width) === 6000)
788
767
  const said = await read(() => screenCheck.deep('figure-preview')[0].shadowRoot.querySelector('div[sizes] p[said]').textContent.trim())
789
768
  assert.ok(said.startsWith('6,000 × '), said)
790
769
  const box = await read(() => { const m = screenCheck.deep('figure-preview')[0].scene.root.components[0].model; return m.left + m.width / 2 })
@@ -988,9 +967,9 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
988
967
  await open('fig-a')
989
968
  })
990
969
 
991
- /* First 15 minutes (2026-10-16): release was refused for a volume never declared, said only at the foot of the verdict tab. */
992
- test('what blocks release is said beside 「발행」, with the way out at hand there', async () => {
993
- const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('occupancy-undeclared')] : [] })), blocked)
970
+ /* First 15 minutes (2026-10-16): what refused release was said only at the foot of the verdict tab. */
971
+ test('what blocks release is said beside 「발행」, the list under the bar', async () => {
972
+ const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('type-is-placeholder')] : [] })), blocked)
994
973
  const bar = () => read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('ox-canvas-bar > button[slot=commit]')].map(b => (b.hasAttribute('bar-overlay') ? 'overlay' : b.hasAttribute('release-blocked') ? 'blocked' : b.hasAttribute('publish') ? 'publish' : 'save')))
995
974
  await answer(true)
996
975
  await open('fig-block', 'two', 'probe-block')
@@ -998,12 +977,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
998
977
  assert.deepEqual(await bar(), ['blocked', 'publish', 'save'])
999
978
  await press('what blocks release', () => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
1000
979
  await until('the list under the bar', () => !!screenCheck.editor().shadowRoot.querySelector('div[release-blockers] li[finding]'))
1001
- assert.equal(await read(() => !!screenCheck.editor().session.revisionNow().asset.occupancy), false)
1002
980
  await answer(false)
1003
- await press('fit to the parts', () => screenCheck.editor().shadowRoot.querySelector('div[release-blockers] button[act]'))
1004
- await until('the space declared, in one step', () => !!screenCheck.editor().session.revisionNow().asset.occupancy)
1005
- await until('nothing blocks: the word is gone', () => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[release-blocked]'))
1006
- assert.deepEqual(await bar(), ['publish', 'save'])
1007
981
  await read(() => product.answer('inspectFigureDraft'))
1008
982
  await saved()
1009
983
  await open('fig-a')
@@ -1011,7 +985,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1011
985
 
1012
986
  /* First 15 minutes, again (2026-10-16 evening): a new figure, nothing in it yet, showed 「발행 막힘 1」 in red from the start. */
1013
987
  test('a figure with no parts has nothing red beside 「발행」; the verdict still holds what blocks it', async () => {
1014
- const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('occupancy-undeclared')] : [] })), blocked)
988
+ const answer = blocked => read(blocked => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked, findings: blocked ? [alone.finding('type-is-placeholder')] : [] })), blocked)
1015
989
  await answer(true)
1016
990
  try {
1017
991
  await open('fig-empty-bar', 'empty', 'probe-empty-bar')
@@ -1120,37 +1094,6 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1120
1094
  await until('saved', () => !screenCheck.editor().session.read().dirty)
1121
1095
  })
1122
1096
 
1123
- test('the verdict\'s 「부품에 맞추기」 declares the space from the parts, in one step', async () => {
1124
- await read(finding => product.answer('inspectFigureDraft', () => ({ __typename: 'FigureInspection', blocked: true, findings: [finding] })), await read(() => alone.finding('occupancy-undeclared')))
1125
- try {
1126
- await open('fig-b', 'two')
1127
- assert.equal(await read(() => JSON.parse(screenCheck.editor().session.source()).occupancy), undefined, 'no space declared yet')
1128
- await pressTab(TAB('verdict'))
1129
- await until('offered', () => !!screenCheck.deep('figure-verdict')[0]?.shadowRoot?.querySelector('button[fit]'))
1130
- const before = await revision()
1131
- await press('fit to the parts', () => screenCheck.deep('figure-verdict')[0].shadowRoot.querySelector('button[fit]'))
1132
- await until('one step', was => screenCheck.editor().session.read().revision === was + 1, before)
1133
- assert.ok(await read(() => !!JSON.parse(screenCheck.editor().session.source()).occupancy), 'the space is declared')
1134
- await saved()
1135
- } finally {
1136
- await read(() => product.answer('inspectFigureDraft'))
1137
- }
1138
- })
1139
-
1140
- test('fitting a figure with no parts says so in the app\'s words and leaves the editor standing', async () => {
1141
- await read(finding => product.answer('inspectFigure', () => ({ __typename: 'FigureInspection', blocked: true, findings: [finding] })), await read(() => alone.finding('occupancy-undeclared')))
1142
- try {
1143
- await open('fig-empty', 'empty')
1144
- await openOverlay('history')
1145
- await until('offered', () => !!screenCheck.deep('figure-history-panel')[0]?.shadowRoot?.querySelector('button[fit-base]'))
1146
- await press('fit to the parts', () => screenCheck.deep('figure-history-panel')[0].shadowRoot.querySelector('button[fit-base]'))
1147
- await until('said', () => screenCheck.deep('p[fit-error]')[0]?.textContent.trim() === 'figure.text.fit-needs-parts')
1148
- assert.equal(await read(() => !!screenCheck.editor()?.isConnected), true, 'the editor stands')
1149
- } finally {
1150
- await read(() => product.answer('inspectFigure'))
1151
- }
1152
- })
1153
-
1154
1097
  test('the view set in the bar\'s menu holds for the next figure opened: its board is stood up with it', async () => {
1155
1098
  await openOverlay('view')
1156
1099
  const colour = await element('the sky colour', () => screenCheck.setting('figure.label.sky-color').querySelector('input[type=color]'))
@@ -1,7 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { readFileSync } from 'node:fs'
4
- import { addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, planFigureResize, proposeFigureOccupancy } from '@hatiolab/figure-model'
4
+ import { addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, planFigureResize } from '@hatiolab/figure-model'
5
5
  import { followWordsOf, partSummaryOf } from '../client/modeller/links.js'
6
6
 
7
7
  /*
@@ -18,7 +18,7 @@ test('a selected part at the head: its kind, and what its size follows only when
18
18
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
19
19
  const sayKo = (key: string, params: Record<string, string> = {}) => Object.entries(params).reduce((t, [k, v]) => t.split(`{${k}}`).join(v), ko[key] ?? key)
20
20
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
21
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
21
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
22
22
  /* As usual -- a size of its own, loose: nothing but what it is. */
23
23
  assert.deepEqual(partSummaryOf(a, 'box', say), { kind: 'figure.label.primitive-cube', size: '' })
24
24
  const only = applyFigureProposal(a, planFigureResize(a, { part: 'box', axes: ['x'] }).steps)
@@ -47,7 +47,6 @@ test('the start card and the figure panel choose the placement as the figure\'s
47
47
  const { createFigureAsset, applyFigureAuthoring } = await import('@hatiolab/figure-model')
48
48
  const hung = applyFigureAuthoring(createFigureAsset('New'), { kind: 'set-placement', placement: 'ceiling' })
49
49
  assert.equal(hung.placement, 'ceiling')
50
- assert.equal(hung.occupancy, undefined)
51
50
  /* Chosen on the start card and in the figure panel, before any part: the browser check's (test/browser/screen.check.mjs). */
52
51
  })
53
52
 
@@ -88,11 +88,6 @@
88
88
  "unit": "ratio",
89
89
  "value": 0.3
90
90
  },
91
- {
92
- "id": "k.minusHalf",
93
- "unit": "ratio",
94
- "value": -0.5
95
- },
96
91
  {
97
92
  "id": "k.zero",
98
93
  "unit": "mm",
@@ -379,50 +374,6 @@
379
374
  "value": "badge.y.fromBottom.value"
380
375
  }
381
376
  },
382
- {
383
- "id": "occupancy.x.min",
384
- "op": "mul@1",
385
- "args": [
386
- "size.x",
387
- "k.minusHalf"
388
- ],
389
- "outputs": {
390
- "value": "occupancy.x.min.value"
391
- }
392
- },
393
- {
394
- "id": "occupancy.x.max",
395
- "op": "mul@1",
396
- "args": [
397
- "size.x",
398
- "k.half"
399
- ],
400
- "outputs": {
401
- "value": "occupancy.x.max.value"
402
- }
403
- },
404
- {
405
- "id": "occupancy.z.min",
406
- "op": "mul@1",
407
- "args": [
408
- "size.z",
409
- "k.minusHalf"
410
- ],
411
- "outputs": {
412
- "value": "occupancy.z.min.value"
413
- }
414
- },
415
- {
416
- "id": "occupancy.z.max",
417
- "op": "mul@1",
418
- "args": [
419
- "size.z",
420
- "k.half"
421
- ],
422
- "outputs": {
423
- "value": "occupancy.z.max.value"
424
- }
425
- },
426
377
  {
427
378
  "id": "hinge.origin.x",
428
379
  "op": "add@1",
@@ -1213,21 +1164,5 @@
1213
1164
  }
1214
1165
  }
1215
1166
  ],
1216
- "occupancy": {
1217
- "bounds": {
1218
- "x": {
1219
- "min": "occupancy.x.min.value",
1220
- "max": "occupancy.x.max.value"
1221
- },
1222
- "y": {
1223
- "min": "k.zero",
1224
- "max": "size.y"
1225
- },
1226
- "z": {
1227
- "min": "occupancy.z.min.value",
1228
- "max": "occupancy.z.max.value"
1229
- }
1230
- },
1231
- "placement": "floor"
1232
- }
1167
+ "placement": "floor"
1233
1168
  }
@@ -83,11 +83,6 @@
83
83
  "unit": "ratio",
84
84
  "value": 0.25
85
85
  },
86
- {
87
- "id": "k.minusHalf",
88
- "unit": "ratio",
89
- "value": -0.5
90
- },
91
86
  {
92
87
  "id": "k.zero",
93
88
  "unit": "mm",
@@ -293,50 +288,6 @@
293
288
  "value": "roller.y.fromBottom.value"
294
289
  }
295
290
  },
296
- {
297
- "id": "occupancy.x.min",
298
- "op": "mul@1",
299
- "args": [
300
- "size.x",
301
- "k.minusHalf"
302
- ],
303
- "outputs": {
304
- "value": "occupancy.x.min.value"
305
- }
306
- },
307
- {
308
- "id": "occupancy.x.max",
309
- "op": "mul@1",
310
- "args": [
311
- "size.x",
312
- "k.half"
313
- ],
314
- "outputs": {
315
- "value": "occupancy.x.max.value"
316
- }
317
- },
318
- {
319
- "id": "occupancy.z.min",
320
- "op": "mul@1",
321
- "args": [
322
- "size.z",
323
- "k.minusHalf"
324
- ],
325
- "outputs": {
326
- "value": "occupancy.z.min.value"
327
- }
328
- },
329
- {
330
- "id": "occupancy.z.max",
331
- "op": "mul@1",
332
- "args": [
333
- "size.z",
334
- "k.half"
335
- ],
336
- "outputs": {
337
- "value": "occupancy.z.max.value"
338
- }
339
- },
340
291
  {
341
292
  "id": "hinge.origin.x",
342
293
  "op": "add@1",
@@ -909,21 +860,5 @@
909
860
  }
910
861
  }
911
862
  ],
912
- "occupancy": {
913
- "bounds": {
914
- "x": {
915
- "min": "occupancy.x.min.value",
916
- "max": "occupancy.x.max.value"
917
- },
918
- "y": {
919
- "min": "k.zero",
920
- "max": "size.y"
921
- },
922
- "z": {
923
- "min": "occupancy.z.min.value",
924
- "max": "occupancy.z.max.value"
925
- }
926
- },
927
- "placement": "floor"
928
- }
863
+ "placement": "floor"
929
864
  }