@things-factory/figure-ui 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/client/modeller/editor-styles.ts +16 -0
  2. package/client/modeller/editor.ts +53 -137
  3. package/client/modeller/fasten-form.ts +50 -11
  4. package/client/modeller/figure-facts.ts +1 -1
  5. package/client/modeller/figure-fasten-dialog.ts +3 -2
  6. package/client/modeller/figure-history-panel.ts +1 -29
  7. package/client/modeller/figure-links.ts +2 -2
  8. package/client/modeller/figure-preview.ts +10 -12
  9. package/client/modeller/figure-verdict.ts +2 -17
  10. package/client/modeller/gizmo.ts +10 -0
  11. package/client/modeller/keys.ts +10 -1
  12. package/client/modeller/links.ts +32 -41
  13. package/client/modeller/maturity.ts +11 -27
  14. package/client/modeller/palette.ts +1 -1
  15. package/client/modeller/product-session.ts +16 -4
  16. package/client/modeller/refusal-teller.ts +3 -25
  17. package/client/modeller/refusals.ts +20 -33
  18. package/client/pages/figure-modeller-page.ts +7 -22
  19. package/dist-client/modeller/editor-styles.js +16 -0
  20. package/dist-client/modeller/editor-styles.js.map +1 -1
  21. package/dist-client/modeller/editor.d.ts +8 -11
  22. package/dist-client/modeller/editor.js +57 -138
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/fasten-form.d.ts +16 -7
  25. package/dist-client/modeller/fasten-form.js +45 -11
  26. package/dist-client/modeller/fasten-form.js.map +1 -1
  27. package/dist-client/modeller/figure-facts.js +1 -1
  28. package/dist-client/modeller/figure-facts.js.map +1 -1
  29. package/dist-client/modeller/figure-fasten-dialog.js +3 -3
  30. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  31. package/dist-client/modeller/figure-history-panel.d.ts +0 -16
  32. package/dist-client/modeller/figure-history-panel.js +1 -27
  33. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  34. package/dist-client/modeller/figure-links.js +2 -2
  35. package/dist-client/modeller/figure-links.js.map +1 -1
  36. package/dist-client/modeller/figure-preview.js +8 -11
  37. package/dist-client/modeller/figure-preview.js.map +1 -1
  38. package/dist-client/modeller/figure-verdict.d.ts +0 -5
  39. package/dist-client/modeller/figure-verdict.js +2 -21
  40. package/dist-client/modeller/figure-verdict.js.map +1 -1
  41. package/dist-client/modeller/gizmo.d.ts +6 -0
  42. package/dist-client/modeller/gizmo.js +11 -0
  43. package/dist-client/modeller/gizmo.js.map +1 -1
  44. package/dist-client/modeller/keys.d.ts +1 -1
  45. package/dist-client/modeller/keys.js +6 -0
  46. package/dist-client/modeller/keys.js.map +1 -1
  47. package/dist-client/modeller/links.d.ts +16 -22
  48. package/dist-client/modeller/links.js +26 -39
  49. package/dist-client/modeller/links.js.map +1 -1
  50. package/dist-client/modeller/maturity.d.ts +2 -4
  51. package/dist-client/modeller/maturity.js +12 -26
  52. package/dist-client/modeller/maturity.js.map +1 -1
  53. package/dist-client/modeller/palette.js +1 -1
  54. package/dist-client/modeller/palette.js.map +1 -1
  55. package/dist-client/modeller/product-session.d.ts +15 -13
  56. package/dist-client/modeller/product-session.js +15 -5
  57. package/dist-client/modeller/product-session.js.map +1 -1
  58. package/dist-client/modeller/refusal-teller.d.ts +1 -12
  59. package/dist-client/modeller/refusal-teller.js +3 -22
  60. package/dist-client/modeller/refusal-teller.js.map +1 -1
  61. package/dist-client/modeller/refusals.d.ts +7 -17
  62. package/dist-client/modeller/refusals.js +17 -34
  63. package/dist-client/modeller/refusals.js.map +1 -1
  64. package/dist-client/pages/figure-modeller-page.d.ts +1 -3
  65. package/dist-client/pages/figure-modeller-page.js +7 -23
  66. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  67. package/dist-client/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +7 -7
  69. package/test/browser/acceptance-page.js +4 -4
  70. package/test/browser/detail-page.js +1 -3
  71. package/test/browser/detail-tabs-page.js +1 -2
  72. package/test/browser/links.check.mjs +32 -50
  73. package/test/browser/modeller.check.mjs +46 -19
  74. package/test/browser/parts-page.js +2 -2
  75. package/test/browser/parts.check.mjs +51 -12
  76. package/test/browser/product-page.check.mjs +6 -9
  77. package/test/browser/product-page.js +1 -2
  78. package/test/browser/screen-page.js +2 -6
  79. package/test/browser/screen.check.mjs +8 -65
  80. package/test/browser/section.check.mjs +27 -0
  81. package/test/fasten-form.test.ts +42 -2
  82. package/test/first-minute.test.ts +2 -3
  83. package/test/fixtures/joint-column.json +1 -66
  84. package/test/fixtures/joint-roller.json +1 -66
  85. package/test/fixtures/robot-arm-6dof.json +2 -67
  86. package/test/gizmo.test.ts +12 -1
  87. package/test/keys.test.ts +11 -0
  88. package/test/links.test.ts +39 -34
  89. package/test/parts-wiring.test.ts +1 -1
  90. package/test/properties.test.ts +9 -12
  91. package/test/refusals.test.ts +20 -42
  92. package/test/resize-with-placement.test.ts +13 -15
  93. package/test/revision.test.ts +1 -2
  94. package/test/verdict.test.ts +1 -15
  95. package/translations/en.json +8 -32
  96. package/translations/es.json +8 -32
  97. package/translations/ja.json +8 -32
  98. package/translations/ko.json +8 -32
  99. package/translations/ms.json +8 -32
  100. package/translations/zh.json +8 -32
  101. package/client/modeller/occupancy.ts +0 -51
  102. package/dist-client/modeller/occupancy.d.ts +0 -31
  103. package/dist-client/modeller/occupancy.js +0 -36
  104. package/dist-client/modeller/occupancy.js.map +0 -1
  105. package/test/release-panel.test.ts +0 -53
@@ -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]'))
@@ -330,6 +330,33 @@ describe('the section editor on a box whose sizes follow the figure', () => {
330
330
  })
331
331
  })
332
332
 
333
+ /*
334
+ * The user's case (2026-10-21, 「수정본 검증 - 기본 버튼 상자」): 「속 공간 만들기」 ticked on a box whose sizes follow the
335
+ * figure's was refused, for a 1 mm the figure had never been asked to go to. Now it is one step and nothing is refused:
336
+ * how small the figure can then be is computed from the walls (ADR-0087 decision 6).
337
+ */
338
+ describe('the inside made on a box whose sizes follow the figure', () => {
339
+ before(async () => {
340
+ await mount('follows')
341
+ await pick('box')
342
+ })
343
+
344
+ test('ticked, it is one step, drawn, and not refused', async () => {
345
+ const start = await state()
346
+ /* Clicked where it is drawn, as a person does. */
347
+ const at = await (await element('label[toggle] input', { within: 'figure-section-panel' })).evaluate(e => {
348
+ e.scrollIntoView({ block: 'center' })
349
+ const r = e.getBoundingClientRect()
350
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
351
+ })
352
+ await page().mouse.click(at.x, at.y)
353
+ await until(r => probe.state().revision === r + 1, 'one step', start.revision)
354
+ assert.equal((await state()).error, '', 'not refused')
355
+ await until(() => probe.sectionPanel()?.cavityDrawing, 'the inside drawn')
356
+ assert.ok((await held('box')).cavity, 'the box has its inside')
357
+ })
358
+ })
359
+
333
360
  describe('a drawn section\'s end drawn to one point', () => {
334
361
  test('turned on, the length is called the height', async () => {
335
362
  await mount('wedge')
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict'
9
9
  import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
10
10
  import type { FigureAsset } from '@hatiolab/figure-model'
11
11
 
12
- import { aimFasten, aimFastenAt, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten } from '../client/modeller/fasten-form.ts'
12
+ import { aimFasten, aimFastenAt, fastenChanged, fastenDetailWordsOf, fastenFormOf, fastenPlanOf, fastenSentenceOf, fastenTargetsOf, freshFasten, keptGapOf } from '../client/modeller/fasten-form.ts'
13
13
  import { revisionOf } from './revision-of.ts'
14
14
 
15
15
  const box = (asset: FigureAsset, id: string, [x, y, z]: number[], at = [0, 0, 0]) => addFigureBox(asset, { id, size: { x, y, z }, position: { x: at[0]!, y: at[1]!, z: at[2]! } })
@@ -94,6 +94,46 @@ test('the drawing\'s changes: another face aims again, the way, the gap, a place
94
94
  assert.equal(fastenPlanOf(r, 'roller', f).targetFace, 'front')
95
95
  })
96
96
 
97
+ /*
98
+ * 「현재 상태로 붙이기」(ADR-0140 stage 4, user 2026-10-20): aimed at a face, the gap starts at the distance the part
99
+ * stands from it, so fastening does not move it. Changing how it meets the face works it out again; a number typed is
100
+ * the author's and stays; a press on the canvas puts the part there, with no gap.
101
+ */
102
+ const centreOf = (asset: FigureAsset, id: string) => (compileFigureAsset(asset).evaluate().geometry as Array<{ id: string; pose?: { t: number[] } }>).find(g => g.id === id)!.pose!.t
103
+ /* A plate on the floor, its top at 20 mm, and a lid 40 mm above it. */
104
+ function floating() {
105
+ let a = fasten(box(createFigureAsset('Lift'), 'plate', [600, 20, 600], [0, 10, 0]), 'plate', 'plane')
106
+ return revisionOf(box(a, 'lid', [100, 20, 100], [0, 70, 0]))
107
+ }
108
+
109
+ test('aimed at a face, the gap is where the part stands, and fastening it does not move it', () => {
110
+ const r = floating()
111
+ const f = keptGapOf(r, 'lid', aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'plate' }))
112
+ assert.deepEqual([f.gap, f.gapFrom], [40, 'kept'])
113
+ const applied = applyFigureAuthoring(r.asset, fastenPlanOf(r, 'lid', f).action!)
114
+ assert.deepEqual(centreOf(applied, 'lid'), centreOf(r.asset, 'lid'), 'the lid where it was')
115
+ const onPlane = keptGapOf(r, 'lid', aimFasten(r, 'lid', f, { plane: true }))
116
+ assert.equal(onPlane.gap, 60, 'aimed at the floor instead: 60 mm above it')
117
+ })
118
+
119
+ test('another way of meeting works the gap out again; a number typed stays; a press on the canvas puts it there', () => {
120
+ const r = floating()
121
+ const aimed = keptGapOf(r, 'lid', aimFasten(r, 'lid', fastenFormOf(r, 'lid'), { part: 'plate' }))
122
+ const flush = fastenChanged(r, 'lid', aimed, { mode: 'flush' })
123
+ const applied = applyFigureAuthoring(r.asset, fastenPlanOf(r, 'lid', flush).action!)
124
+ assert.deepEqual(centreOf(applied, 'lid'), centreOf(r.asset, 'lid'), 'flush, and still where it was')
125
+ assert.deepEqual(fastenDetailWordsOf(aimed, say), [], 'the gap it stands at is the default: nothing said on the fold')
126
+ const typed = fastenChanged(r, 'lid', aimed, { gap: 5 })
127
+ assert.equal(typed.gapFrom, 'typed')
128
+ assert.deepEqual(fastenDetailWordsOf(typed, say), ['figure.text.fasten-summary-gap(mm=5)'])
129
+ assert.equal(fastenChanged(r, 'lid', typed, { mode: 'flush' }).gap, 5, 'the author\'s number is left')
130
+ assert.equal(keptGapOf(r, 'lid', aimFasten(r, 'lid', typed, { plane: true })).gap, 5)
131
+ const plate = (r.geometry as Array<{ id: string }>).find(g => g.id === 'plate')!.id
132
+ const pressed = aimFastenAt(r, 'lid', aimed, { id: plate, point: [0, 20, 0], normal: [0, 1, 0] }).form!
133
+ assert.deepEqual([pressed.gap, pressed.gapFrom], [0, 'pressed'])
134
+ assert.equal(fastenChanged(r, 'lid', pressed, { mode: 'flush' }).gap, 0, 'pressed: left at 0')
135
+ })
136
+
97
137
  test('the target list: every other part, the one hanging from this part listed as a loop', () => {
98
138
  const r = conveyor()
99
139
  assert.deepEqual(fastenTargetsOf(r, 'frame'), [
@@ -107,7 +147,7 @@ test('the target list: every other part, the one hanging from this part listed a
107
147
  async function curved() {
108
148
  const { addFigurePrimitive } = await import('@hatiolab/figure-model')
109
149
  let a = addFigurePrimitive(createFigureAsset('Curved'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 1600 }, position: { x: 0, y: 0, z: 0 } } as never)
110
- a = addFigurePrimitive(a, { id: 'ball', kind: 'sphere', dimensions: { radiusX: 100, radiusY: 100, radiusZ: 100 }, position: { x: 0, y: 0, z: 3000 } } as never)
150
+ a = addFigurePrimitive(a, { id: 'ball', kind: 'sphere', dimensions: { radius: 100, radiusY: 100 }, position: { x: 0, y: 0, z: 3000 } } as never)
111
151
  a = box(a, 'tag', [40, 40, 40], [600, 600, 600])
112
152
  return box(a, 'cap', [40, 40, 40], [0, 600, 3000])
113
153
  }
@@ -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
  }
@@ -148,11 +148,6 @@
148
148
  "unit": "ratio",
149
149
  "value": 0.058333
150
150
  },
151
- {
152
- "id": "k.minusHalf",
153
- "unit": "ratio",
154
- "value": -0.5
155
- },
156
151
  {
157
152
  "id": "k.zero",
158
153
  "unit": "mm",
@@ -777,50 +772,6 @@
777
772
  "value": "finger-right.y.fromBottom.value"
778
773
  }
779
774
  },
780
- {
781
- "id": "occupancy.x.min",
782
- "op": "mul@1",
783
- "args": [
784
- "size.x",
785
- "k.minusHalf"
786
- ],
787
- "outputs": {
788
- "value": "occupancy.x.min.value"
789
- }
790
- },
791
- {
792
- "id": "occupancy.x.max",
793
- "op": "mul@1",
794
- "args": [
795
- "size.x",
796
- "k.half"
797
- ],
798
- "outputs": {
799
- "value": "occupancy.x.max.value"
800
- }
801
- },
802
- {
803
- "id": "occupancy.z.min",
804
- "op": "mul@1",
805
- "args": [
806
- "size.z",
807
- "k.minusHalf"
808
- ],
809
- "outputs": {
810
- "value": "occupancy.z.min.value"
811
- }
812
- },
813
- {
814
- "id": "occupancy.z.max",
815
- "op": "mul@1",
816
- "args": [
817
- "size.z",
818
- "k.half"
819
- ],
820
- "outputs": {
821
- "value": "occupancy.z.max.value"
822
- }
823
- },
824
775
  {
825
776
  "id": "gripper.finger-left.translation.u",
826
777
  "op": "add@1",
@@ -2901,23 +2852,6 @@
2901
2852
  }
2902
2853
  }
2903
2854
  ],
2904
- "occupancy": {
2905
- "bounds": {
2906
- "x": {
2907
- "min": "occupancy.x.min.value",
2908
- "max": "occupancy.x.max.value"
2909
- },
2910
- "y": {
2911
- "min": "k.zero",
2912
- "max": "size.y"
2913
- },
2914
- "z": {
2915
- "min": "occupancy.z.min.value",
2916
- "max": "occupancy.z.max.value"
2917
- }
2918
- },
2919
- "placement": "floor"
2920
- },
2921
2855
  "detailLevel": "L",
2922
2856
  "stateInputs": {
2923
2857
  "base-pan": {
@@ -2948,5 +2882,6 @@
2948
2882
  "label": "Gripper stroke",
2949
2883
  "sweep": 1
2950
2884
  }
2951
- }
2885
+ },
2886
+ "placement": "floor"
2952
2887
  }
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
11
11
 
12
12
  import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset, createFigureEditorWorkspace } from '@hatiolab/figure-model'
13
13
 
14
- import { gizmoChanges, gizmoReachOf } from '../client/modeller/gizmo.ts'
14
+ import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf } from '../client/modeller/gizmo.ts'
15
15
  import { moving } from './joints.ts'
16
16
 
17
17
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -159,3 +159,14 @@ test('도구 막대의 옮기기 · 돌리기 · 크기 — 키와 같은 길,
159
159
  const ko = JSON.parse(read('../translations/ko.json'))
160
160
  assert.deepEqual(['translate', 'rotate', 'scale'].map(m => ko['figure.button.gizmo-' + m]), ['옮기기', '돌리기', '크기 바꾸기'])
161
161
  })
162
+
163
+ /* ADR-0140 4.1: the size handles that change one measure are said while the size tool is on. */
164
+ test('같은 치수 — 원기둥의 X · Z 핸들은 한 지름, 구는 셋이 하나, 상자는 따로', () => {
165
+ const at = { x: 0, y: 0, z: 0 }
166
+ const drum = addFigurePrimitive(createFigureAsset('Drum'), { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 200 }, position: at })
167
+ const ball = addFigurePrimitive(createFigureAsset('Ball'), { id: 'ball', kind: 'sphere', dimensions: { radius: 50, radiusY: 50 }, position: at })
168
+ const crate = addFigureBox(createFigureAsset('Crate'), { id: 'crate', size: { x: 100, y: 100, z: 100 }, position: at })
169
+ assert.deepEqual(sameMeasureAxesOf(gizmoReachOf(drum, 'drum')), ['x', 'z'])
170
+ assert.deepEqual(sameMeasureAxesOf(gizmoReachOf(ball, 'ball')), ['x', 'y', 'z'])
171
+ assert.equal(sameMeasureAxesOf(gizmoReachOf(crate, 'crate')), undefined)
172
+ })
package/test/keys.test.ts CHANGED
@@ -41,3 +41,14 @@ test('캔버스에서 누른 W · E · R 은 씬이 이미 받았다 — 또 넘
41
41
  for (const key of ['w', 'e', 'r']) assert.equal(keyActionOf({ key, inScene: true }, 'arm'), null, key)
42
42
  assert.equal(keyActionOf({ key: 'Delete', inScene: true }, 'arm'), 'remove', '다른 키는 그대로')
43
43
  })
44
+
45
+ /* Ctrl+C · Ctrl+V are the modeller's: the scene's copied the canvas object with the part's name (user 2026-10-21). */
46
+ test('복사 · 붙여넣기는 모델러의 것 — 고른 부품의 Ctrl+C 는 복사, Ctrl+V 는 붙여넣기, 씬의 잘라내기 · 여러 개 붙여넣기는 막는다', () => {
47
+ assert.equal(keyActionOf({ key: 'c', ctrlKey: true }, 'arm'), 'copy')
48
+ assert.equal(keyActionOf({ key: 'C', metaKey: true }, 'arm'), 'copy')
49
+ assert.equal(keyActionOf({ key: 'c', ctrlKey: true }, ''), 'hold', '고른 것이 없어도 씬에 넘기지 않는다')
50
+ assert.equal(keyActionOf({ key: 'v', ctrlKey: true }, ''), 'paste', '고른 것이 없어도 붙여넣기')
51
+ assert.equal(keyActionOf({ key: 'V', ctrlKey: true, shiftKey: true }, 'arm'), 'hold')
52
+ assert.equal(keyActionOf({ key: 'x', ctrlKey: true }, 'arm'), 'hold')
53
+ assert.equal(keyActionOf({ key: 'v', ctrlKey: true, target: { tagName: 'INPUT' } }, ''), null, '칸 안의 붙여넣기는 칸의 것')
54
+ })