@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
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict'
10
10
  import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
11
11
  import type { FigureAsset } from '@hatiolab/figure-model'
12
12
 
13
- import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/links.ts'
13
+ import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/links.ts'
14
14
  import { revisionOf } from './revision-of.ts'
15
15
 
16
16
  const pair = (): FigureAsset => {
@@ -49,6 +49,10 @@ test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', ()
49
49
  assert.equal(canFasten(withDrum, 'drum'), true, '원기둥도 면으로 붙인다(figure-model 0.1.47)')
50
50
  assert.equal(canFollow(withDrum, 'drum'), true, '원기둥은 반지름(X)과 길이(Y)를 따를 수 있다')
51
51
  assert.deepEqual(followableAxes(withDrum, 'drum'), ['x', 'y'], '원기둥에는 제 Z 가 없다')
52
+ /* 구도 원기둥처럼 가로(X, Z 가 함께)와 높이(Y)를 따르고, 붙일 수 있다(사용자 2026-10-21). */
53
+ const withBall = addFigurePrimitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radius: 50, radiusY: 50 }, position: { x: 0, y: 50, z: 0 } })
54
+ assert.deepEqual(followableAxes(withBall, 'ball'), ['x', 'y'], '구의 가로 반지름은 X 와 Z 가 한 값')
55
+ assert.equal(canFasten(withBall, 'ball'), true)
52
56
  })
53
57
 
54
58
  test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는다', () => {
@@ -206,38 +210,39 @@ test('made to follow in proportion, a measure is the share of the figure size it
206
210
  assert.deepEqual(followCommandOf(revisionOf(linked), 'flange', 'y', 'part:base:x', followOf(linked, 'flange', 'y')), { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'part', part: 'base', axis: 'x' }, replace: true })
207
211
  })
208
212
 
209
- test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로 바꾸면 다시 재어 넓힌다', () => {
210
- assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
211
- let asset = sizedPair()
213
+ /* ADR-0087 결정 6(사용자 2026-10-21): 작성자가 좁힌 끝만 저장되고, 나머지는 부품에서 figure-model 이 계산한다. */
214
+ const openSides = () => {
215
+ const asset = sizedPair()
212
216
  for (const axis of ['x', 'z'] as const) {
213
- asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
217
+ asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', role: 'design' } as never)
214
218
  asset.designInputs[`size.${axis}`] = 200
215
219
  }
220
+ return asset
221
+ }
222
+
223
+ test('도면에서의 크기 — 좁히지 않은 끝은 부품에서 계산하고, 좁힌 끝은 작성자의 값이다', () => {
224
+ assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
225
+ const asset = openSides()
216
226
  const y = asset.document.model.inputs.find(i => i.id === 'size.y')!
217
227
  y.min = 90
218
228
  y.max = 1000
219
229
  const range = instanceRangeOf(asset)!
220
- assert.deepEqual(range.declared.y, { min: 90, max: 1000 })
221
- assert.deepEqual(range.measured.y, { min: 1, max: 1000 }, '기본 범위: 1 mm 에서 열 배')
222
- assert.equal(rangeDiffers(range.declared, range.measured), true)
223
- asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
224
- assert.equal(rangeDiffers(instanceRangeOf(asset)!.declared, instanceRangeOf(asset)!.measured), false, '다시 재면 같다')
230
+ assert.deepEqual(range.y, { min: 90, max: 1000, narrowed: { min: true, max: true } })
231
+ assert.deepEqual(range.x, { min: 0, max: Infinity, narrowed: { min: false, max: false } }, '막는 부품이 없으면 끝이 없다')
225
232
  })
226
233
 
227
- test('놓일 수 있는 크기 — 저작자가 끝을 적어 좁힌다(갠트리, 09-25)', () => {
228
- let asset = sizedPair()
229
- for (const axis of ['x', 'z'] as const) {
230
- asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
231
- asset.designInputs[`size.${axis}`] = 200
232
- }
233
- const declared = instanceRangeOf(asset)!.declared
234
- assert.equal(rangeWith(declared, 'x', 'min', Number.NaN), undefined, '숫자가 아니면 명령이 없다')
235
- const next = rangeWith(declared, 'x', 'min', 150)!
236
- assert.deepEqual(next.x, { min: 150, max: 20000 })
237
- assert.deepEqual(declared.x, { min: 1, max: 20000 }, '선언된 범위는 그대로')
238
- asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: next, replace: true })
239
- assert.deepEqual(instanceRangeOf(asset)!.declared.x, { min: 150, max: 20000 })
240
- assert.throws(() => applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: rangeWith(next, 'x', 'min', 300)!, replace: true }), (e: { code?: string }) => e.code === 'INSTANCE_SIZE_RANGE', '저작한 크기(200)를 벗어나면 거절')
234
+ test('도면에서의 크기 — 숫자를 치면 그 끝을 좁히고, 칸을 비우면 다시 연다(갠트리, 09-25)', () => {
235
+ let asset = openSides()
236
+ const range = instanceRangeOf(asset)!
237
+ assert.deepEqual(rangeWith(range, 'x', 'min', 150), { x: { min: 150 }, y: { min: 1, max: 20000 } }, '다른 축의 좁힌 끝은 그대로')
238
+ assert.deepEqual(rangeWith(range, 'y', 'min', Number.NaN), { y: { max: 20000 } }, '비운 칸은 열린다')
239
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: rangeWith(range, 'x', 'min', 150), replace: true })
240
+ assert.deepEqual(instanceRangeOf(asset)!.x, { min: 150, max: Infinity, narrowed: { min: true, max: false } })
241
+ assert.throws(
242
+ () => applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: rangeWith(instanceRangeOf(asset)!, 'x', 'min', 300), replace: true }),
243
+ (e: { code?: string }) => e.code === 'INSTANCE_SIZE_RANGE',
244
+ '저작한 크기(200)를 벗어나면 거절'
245
+ )
241
246
  })
242
247
 
243
248
  test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
@@ -376,7 +381,7 @@ test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구
376
381
  dimensions: { width: 1100, height: 2200, depth: 150, jamb: 100, head: 100 },
377
382
  position: { x: 0, y: 0, z: 0 }
378
383
  })
379
- asset = addFigurePrimitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radiusX: 50, radiusY: 50, radiusZ: 50 }, position: { x: 0, y: 0, z: 0 } })
384
+ asset = addFigurePrimitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radius: 50, radiusY: 50 }, position: { x: 0, y: 0, z: 0 } })
380
385
  asset = addFigurePrimitive(asset, { id: 'cone', kind: 'frustum', dimensions: { radiusTop: 20, radiusBottom: 60, height: 100 }, position: { x: 0, y: 0, z: 0 } })
381
386
  asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
382
387
  asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 400 }, position: { x: 0, y: 0, z: 0 } })
@@ -427,18 +432,18 @@ test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자
427
432
  })
428
433
 
429
434
  test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
430
- const { instanceRangeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
435
+ const { instanceSizeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
431
436
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
432
437
  let asset = createFigureAsset('Conveyor')
433
- assert.equal(instanceRangeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
438
+ assert.equal(instanceSizeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
434
439
  asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
435
440
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
436
441
  assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
437
- const toDeclare = instanceRangeToDeclare(asset)!
442
+ const toDeclare = instanceSizeToDeclare(asset)!
438
443
  assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
439
- asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })
440
- assert.ok(instanceRangeOf(asset), '선언 뒤에는 놓일 수 있는 크기가 보인다')
441
- assert.equal(instanceRangeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
444
+ asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', at: toDeclare.at })
445
+ assert.ok(instanceRangeOf(asset), '선언 뒤에는 도면에서의 크기가 보인다')
446
+ assert.equal(instanceSizeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
442
447
  assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
443
448
  asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
444
449
  const at = (size: number) => (compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as Record<string, number>)['size.x']
@@ -505,7 +510,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
505
510
  })
506
511
 
507
512
  test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
508
- const { instanceRangeToDeclare } = await import('../client/modeller/links.ts')
513
+ const { instanceSizeToDeclare } = await import('../client/modeller/links.ts')
509
514
  let asset = createFigureAsset('Door')
510
515
  asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
511
516
  asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
@@ -513,7 +518,7 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
513
518
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
514
519
  const j = asset.joints!.find(o => o.id === 'leaf')!
515
520
  asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
516
- assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
521
+ assert.deepEqual(instanceSizeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
517
522
  })
518
523
 
519
524
  test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
@@ -563,7 +568,7 @@ test('벽 배치 — 장착 평면은 Z = 0, 벽에서 나와 서고 가로질
563
568
  const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/links.ts')
564
569
  let a = addFigureBox(createFigureAsset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
565
570
  assert.deepEqual(mountingPlaneOf(a), { normal: 'y', facings: ['meet', 'flush'] }, '배치 선언 전에는 바닥 — 올려 서거나 아래에 매달림')
566
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
571
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'wall' as never })
567
572
  assert.deepEqual(mountingPlaneOf(a), { normal: 'z', facings: ['meet'] })
568
573
  assert.deepEqual(alignAxesOf(a, 'plane').map(x => x.axis), ['x', 'y'])
569
574
  })
@@ -21,7 +21,7 @@ test('지울 수 없는 부품은 사람의 말로 거절하고, 지우며 풀
21
21
  /* The reason is told apart in refusals.test.ts; that the editor tells it, and what a removal let go of, is parts.check.mjs's. */
22
22
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
23
23
  const table = JSON.parse(read(`../translations/${lang}.json`))
24
- for (const key of ['part-is-referenced', 'part-in-declared-space', 'part-row-along', 'part-row-carries'])
24
+ for (const key of ['part-is-referenced', 'part-row-along', 'part-row-carries'])
25
25
  assert.ok(table[`figure.text.${key}`]?.includes('{part}'), `${lang} ${key}`)
26
26
  /* What a removal let go of, in every language, with the names it carries. */
27
27
  for (const [key, word] of [['removal-released', '{parts}'], ['removal-released-motion', '{control}'], ['removal-own-control', '{max}'], ['removal-fixed', '{parts}'], ['removal-bindings', '{places}'], ['edit-refused', '{code}'], ['edit-refused-node-limit', '{max}']])
@@ -9,10 +9,9 @@ import assert from 'node:assert/strict'
9
9
  import { readFileSync } from 'node:fs'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
12
- import { addFigureBox, applyFigureAuthoring, createFigureAsset, inspectFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
12
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
13
13
 
14
14
  import { tagsOf, figureFigureFacts } from '../client/modeller/figure-facts.ts'
15
- import { occupancyFromParts } from '../client/modeller/occupancy.ts'
16
15
  import { revisionOf } from './revision-of.ts'
17
16
  import { wordsOf } from './words.ts'
18
17
 
@@ -32,18 +31,16 @@ test('탭이 읽는 사실 — 이름, 놓이는 곳, 이름 붙은 장착면뿐
32
31
  assert.equal(read.mounting, undefined)
33
32
  })
34
33
 
35
- test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
34
+ test('배치 기준을 고르면 탭이 그 기준을 다시 읽는다', () => {
36
35
  const source = serializeFigureAsset(probe())
37
- const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(parseFigureAsset(source), 'ceiling'))
36
+ const placed = applyFigureAuthoring(parseFigureAsset(source), { kind: 'set-placement', placement: 'ceiling' })
38
37
 
39
- assert.equal(facts(declared).placement, 'ceiling')
40
- assert.ok(!inspectFigureAsset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
38
+ assert.equal(facts(placed).placement, 'ceiling')
41
39
  })
42
40
 
43
41
  test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
44
42
  const source = serializeFigureAsset(probe())
45
- const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(parseFigureAsset(source)))
46
- const mounted = applyFigureAuthoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
43
+ const mounted = applyFigureAuthoring(parseFigureAsset(source), { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
47
44
 
48
45
  assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
49
46
  assert.equal(facts(applyFigureAuthoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
@@ -76,12 +73,12 @@ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
76
73
  })
77
74
 
78
75
  /*
79
- * The walk without the standing group (user 2026-10-18): nothing on the tab declares the room or names a face. The
80
- * room is declared where release asks for it; whether the figure meets its plane the release check says from the parts.
76
+ * The walk without the standing group (user 2026-10-18): nothing on the tab names a face. Whether the figure meets its
77
+ * plane the release check says from the parts.
81
78
  */
82
- test('속성 탭에는 공간을 정하거나 장착면을 고르는 명령이 없다 — 이름 붙은 장착면을 떼는 것만 있다', () => {
79
+ test('속성 탭에는 장착면을 고르는 명령이 없다 — 이름 붙은 장착면을 떼는 것만 있다', () => {
83
80
  const tab = read('../client/modeller/figure-properties.ts')
84
81
  const kinds = [...tab.matchAll(/kind: '([a-z-]+)'/g)].map(m => m[1]).sort()
85
82
  assert.deepEqual(kinds, ['clear-mounting-face', 'rename', 'set-placement'])
86
- assert.doesNotMatch(tab, /occupancyFromParts|declare-occupancy|set-mounting-face/)
83
+ assert.doesNotMatch(tab, /set-mounting-face/)
87
84
  })
@@ -15,9 +15,7 @@ import type { FigureAsset } from '@hatiolab/figure-model'
15
15
  import {
16
16
  clipLossOf,
17
17
  editRefusalOf,
18
- inDeclaredRoom,
19
18
  inputRangeRefusalOf,
20
- narrowingRetryOf,
21
19
  partNameRefusalOf,
22
20
  partRefusalOf,
23
21
  placedRefusalOf,
@@ -26,6 +24,7 @@ import {
26
24
  refusalRecordOf,
27
25
  removalNoticeOf,
28
26
  repeatPitchRefusalOf,
27
+ sizeRangeFitsOf,
29
28
  sizeRangeRefusalOf
30
29
  } from '../client/modeller/refusals.ts'
31
30
  import { wordsOf } from './words.ts'
@@ -35,6 +34,7 @@ const say = (key: string, params: Record<string, string>) => {
35
34
  assert.ok(key in ko, `no words for ${key}`)
36
35
  return ko[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
37
36
  }
37
+
38
38
  /** The key and its values, to see which words were chosen. */
39
39
  const keyed = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
40
40
 
@@ -66,35 +66,30 @@ function conveyor(): FigureAsset {
66
66
  return act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
67
67
  }
68
68
 
69
- test('크기 범위의 거절 다섯 — 축 · 숫자 · 부품을 커널이 넘긴 값으로 말한다', () => {
70
- const small = sizeRangeRefusalOf(refusalOf(() => act(panel(), followsWidth(-50))), say)!
71
- assert.ok(small.includes('X 축을') && small.includes('50 mm') && small.includes('panel 부품') && small.includes('다시 재기'), small)
72
- const large = sizeRangeRefusalOf(refusalOf(() => act(panel(), followsWidth(1000, { times: -1 }))), say)!
73
- assert.ok(large.includes('X 축을') && large.includes('1,000 mm') && large.includes('그릴 수 없는데'), large)
69
+ /*
70
+ * 크기 범위로 남은 거절은 둘 — 작성자가 친 끝이 지금 크기를 빼거나 0 에서 시작할 때(ADR-0087 결정 6). 부품을 바꾸는 명령은
71
+ * 범위 때문에 거절되지 않고, 작성자가 좁힌 끝이 부품이 허락하는 데까지 옮겨진 것을 한 줄로 말한다(사용자 2026-10-21).
72
+ */
73
+ test('크기 범위의 거절 둘 — 축 이름 · 숫자를 커널이 넘긴 값으로 말한다', () => {
74
74
  const outside = sizeRangeRefusalOf(refusalOf(() => act(box(), declaring({ min: 500, max: 4000 }))), say)!
75
- assert.ok(outside.includes('X') && outside.includes('400 mm') && outside.includes('500') && outside.includes('4,000'), outside)
75
+ assert.equal(outside, '도면에서의 너비 (X) 범위 500 – 4,000 mm 에 지금 크기 400 mm 가 들어가지 않습니다.')
76
+ const open = sizeRangeRefusalOf(refusalOf(() => act(box(), { kind: 'declare-instance-size', range: { x: { min: 500 } } })), say)!
77
+ assert.equal(open, '도면에서의 너비 (X) 범위 500 – ∞ mm 에 지금 크기 400 mm 가 들어가지 않습니다.', '한쪽 끝만 친 범위')
76
78
  const zero = sizeRangeRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say)!
77
- assert.ok(zero.includes('X') && zero.includes('0 보다'), zero)
78
- const gone = sizeRangeRefusalOf(refusalOf(() => act(panel(), followsWidth(-399.9999995, { narrowRange: true }))), say)!
79
- assert.ok(gone.includes('X') && gone.includes('400 mm') && gone.includes('이미 0'), gone)
80
- for (const said of [small, large, outside, zero, gone]) assert.doesNotMatch(said, /below|above|declare|authored|INSTANCE_SIZE_RANGE/)
79
+ assert.equal(zero, '도면에서의 너비 (X) 최소는 0 보다 커야 합니다.')
80
+ for (const said of [outside, open, zero]) assert.doesNotMatch(said, /below|above|declare|authored|INSTANCE_SIZE_RANGE|다시 재기/)
81
81
  assert.equal(sizeRangeRefusalOf(refusalOf(() => act(conveyor(), { kind: 'set-repeat', part: 'frame', pitch: 10 })), say), null, '다른 거절은 건드리지 않는다')
82
82
  })
83
83
 
84
- test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀 다시 권한다 — 다시 재기로는 풀리지 않는다 (사이징, 2026-09-25)', () => {
84
+ test('부품을 줄이는 연결은 거절되지 않고, 좁혀 둔 끝이 옮겨진 것을 한 줄로 말한다 (사용자 2026-10-21)', () => {
85
85
  const asset = panel()
86
- const link = followsWidth(-50)
87
- const error = refusalOf(() => act(asset, link))
88
- const offer = narrowingRetryOf(link, error, keyed)!
89
- assert.ok(offer, '권한다')
90
- assert.match(offer.label, /figure.button.narrow-range-from .*"axis":"X"/)
91
- assert.match(offer.message, /link-needs-smaller-range .*"part":"panel"/)
92
- assert.deepEqual(offer.retry, { ...link, narrowRange: true })
93
- assert.doesNotThrow(() => act(asset, offer.retry), '권한 것은 받아들여진다')
94
- assert.equal(narrowingRetryOf({ kind: 'attach' }, error, keyed), null, '연결이 아닌 명령에는 권하지 않는다')
86
+ const linked = act(asset, followsWidth(-50))
87
+ const fits = fm.figureSizeRangeFitsOf(asset, linked)
88
+ assert.equal(sizeRangeFitsOf(fits, say), '도면에서의 너비 (X) 최소를 1 mm 에서 50 mm 로 바꿨습니다.')
89
+ assert.equal(sizeRangeFitsOf([], say), '', '옮긴 것이 없으면 말하지 않는다')
95
90
  })
96
91
 
97
- test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그 이름을 한 줄로 말한다; 남는 거절은 공간 선언과 줄 (아키텍트 판정 2026-09-27)', () => {
92
+ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그 이름을 한 줄로 말한다; 남는 거절은 줄 (아키텍트 판정 2026-09-27)', () => {
98
93
  let a = fm.createFigureAsset('Bench')
99
94
  a = fm.addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 0, z: 0 } })
100
95
  a = fm.addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
@@ -116,37 +111,20 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
116
111
  assert.match(lamp, /removal-own-control .*"part":"shade".*"control":"shade".*"min":"-180".*"max":"180"/, '따르던 것은 제 조작값을 받는다')
117
112
  assert.equal(removalNoticeOf(fm.removePart(a, { kind: 'remove-part', target: 'shade' }).notice, keyed), null, '아무것도 풀지 않았으면 말하지 않는다')
118
113
 
119
- const declared = act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
120
114
  const why = (asset: FigureAsset, part: string) => partRefusalOf(refusalOf(() => fm.editFigurePart(asset, { kind: 'remove-part', target: part })), part, keyed)
121
- assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
122
115
  assert.match(why(conveyor(), 'frame')!, /part-row-along .*"part":"frame".*"rows":"roller"/, '줄이 따라 놓인 부품')
123
116
  let hung = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
124
117
  hung = act(hung, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
125
118
  assert.match(why(hung, 'roller')!, /part-row-carries .*"part":"roller".*"others":"cap"/, '줄에 매달린 것')
126
119
  })
127
120
 
128
- test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은 부품에 다시 맞추며 그렇다고 한 줄 말한다(묻지 않는다, 2026-09-27)', () => {
129
- let a = fm.addFigureBox(fm.createFigureAsset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
130
- a = fm.addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
131
- a = act(a, { kind: 'attach', part: 'housing', to: { plane: 'mounting-plane' } })
132
- a = act(a, { kind: 'attach', part: 'stray', to: { plane: 'mounting-plane' } })
133
- a = act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
134
- assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(a, { kind: 'remove-part', target: 'stray' }))), true)
135
- assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(conveyor(), { kind: 'remove-part', target: 'frame' }))), false, '줄은 이 길이 아니다')
136
- const after = fm.editFigurePart(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
137
- assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
138
- assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
139
- /* Removed in one click, nothing asked, the space fitted to the parts left and said so: parts.check.mjs. */
140
- assert.ok(ko['figure.text.removal-space-refitted'])
141
- })
142
-
143
121
  test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평면이 받는 면을 말한다(벽 배치, 09-25)', () => {
144
122
  const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
145
123
  kind: 'attach',
146
124
  part: 'post',
147
125
  to: { plane: 'mounting-plane' }
148
126
  })
149
- const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
127
+ const refused = refusalOf(() => act(a, { kind: 'set-placement', placement: 'wall' }))
150
128
  const said = placementRefusalOf(refused, say)!
151
129
  assert.equal(said, '장착 평면에 붙은 부품이 있어 놓이는 곳을 바꿀 수 없습니다: post. 먼저 이 부품을 떼어 주십시오.')
152
130
  assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
@@ -244,7 +222,7 @@ test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라
244
222
  part: 'post',
245
223
  to: { plane: 'mounting-plane' }
246
224
  })
247
- const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
225
+ const refused = refusalOf(() => act(a, { kind: 'set-placement', placement: 'wall' }))
248
226
  const record = refusalRecordOf(refused)
249
227
  assert.ok(record.details, 'its details kept')
250
228
  assert.equal(editRefusalOf(record, say), editRefusalOf(refused, say))
@@ -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, createFigureAsset, applyFigureAuthoring, applyFigureProposal, compileFigureAsset, figureFollowOf, figureRoomOnlyInputsOf, proposeFigureOccupancy, parseFigureAsset, planFigureResize, serializeFigureAsset } from '@hatiolab/figure-model'
4
+ import { addFigureBox, createFigureAsset, applyFigureAuthoring, applyFigureProposal, compileFigureAsset, figureFollowOf, parseFigureAsset, planFigureResize, serializeFigureAsset } from '@hatiolab/figure-model'
5
5
  import type { FigurePlacement } from '@hatiolab/figure-model'
6
6
  import { createFigureProductSession } from '../client/modeller/product-session.js'
7
7
  import { resizeRefusalOf } from '../client/modeller/refusals.js'
@@ -26,7 +26,7 @@ const START: Record<FigurePlacement, { x: number; y: number; z: number }> = {
26
26
  for (const mode of ['floor', 'ceiling', 'wall', 'center'] as const) {
27
27
  test(`${mode}: one step through the session -- undo, redo, save and open again`, async () => {
28
28
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: START[mode] })
29
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: mode })
29
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: mode })
30
30
  const before = serializeFigureAsset(a)
31
31
  let record = { id: 'ux', source: before }
32
32
  const session = createFigureProductSession({ load: async () => record, save: async q => (record = { id: 'ux', source: q.source }) })
@@ -60,7 +60,7 @@ for (const mode of ['floor', 'ceiling', 'wall', 'center'] as const) {
60
60
  test('a refusal is said in the author\'s words, never the kernel\'s English', () => {
61
61
  const said: Array<[string, Record<string, string>]> = []
62
62
  const say = (key: string, params: Record<string, string>) => (said.push([key, params]), key)
63
- /* No room declared: the figure stands on the floor by default. */
63
+ /* No placement set: the figure stands on the floor by default. */
64
64
  const a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 }, rotation: { x: 0, y: 30, z: 0 } })
65
65
  assert.equal(resizeRefusalOf(planFigureResize(a, { part: 'box' }), 'box', say), 'figure.text.resize-refused-turned')
66
66
  assert.deepEqual(said.at(-1)![1].place, 'figure.label.resize-place-floor')
@@ -74,7 +74,7 @@ test('a refusal is said in the author\'s words, never the kernel\'s English', ()
74
74
  */
75
75
  test('fresh, width only, and all set back to fixed are three different answers', async () => {
76
76
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 20, y: 50, z: 30 } })
77
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
77
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
78
78
  /* Fresh: all three are suggested -- and the button says "all three", never passing it off as the author's choice. */
79
79
  assert.deepEqual(resizeAxesOf(a, 'box'), { axes: ['x', 'y', 'z'], basis: 'suggested' })
80
80
  /* Width only ticked: width alone is planned, and height and depth stay fixed. */
@@ -99,19 +99,17 @@ test('fresh, width only, and all set back to fixed are three different answers',
99
99
 
100
100
  test('a part off the floor is not moved by choosing its size response', () => {
101
101
  let a = addFigureBox(createFigureAsset('UX'), { id: 'shelf', size: { x: 400, y: 40, z: 300 }, position: { x: 0, y: 520, z: 0 } })
102
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
102
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
103
103
  const asked = planFigureResize(a, { part: 'shelf', axes: resizeAxesOf(a, 'shelf').axes })
104
104
  assert.deepEqual([asked.refusal?.code, asked.refusal?.mm], ['GAP_CHOICE', 500])
105
105
  })
106
106
 
107
- test('a field only the room reads is taken out of the part\'s position row and shown apart', () => {
107
+ test('a part held by the floor shows X and Z in its position row, not Y', () => {
108
108
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 20, y: 50, z: 30 } })
109
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
109
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
110
110
  const resized = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
111
- const roomOnly = figureRoomOnlyInputsOf(resized, 'box')
112
- assert.ok(roomOnly.includes('box.position.y'))
113
111
  const fields = Object.entries(resized.designInputs).filter(([id]) => id.startsWith('box.')).map(([id, value]) => ({ id, unit: 'mm', role: 'design', value }))
114
- const { vectors } = groupFields(fields.filter(f => !roomOnly.includes(f.id)), 'box')
112
+ const { vectors } = groupFields(fields, 'box')
115
113
  const position = vectors.find(v => v.kind === 'vector' && v.name === 'position')!
116
114
  assert.ok(position.kind === 'vector' && position.cells.x && !position.cells.y && position.cells.z, 'X and Z still move it; Y is held by the floor')
117
115
  })
@@ -125,7 +123,7 @@ test('the part component leaves its place in a size drag to the modeller, for ea
125
123
  const { componentOf } = await import('../client/modeller/stage-model.js')
126
124
  for (const mode of ['floor', 'ceiling', 'wall', 'center'] as const) {
127
125
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: START[mode] })
128
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: mode })
126
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: mode })
129
127
  const body = compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'box') as never
130
128
  const state = componentOf(body, {}, { width: 1000, height: 1000 }, true)!
131
129
  assert.equal(state.scalePlacement, 'owner', mode)
@@ -138,9 +136,9 @@ test('the part component leaves its place in a size drag to the modeller, for ea
138
136
  test('a fastened part keeps its position and turn rows, with the numbers the rule gives', async () => {
139
137
  const { placementOf } = await import('../client/modeller/stage-model.js')
140
138
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 20, y: 50, z: 30 } })
141
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
139
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
142
140
  const resized = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
143
- const fields = Object.entries(resized.designInputs).filter(([id]) => id.startsWith('box.') && !figureRoomOnlyInputsOf(resized, 'box').includes(id)).map(([id, value]) => ({ id, unit: 'mm', role: 'design', value }))
141
+ const fields = Object.entries(resized.designInputs).filter(([id]) => id.startsWith('box.')).map(([id, value]) => ({ id, unit: 'mm', role: 'design', value }))
144
142
  const { vectors } = groupFields(fields, 'box')
145
143
  const rows = Object.fromEntries(vectors.map(v => [v.name, v]))
146
144
  assert.ok(rows.rotation && rows.position, 'the turn row stands though every turn axis is the joint\'s')
@@ -171,7 +169,7 @@ test('a part fastened at any angle is sized by the gizmo too: its fastening keep
171
169
  test('"open attachment settings" opens this part\'s fastening, as it is now, and not another tab', async () => {
172
170
  const { fasteningOf } = await import('../client/modeller/links.js')
173
171
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 20, y: 150, z: 30 } })
174
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
172
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
175
173
  const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box', gap: 'keep' }).steps)
176
174
  const form = fasteningOf(revisionOf(kept), 'box')!
177
175
  assert.equal(form.toPlane, true, 'on the plane')
@@ -186,7 +184,7 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
186
184
  test('the fastening form keeps a place that follows a position field, and fastening again keeps the field', async () => {
187
185
  const { fasteningOf, alignOf } = await import('../client/modeller/links.js')
188
186
  let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 200, y: 50, z: 150 } })
189
- a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
187
+ a = applyFigureAuthoring(a, { kind: 'set-placement', placement: 'floor' })
190
188
  const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
191
189
  const form = fasteningOf(revisionOf(kept), 'box')!
192
190
  assert.deepEqual([form.align.x, form.alignInput.x, form.align.z, form.alignInput.z], ['input', 'box.position.x', 'input', 'box.position.z'])
@@ -12,7 +12,7 @@ import assert from 'node:assert/strict'
12
12
  import { readFileSync } from 'node:fs'
13
13
  import { fileURLToPath } from 'node:url'
14
14
 
15
- import { addFigureBox, applyFigureAuthoring, createFigureAsset, figureFaceOverhangOf, figurePartFaceOf, figurePlaneAlignChanges, figureRoomOnlyInputsOf, figureRowOf, measureFigureWork, serializeFigureAsset } from '@hatiolab/figure-model'
15
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, figureFaceOverhangOf, figurePartFaceOf, figurePlaneAlignChanges, figureRowOf, measureFigureWork, serializeFigureAsset } from '@hatiolab/figure-model'
16
16
  import type { FigureAsset } from '@hatiolab/figure-model'
17
17
 
18
18
  import { createFigureProductSession } from '../client/modeller/product-session.ts'
@@ -88,7 +88,6 @@ function selectionQuestions(shown: FigureRevision, part: string) {
88
88
  gridStepOf(shown)
89
89
  jointFanOf(shown, part)
90
90
  partSummaryOf(asset, part, key => key, 'ko')
91
- figureRoomOnlyInputsOf(asset, part)
92
91
  for (const axis of ['x', 'y', 'z'] as const) measureNow(shown, part, axis)
93
92
  for (const axis of ['x', 'z'] as const) canMirror(shown, part, axis, reach)
94
93
  figurePlaneAlignChanges(asset, part)
@@ -17,7 +17,6 @@ import { addFigureBox, createFigureAsset, editFigurePart, figureCostOf } from '@
17
17
  import { budgetOf, figureReportOf, verdictMeasureOf } from '../client/modeller/maturity.ts'
18
18
  import { newestAnswer, type FigureOutcome } from '../client/modeller/newest-answer.ts'
19
19
  import { capacityFrom, SCENE_BUDGET } from '../client/modeller/scene-budget.ts'
20
- import { moving } from './joints.ts'
21
20
  import { wordsOf } from './words.ts'
22
21
 
23
22
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
@@ -37,7 +36,7 @@ test('개수는 목록 카드와 같은 셈이다 — 원가를 같은 capacityF
37
36
  assert.deepEqual(measured.report, figureReportOf(asset), '탭은 그 셈을 부르고 보여 줄 뿐이다')
38
37
  assert.equal(measured.told.instances, capacityFrom(cost.groups, cost.triangles)!.instances, '탭 이름표의 개수도 같은 셈에서 온다')
39
38
  assert.deepEqual(measured.told.budget, budgetOf(figureReportOf(asset)), '부품 목록 머리의 줄도 같은 보고서에서 온다')
40
- assert.deepEqual(verdictMeasureOf(undefined), { report: undefined, bodies: 0, told: { instances: undefined, budget: undefined } }, '연 도형이 없으면 잴 것도 알릴 개수도 없다')
39
+ assert.deepEqual(verdictMeasureOf(undefined), { report: undefined, told: { instances: undefined, budget: undefined } }, '연 도형이 없으면 잴 것도 알릴 개수도 없다')
41
40
  })
42
41
 
43
42
  test('판정은 발행 패널과 같은 서버 문에서 온다 — 화면이 따로 판정하지 않는다', () => {
@@ -80,19 +79,6 @@ test('판정 탭의 말은 다섯 말 모두에 있다 — 없으면 키가 그
80
79
  }
81
80
  })
82
81
 
83
- test('감춘 부품도 「부품에 맞추기」가 잴 수 있는 부품이다', () => {
84
- /* 새 도형은 부품 없이도 「공간 선언 없음」으로 막힌다. 그때 맞추기는 OCCUPANCY_NO_PARTS 로 던진다 — 단추는 잴 부품이 있을 때만. */
85
- let asset = createFigureAsset('Probe')
86
- asset = addFigureBox(asset, { id: 'base', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
87
- asset = addFigureBox(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 200, y: 50, z: 0 } })
88
- asset = moving(asset, 'base', { kind: 'turn', axis: 'y', id: 'base.turn', min: 0, max: 90 })
89
- asset = editFigurePart(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'base.turn', atLeast: 80 } })
90
- const cost = figureCostOf(asset)
91
- assert.equal(cost.hidden, 1, '전제: 등은 지금 감춰져 있다')
92
- assert.equal(verdictMeasureOf(asset).bodies, 2, '그려지는 하나와 감춘 하나')
93
- assert.equal(verdictMeasureOf(createFigureAsset('Empty')).bodies, 0, '부품이 없으면 잴 것이 없다')
94
- })
95
-
96
82
  test('a finding\'s affected parts are the figure\'s part names, not the format\'s words (user 2026-10-16: 「occupancy · roller-row-1-roller」)', async () => {
97
83
  const { affectedPartsOf } = await import('../client/modeller/affected-parts.ts')
98
84
  assert.deepEqual(affectedPartsOf('occupancy · roller-row-1-roller', ['box', 'roller-row-1-roller']), ['roller-row-1-roller'])