@things-factory/figure-ui 10.1.204 → 10.1.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/canvas.ts +91 -9
- package/client/modeller/editor-styles.ts +23 -22
- package/client/modeller/editor.ts +217 -184
- package/client/modeller/fasten-form.ts +130 -12
- package/client/modeller/figure-fasten-dialog.ts +45 -11
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +22 -1
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +79 -46
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +23 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +35 -35
- package/dist-client/modeller/editor.js +221 -193
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +46 -4
- package/dist-client/modeller/fasten-form.js +120 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
- package/dist-client/modeller/figure-fasten-dialog.js +49 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +18 -1
- package/dist-client/modeller/gizmo.js +19 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +30 -24
- package/dist-client/modeller/links.js +67 -42
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +11 -1
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +124 -69
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +77 -26
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +29 -2
- package/test/fasten-form.test.ts +102 -1
- package/test/gizmo.test.ts +25 -16
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +46 -36
- package/test/refusals.test.ts +17 -21
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +32 -34
- package/translations/es.json +32 -34
- package/translations/ja.json +32 -34
- package/translations/ko.json +32 -34
- package/translations/ms.json +32 -34
- package/translations/zh.json +32 -34
package/test/links.test.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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',
|
|
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.
|
|
221
|
-
assert.deepEqual(range.
|
|
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('
|
|
228
|
-
let asset =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
assert.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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)', () => {
|
|
@@ -367,8 +372,8 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
|
|
|
367
372
|
assert.deepEqual(motionOf(asset, 'right')?.follows, { sum: [{ joint: 'left', multiplier: -1 }], offset: 0 }, '화면은 무엇을 따르는지 읽는다')
|
|
368
373
|
})
|
|
369
374
|
|
|
370
|
-
test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은
|
|
371
|
-
const { facesOf, faceAxisOf, canFasten, alignAxesOf } = await import('../client/modeller/links.ts')
|
|
375
|
+
test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없고 곡면, 원뿔대는 위아래뿐, 원기둥 옆은 면이 아님 (2026-09-25, ADR-0140 3.9)', async () => {
|
|
376
|
+
const { facesOf, faceAxisOf, canFasten, alignAxesOf, surfaceOf } = await import('../client/modeller/links.ts')
|
|
372
377
|
let asset = createFigureAsset('Doorway')
|
|
373
378
|
asset = addFigurePrimitive(asset, {
|
|
374
379
|
id: 'frame',
|
|
@@ -376,13 +381,18 @@ 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: {
|
|
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 } })
|
|
387
|
+
asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
382
388
|
assert.deepEqual(facesOf(asset, 'frame'), ['left', 'right', 'bottom', 'top', 'back', 'front', 'opening-left', 'opening-right', 'opening-top'])
|
|
383
389
|
assert.equal(faceAxisOf(asset, 'frame', 'opening-top'), 'y')
|
|
384
390
|
assert.deepEqual(facesOf(asset, 'ball'), [])
|
|
385
|
-
assert.equal(canFasten(asset, 'ball'),
|
|
391
|
+
assert.equal(canFasten(asset, 'ball'), true, '공은 면이 없어도 곡면 위 점에 붙인다')
|
|
392
|
+
assert.deepEqual(surfaceOf(asset, 'ball'), { form: 'toward', axis: 'y' })
|
|
393
|
+
assert.deepEqual(facesOf(asset, 'drum'), ['bottom', 'top'], '원기둥 옆은 곡면이라 면이 아니다')
|
|
394
|
+
assert.deepEqual(surfaceOf(asset, 'drum'), { form: 'around', axis: 'y' })
|
|
395
|
+
assert.equal(surfaceOf(asset, 'cone'), undefined, '원뿔대 옆은 아직 받지 않는다')
|
|
386
396
|
assert.equal(canFasten(asset, 'frame'), true)
|
|
387
397
|
assert.deepEqual(facesOf(asset, 'cone'), ['bottom', 'top'])
|
|
388
398
|
assert.deepEqual(alignAxesOf(asset, { part: 'frame', face: 'opening-left' }), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
@@ -422,18 +432,18 @@ test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자
|
|
|
422
432
|
})
|
|
423
433
|
|
|
424
434
|
test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
|
|
425
|
-
const {
|
|
435
|
+
const { instanceSizeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
|
|
426
436
|
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
427
437
|
let asset = createFigureAsset('Conveyor')
|
|
428
|
-
assert.equal(
|
|
438
|
+
assert.equal(instanceSizeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
|
|
429
439
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
430
440
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
431
441
|
assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
|
|
432
|
-
const toDeclare =
|
|
442
|
+
const toDeclare = instanceSizeToDeclare(asset)!
|
|
433
443
|
assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
|
|
434
|
-
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size',
|
|
435
|
-
assert.ok(instanceRangeOf(asset), '선언 뒤에는
|
|
436
|
-
assert.equal(
|
|
444
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', at: toDeclare.at })
|
|
445
|
+
assert.ok(instanceRangeOf(asset), '선언 뒤에는 도면에서의 크기가 보인다')
|
|
446
|
+
assert.equal(instanceSizeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
|
|
437
447
|
assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
|
|
438
448
|
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
|
|
439
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']
|
|
@@ -500,7 +510,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
|
|
|
500
510
|
})
|
|
501
511
|
|
|
502
512
|
test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
|
|
503
|
-
const {
|
|
513
|
+
const { instanceSizeToDeclare } = await import('../client/modeller/links.ts')
|
|
504
514
|
let asset = createFigureAsset('Door')
|
|
505
515
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
506
516
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
@@ -508,7 +518,7 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
508
518
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
509
519
|
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
510
520
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
511
|
-
assert.deepEqual(
|
|
521
|
+
assert.deepEqual(instanceSizeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
512
522
|
})
|
|
513
523
|
|
|
514
524
|
test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
|
package/test/refusals.test.ts
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
editRefusalOf,
|
|
18
18
|
inDeclaredRoom,
|
|
19
19
|
inputRangeRefusalOf,
|
|
20
|
-
narrowingRetryOf,
|
|
21
20
|
partNameRefusalOf,
|
|
22
21
|
partRefusalOf,
|
|
23
22
|
placedRefusalOf,
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
refusalRecordOf,
|
|
27
26
|
removalNoticeOf,
|
|
28
27
|
repeatPitchRefusalOf,
|
|
28
|
+
sizeRangeFitsOf,
|
|
29
29
|
sizeRangeRefusalOf
|
|
30
30
|
} from '../client/modeller/refusals.ts'
|
|
31
31
|
import { wordsOf } from './words.ts'
|
|
@@ -35,6 +35,7 @@ const say = (key: string, params: Record<string, string>) => {
|
|
|
35
35
|
assert.ok(key in ko, `no words for ${key}`)
|
|
36
36
|
return ko[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
|
|
37
37
|
}
|
|
38
|
+
|
|
38
39
|
/** The key and its values, to see which words were chosen. */
|
|
39
40
|
const keyed = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
40
41
|
|
|
@@ -66,32 +67,27 @@ function conveyor(): FigureAsset {
|
|
|
66
67
|
return act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/*
|
|
71
|
+
* 크기 범위로 남은 거절은 둘 — 작성자가 친 끝이 지금 크기를 빼거나 0 에서 시작할 때(ADR-0087 결정 6). 부품을 바꾸는 명령은
|
|
72
|
+
* 범위 때문에 거절되지 않고, 작성자가 좁힌 끝이 부품이 허락하는 데까지 옮겨진 것을 한 줄로 말한다(사용자 2026-10-21).
|
|
73
|
+
*/
|
|
74
|
+
test('크기 범위의 거절 둘 — 축 이름 · 숫자를 커널이 넘긴 값으로 말한다', () => {
|
|
74
75
|
const outside = sizeRangeRefusalOf(refusalOf(() => act(box(), declaring({ min: 500, max: 4000 }))), say)!
|
|
75
|
-
assert.
|
|
76
|
+
assert.equal(outside, '도면에서의 너비 (X) 범위 500 – 4,000 mm 에 지금 크기 400 mm 가 들어가지 않습니다.')
|
|
77
|
+
const open = sizeRangeRefusalOf(refusalOf(() => act(box(), { kind: 'declare-instance-size', range: { x: { min: 500 } } })), say)!
|
|
78
|
+
assert.equal(open, '도면에서의 너비 (X) 범위 500 – ∞ mm 에 지금 크기 400 mm 가 들어가지 않습니다.', '한쪽 끝만 친 범위')
|
|
76
79
|
const zero = sizeRangeRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say)!
|
|
77
|
-
assert.
|
|
78
|
-
const
|
|
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/)
|
|
80
|
+
assert.equal(zero, '도면에서의 너비 (X) 최소는 0 보다 커야 합니다.')
|
|
81
|
+
for (const said of [outside, open, zero]) assert.doesNotMatch(said, /below|above|declare|authored|INSTANCE_SIZE_RANGE|다시 재기/)
|
|
81
82
|
assert.equal(sizeRangeRefusalOf(refusalOf(() => act(conveyor(), { kind: 'set-repeat', part: 'frame', pitch: 10 })), say), null, '다른 거절은 건드리지 않는다')
|
|
82
83
|
})
|
|
83
84
|
|
|
84
|
-
test('
|
|
85
|
+
test('부품을 줄이는 연결은 거절되지 않고, 좁혀 둔 끝이 옮겨진 것을 한 줄로 말한다 (사용자 2026-10-21)', () => {
|
|
85
86
|
const asset = panel()
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
assert.
|
|
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, '연결이 아닌 명령에는 권하지 않는다')
|
|
87
|
+
const linked = act(asset, followsWidth(-50))
|
|
88
|
+
const fits = fm.figureSizeRangeFitsOf(asset, linked)
|
|
89
|
+
assert.equal(sizeRangeFitsOf(fits, say), '도면에서의 너비 (X) 최소를 1 mm 에서 50 mm 로 바꿨습니다.')
|
|
90
|
+
assert.equal(sizeRangeFitsOf([], say), '', '옮긴 것이 없으면 말하지 않는다')
|
|
95
91
|
})
|
|
96
92
|
|
|
97
93
|
test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그 이름을 한 줄로 말한다; 남는 거절은 공간 선언과 줄 (아키텍트 판정 2026-09-27)', () => {
|
|
@@ -117,22 +117,20 @@ test('a field only the room reads is taken out of the part\'s position row and s
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
/*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* ADR-0140 4.4: what the drag shows is what is written. The part component asks the scene to leave its place to the
|
|
121
|
+
* modeller (things-scene `scalePlacement: 'owner'`), which draws each frame from figure-model's one size function --
|
|
122
|
+
* whatever the placement; no face is named to the scene.
|
|
122
123
|
*/
|
|
123
|
-
test('the part component
|
|
124
|
+
test('the part component leaves its place in a size drag to the modeller, for each placement', async () => {
|
|
124
125
|
const { componentOf } = await import('../client/modeller/stage-model.js')
|
|
125
|
-
const { figureScaleAnchorOf } = await import('@hatiolab/figure-model')
|
|
126
|
-
const expected = { floor: { x: 0, y: 1, z: 0 }, ceiling: { x: 0, y: -1, z: 0 }, wall: { x: 0, y: 0, z: 1 }, center: { x: 0, y: 0, z: 0 } } as const
|
|
127
126
|
for (const mode of ['floor', 'ceiling', 'wall', 'center'] as const) {
|
|
128
127
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: START[mode] })
|
|
129
128
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: mode })
|
|
130
|
-
const anchor = figureScaleAnchorOf(a, 'box')!
|
|
131
|
-
assert.deepEqual(anchor, expected[mode], mode)
|
|
132
129
|
const body = compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'box') as never
|
|
133
|
-
const state = componentOf(body, {}, { width: 1000, height: 1000 },
|
|
134
|
-
assert.
|
|
135
|
-
assert.equal(
|
|
130
|
+
const state = componentOf(body, {}, { width: 1000, height: 1000 }, true)!
|
|
131
|
+
assert.equal(state.scalePlacement, 'owner', mode)
|
|
132
|
+
assert.equal(state.scaleAnchor, undefined, `${mode}: no face named to the scene`)
|
|
133
|
+
assert.equal(componentOf(body, {}, { width: 1000, height: 1000 })!.scalePlacement, undefined, 'a shown-only body is not')
|
|
136
134
|
}
|
|
137
135
|
})
|
|
138
136
|
|
|
@@ -155,12 +153,15 @@ test('size response and attachment are separate screen paths', () => {
|
|
|
155
153
|
assert.equal(ko['figure.text.held-on-plane'].replace('{place}', '바닥').replace('{gap}', '0'), '바닥에 붙어 있음 (간격 0 mm)')
|
|
156
154
|
})
|
|
157
155
|
|
|
158
|
-
test('a part fastened
|
|
159
|
-
const {
|
|
156
|
+
test('a part fastened at any angle is sized by the gizmo too: its fastening keeps it, and nothing else is written', async () => {
|
|
157
|
+
const { figureSizeEditOf } = await import('@hatiolab/figure-model')
|
|
158
|
+
const { gizmoReachOf } = await import('../client/modeller/gizmo.js')
|
|
160
159
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'hub', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
161
160
|
a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
|
|
162
161
|
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'blade', type: 'fixed', body0: { part: 'hub', face: 'top' }, body1: { part: 'blade' }, origin: { childRotate: { z: 30 } } } })
|
|
163
|
-
assert.
|
|
162
|
+
assert.ok(gizmoReachOf(a, 'blade').scale, 'offered')
|
|
163
|
+
const size = [{ kind: 'set-design', input: 'blade.size.x', unit: 'mm', value: 400 }] as never
|
|
164
|
+
assert.deepEqual(figureSizeEditOf(a, 'blade', size, { keep: 'opposite', axis: 'x', side: 1 }), { changes: size, kept: 'relation' })
|
|
164
165
|
})
|
|
165
166
|
|
|
166
167
|
/*
|
package/test/revision.test.ts
CHANGED
|
@@ -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,
|
|
15
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, figureFaceOverhangOf, figurePartFaceOf, figurePlaneAlignChanges, figureRoomOnlyInputsOf, 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'
|
|
@@ -24,6 +24,7 @@ import { faceViewOf } from '../client/modeller/fasten-geometry.ts'
|
|
|
24
24
|
import { motionEndsOf } from '../client/modeller/motion-ends.ts'
|
|
25
25
|
import { figureFigureFacts } from '../client/modeller/figure-facts.ts'
|
|
26
26
|
import { gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
27
|
+
import { keptBySizeOf } from '../client/modeller/size-drag.ts'
|
|
27
28
|
import { moving } from './joints.ts'
|
|
28
29
|
import { revisionOf } from './revision-of.ts'
|
|
29
30
|
|
|
@@ -82,7 +83,7 @@ test('a version is readable while it is saved, and a save hands out the same tex
|
|
|
82
83
|
function selectionQuestions(shown: FigureRevision, part: string) {
|
|
83
84
|
const asset = shown.asset
|
|
84
85
|
const reach = gizmoReachOf(asset as never, part)
|
|
85
|
-
|
|
86
|
+
keptBySizeOf(asset, part)
|
|
86
87
|
boxOf(shown, part)
|
|
87
88
|
gridStepOf(shown)
|
|
88
89
|
jointFanOf(shown, part)
|
|
@@ -173,7 +173,7 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
|
|
|
173
173
|
const next = applyFigureAuthoring(box, { kind: 'set-section', part: 'b', across: { x: 500 }, length: 260 })
|
|
174
174
|
const b = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape; pose: { t: number[] } })
|
|
175
175
|
assert.deepEqual(b.shape.extrusion!.section, { kind: 'rectangle', size: { x: 500, z: 300 } })
|
|
176
|
-
assert.
|
|
176
|
+
assert.deepEqual([b.pose.t[1]! + b.shape.extrusion!.start, b.pose.t[1]! + b.shape.extrusion!.end], [-30, 230], 'a free part keeps its middle (ADR-0140 4.1)')
|
|
177
177
|
/* A corner that no longer fits, and a length of 0, are said before anything is sent. */
|
|
178
178
|
const rounded = applyFigureAuthoring(box, { kind: 'set-section-corner', part: 'b', corner: 100 })
|
|
179
179
|
const r = (compileFigureAsset(rounded).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape }).shape
|
|
@@ -217,15 +217,14 @@ test('창 안에서 폭과 속 공간을 함께 — 넓힌 폭에서만 들어
|
|
|
217
217
|
assert.deepEqual(sectionActionOf('t', 'rectangle', w, { ...w, cavity: null }), { kind: 'set-section', part: 't', cavity: null })
|
|
218
218
|
})
|
|
219
219
|
|
|
220
|
-
test('치수 칸 밑의 말: 다른 값을 따르면 그렇다고, 길이는
|
|
221
|
-
assert.deepEqual(measureNoteOf('linked',
|
|
222
|
-
assert.deepEqual(measureNoteOf('editable',
|
|
223
|
-
assert.deepEqual(measureNoteOf('editable',
|
|
224
|
-
assert.deepEqual(measureNoteOf('editable',
|
|
225
|
-
assert.
|
|
226
|
-
assert.equal(measureNoteOf(
|
|
227
|
-
assert.
|
|
228
|
-
assert.equal(say('figure.text.section-editor-keeps', { face: ko['figure.label.face-bottom']! }), '아랫면 유지')
|
|
220
|
+
test('치수 칸 밑의 말: 다른 값을 따르면 그렇다고, 길이는 가운데나 붙인 위치를 지킨다고 — 말할 것이 없으면 아무것도', () => {
|
|
221
|
+
assert.deepEqual(measureNoteOf('linked', 'centre'), { key: 'figure.text.section-editor-linked' }, 'following another value is said first')
|
|
222
|
+
assert.deepEqual(measureNoteOf('editable', 'centre'), { key: 'figure.text.size-keeps-centre' }, 'a free part keeps its middle (ADR-0140 4.1)')
|
|
223
|
+
assert.deepEqual(measureNoteOf('editable', 'relation'), { key: 'figure.text.size-keeps-fastening' }, 'a fastened part keeps where it is fastened')
|
|
224
|
+
assert.deepEqual(measureNoteOf('editable', 'none'), { key: 'figure.text.size-keeps-none' }, 'a position worked out: the size alone changes')
|
|
225
|
+
assert.equal(measureNoteOf('editable', undefined), null, 'nothing known: nothing')
|
|
226
|
+
assert.equal(measureNoteOf(undefined, undefined), null, 'a width across says only that it follows')
|
|
227
|
+
assert.deepEqual([say('figure.text.size-keeps-centre', {}), say('figure.text.size-keeps-fastening', {})], ['가운데 유지', '붙인 위치 유지'])
|
|
229
228
|
})
|
|
230
229
|
|
|
231
230
|
test('ADR-0100 A1: 상자·판·벽은 같은 돌출에 처음 치수만 다르고, 더해도 숨은 부착이나 연결이 생기지 않는다', async () => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A size drag drawn from figure-model frame by frame (ADR-0140 4.4, `size-drag.ts`): each frame is what its release
|
|
3
|
+
* writes -- the same changes, and every body where the committed figure puts it, to the bit. A free part keeps the
|
|
4
|
+
* side opposite the handle; a fastened one moves what hangs from it, by its fastening.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
|
|
9
|
+
import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAsset, figureSizeEditOf } from '@hatiolab/figure-model'
|
|
10
|
+
import type { FigureAsset, FigureInputChange, FigureSizeKeep } from '@hatiolab/figure-model'
|
|
11
|
+
|
|
12
|
+
import { partsAtFrame, startSizeDrag } from '../client/modeller/size-drag.ts'
|
|
13
|
+
import { drawableFigureParts, type FigureRenderPart } from '../client/modeller/renderer.ts'
|
|
14
|
+
|
|
15
|
+
const set = (input: string, value: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value })
|
|
16
|
+
const committed = (asset: FigureAsset, changes: FigureInputChange[]) =>
|
|
17
|
+
drawableFigureParts(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, ...Object.fromEntries(changes.map(c => [c.input, c.value])) } }).evaluate().geometry)
|
|
18
|
+
const drawn = (asset: FigureAsset) => drawableFigureParts(compileFigureAsset(asset).evaluate().geometry)
|
|
19
|
+
const posesOf = (parts: FigureRenderPart[]) => Object.fromEntries(parts.map(p => [p.id, { r: p.pose.r, t: p.pose.t, dimensions: p.shape.dimensions }]))
|
|
20
|
+
|
|
21
|
+
/* A turned box on the floor, and a lid fastened on its top. */
|
|
22
|
+
function figure() {
|
|
23
|
+
let a = addFigureBox(createFigureAsset('Drag'), { id: 'box', size: { x: 200, y: 100, z: 150 }, position: { x: 40, y: 300, z: -20 }, rotation: { x: 10, y: 30, z: 0 } })
|
|
24
|
+
a = addFigureBox(a, { id: 'base', size: { x: 400, y: 100, z: 300 }, position: { x: 0, y: 50, z: 0 } })
|
|
25
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } })
|
|
26
|
+
a = addFigureBox(a, { id: 'lid', size: { x: 100, y: 20, z: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
27
|
+
return applyFigureAuthoring(a, { kind: 'attach', part: 'lid', to: { part: 'base', face: 'top' } })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('each frame of a free part is what its release writes: the same changes, every body where the commit puts it', () => {
|
|
31
|
+
const asset = figure()
|
|
32
|
+
const keep: FigureSizeKeep = { keep: 'opposite', axis: 'x', side: 1 }
|
|
33
|
+
const drag = startSizeDrag(asset, 'box', {}, keep)
|
|
34
|
+
for (const width of [230, 300, 180]) {
|
|
35
|
+
const sizes = [set('box.size.x', width)]
|
|
36
|
+
const frame = drag.frame(sizes)
|
|
37
|
+
assert.deepEqual(frame.changes, figureSizeEditOf(asset, 'box', sizes, keep).changes, `${width}: the changes`)
|
|
38
|
+
assert.deepEqual(posesOf(partsAtFrame(drawn(asset), frame.moved)), posesOf(committed(asset, frame.changes)), `${width}: the bodies`)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a fastened part dragged: what hangs from it moves by its fastening, frame by frame, as the commit puts it', () => {
|
|
43
|
+
const asset = figure()
|
|
44
|
+
const drag = startSizeDrag(asset, 'base', {}, { keep: 'opposite', axis: 'y', side: 1 })
|
|
45
|
+
for (const height of [140, 220]) {
|
|
46
|
+
const frame = drag.frame([set('base.size.y', height)])
|
|
47
|
+
assert.equal(frame.changes.length, 1, 'its size alone: its fastening places it')
|
|
48
|
+
assert.ok(frame.moved.has('lid'), 'the lid on its top moves with it')
|
|
49
|
+
assert.deepEqual(posesOf(partsAtFrame(drawn(asset), frame.moved)), posesOf(committed(asset, frame.changes)), `${height}`)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a body that moved and then stood still is drawn where it stopped, not where the drag began', () => {
|
|
54
|
+
const asset = figure()
|
|
55
|
+
const drag = startSizeDrag(asset, 'base', {}, { keep: 'centre' })
|
|
56
|
+
drag.frame([set('base.size.y', 200)])
|
|
57
|
+
/* The same height again: nothing changes in this frame, and the lid stays up where the last frame put it. */
|
|
58
|
+
const frame = drag.frame([set('base.size.y', 200)])
|
|
59
|
+
assert.deepEqual(posesOf(partsAtFrame(drawn(asset), frame.moved)), posesOf(committed(asset, frame.changes)))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('the handle held is what is kept from: one axis keeps the side opposite it; the uniform handle, or none, the middle', async () => {
|
|
63
|
+
const { sizeKeepOf } = await import('../client/modeller/gizmo.ts')
|
|
64
|
+
assert.deepEqual(sizeKeepOf({ x: 0, y: 1, z: 0 }), { keep: 'opposite', axis: 'y', side: 1 })
|
|
65
|
+
assert.deepEqual(sizeKeepOf({ x: -1, y: 0, z: 0 }), { keep: 'opposite', axis: 'x', side: -1 })
|
|
66
|
+
assert.deepEqual(sizeKeepOf({ x: 1, y: 1, z: 1 }), { keep: 'centre' }, 'the uniform handle')
|
|
67
|
+
assert.deepEqual(sizeKeepOf(undefined), { keep: 'centre' }, 'no handle known')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('what a size typed keeps, as the panel says it: the middle of a free part, the fastening of a fastened one', async () => {
|
|
71
|
+
const { keptBySizeOf } = await import('../client/modeller/size-drag.ts')
|
|
72
|
+
const asset = figure()
|
|
73
|
+
assert.deepEqual(['box', 'base', 'lid'].map(part => keptBySizeOf(asset, part)), ['centre', 'relation', 'relation'])
|
|
74
|
+
/* Its height placed by a rule: no point is kept, only the size changes -- said as such (chief architect 10-20). */
|
|
75
|
+
const ruled = structuredClone(asset) as FigureAsset
|
|
76
|
+
;(ruled.document.model as unknown as { nodes: Array<{ id: string; args: string[] }> }).nodes.find(n => n.id === 'box.pose')!.args[1] = 'box.position.x'
|
|
77
|
+
assert.equal(keptBySizeOf(ruled, 'box'), 'none')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('how far a free part goes past its mounting plane: its real extreme against the side its placement uses', async () => {
|
|
81
|
+
const { throughPlaneOf } = await import('../client/modeller/size-drag.ts')
|
|
82
|
+
const box = (placement: 'floor' | 'ceiling' | 'wall' | 'center', position: { x: number; y: number; z: number }, rotation = { x: 0, y: 0, z: 0 }) =>
|
|
83
|
+
applyFigureAuthoring(addFigureBox(createFigureAsset('P'), { id: 'b', size: { x: 100, y: 100, z: 100 }, position, rotation }), { kind: 'set-placement', placement })
|
|
84
|
+
assert.equal(throughPlaneOf(box('floor', { x: 0, y: 20, z: 0 }), drawn(box('floor', { x: 0, y: 20, z: 0 })), 'b'), 30, 'its bottom 30 under the floor')
|
|
85
|
+
assert.equal(throughPlaneOf(box('floor', { x: 0, y: 50, z: 0 }), drawn(box('floor', { x: 0, y: 50, z: 0 })), 'b'), 0, 'standing on it')
|
|
86
|
+
assert.equal(throughPlaneOf(box('ceiling', { x: 0, y: -40, z: 0 }), drawn(box('ceiling', { x: 0, y: -40, z: 0 })), 'b'), 10, 'its top 10 above the ceiling')
|
|
87
|
+
assert.equal(throughPlaneOf(box('wall', { x: 0, y: 0, z: 45 }), drawn(box('wall', { x: 0, y: 0, z: 45 })), 'b'), 5, 'its back 5 into the wall')
|
|
88
|
+
assert.equal(throughPlaneOf(box('center', { x: 0, y: -500, z: 0 }), drawn(box('center', { x: 0, y: -500, z: 0 })), 'b'), 0, 'a figure placed by its centre has no plane')
|
|
89
|
+
/* Turned 45° about Z, its lowest corner is √2·50 under its middle. */
|
|
90
|
+
const turned = box('floor', { x: 0, y: 60, z: 0 }, { x: 0, y: 0, z: 45 })
|
|
91
|
+
assert.equal(throughPlaneOf(turned, drawn(turned), 'b'), Math.round((50 * Math.SQRT2 - 60) * 10) / 10, 'its real corner, not its upright box')
|
|
92
|
+
const fastened = applyFigureAuthoring(box('floor', { x: 0, y: 20, z: 0 }), { kind: 'attach', part: 'b', to: { plane: 'mounting-plane' } })
|
|
93
|
+
assert.equal(throughPlaneOf(fastened, drawn(fastened), 'b'), 0, 'its fastening places it')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
A part whose height is placed by a rule keeps nothing a handle could keep (kept 'none'): only its size is written. The
|
|
98
|
+
drag draws exactly that, frame by frame -- it does not look held at the opposite side and then land elsewhere (ADR-0097
|
|
99
|
+
§4.3: a drag whose frames differ from its release is not done; chief architect 10-20).
|
|
100
|
+
*/
|
|
101
|
+
test('a position placed by a rule: each frame draws what the release writes -- the size alone, kept none', () => {
|
|
102
|
+
/* A command's result is sealed (ADR-0119): the rule goes into a copy. Its height now reads its x. */
|
|
103
|
+
const ruled = structuredClone(figure()) as FigureAsset
|
|
104
|
+
;(ruled.document.model as unknown as { nodes: Array<{ id: string; args: string[] }> }).nodes.find(n => n.id === 'box.pose')!.args[1] = 'box.position.x'
|
|
105
|
+
const keep: FigureSizeKeep = { keep: 'opposite', axis: 'y', side: 1 }
|
|
106
|
+
const drag = startSizeDrag(ruled, 'box', {}, keep)
|
|
107
|
+
for (const height of [160, 240]) {
|
|
108
|
+
const sizes = [set('box.size.y', height)]
|
|
109
|
+
const frame = drag.frame(sizes)
|
|
110
|
+
assert.deepEqual(figureSizeEditOf(ruled, 'box', sizes, keep), { changes: sizes, kept: 'none' }, `${height}: the size alone`)
|
|
111
|
+
assert.deepEqual(frame.changes, sizes, `${height}: the frame writes the same`)
|
|
112
|
+
assert.deepEqual(posesOf(partsAtFrame(drawn(ruled), frame.moved)), posesOf(committed(ruled, frame.changes)), `${height}: drawn as written`)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
@@ -37,22 +37,21 @@ test("no layout yet: a form on the selected part's top, and the equipment preset
|
|
|
37
37
|
assert.equal(layout, undefined)
|
|
38
38
|
const faces = anchorFacesOf(form.anchors, compileFigureAsset(asset).evaluate().geometry as never)
|
|
39
39
|
assert.deepEqual(faces, { fork: { width: 1200, depth: 900 } })
|
|
40
|
-
const
|
|
41
|
-
const presets = slotPresetsOf(form.anchors, say)
|
|
40
|
+
const presets = slotPresetsOf(form.anchors)
|
|
42
41
|
assert.deepEqual(presets.map(p => p.key)[0], 'single', 'a single slot first: where an author starts')
|
|
43
42
|
assert.equal(presets.length, 9)
|
|
44
43
|
const size = { ...faces.fork!, slotHeight: 1000 }
|
|
45
44
|
const single = presets[0]!.build(size, {})
|
|
46
45
|
assert.deepEqual(single.tiers[0]!, { tierNo: 1, anchorId: 'fork', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 1200, depth: 900, height: 1000 } } } })
|
|
47
46
|
assert.deepEqual(presets.find(p => p.key === 'floorBlock')!.params, [
|
|
48
|
-
{ key: 'rows',
|
|
49
|
-
{ key: 'cols',
|
|
50
|
-
{ key: 'stackLimit',
|
|
47
|
+
{ key: 'rows', integer: true },
|
|
48
|
+
{ key: 'cols', integer: true },
|
|
49
|
+
{ key: 'stackLimit', integer: true }
|
|
51
50
|
])
|
|
52
51
|
/* Two anchors: the twin fork stands one tier on each. A bottom anchor hangs its single slot. */
|
|
53
|
-
const twin = slotPresetsOf([{ id: 'left', part: 'a', face: 'top' }, { id: 'right', part: 'b', face: 'top' }]
|
|
52
|
+
const twin = slotPresetsOf([{ id: 'left', part: 'a', face: 'top' }, { id: 'right', part: 'b', face: 'top' }])
|
|
54
53
|
assert.deepEqual(twin.find(p => p.key === 'craneTwinFork')!.build(size, {}).tiers.map(t => t.anchorId), ['left', 'right'])
|
|
55
|
-
assert.equal(slotPresetsOf([{ id: 'under', part: 'fork', face: 'bottom' }]
|
|
54
|
+
assert.equal(slotPresetsOf([{ id: 'under', part: 'fork', face: 'bottom' }])[0]!.build(size, {}).tiers[0]!.mount, 'HANG')
|
|
56
55
|
})
|
|
57
56
|
|
|
58
57
|
test('the editor is told what figure-model asks beyond its own checks: every slot names a type', () => {
|
|
@@ -103,7 +102,7 @@ test('refusals are said from their details, never the kernel\'s sentence', () =>
|
|
|
103
102
|
})
|
|
104
103
|
|
|
105
104
|
test('the slot layout panel starts from presets that keep where they started as a record', () => {
|
|
106
|
-
const [single] = slotPresetsOf([{ id: 'fork', part: 'fork', face: 'top' }]
|
|
105
|
+
const [single] = slotPresetsOf([{ id: 'fork', part: 'fork', face: 'top' }])
|
|
107
106
|
assert.deepEqual(single!.build({ width: 1200, depth: 900, slotHeight: 1000 }, {}).preset, { key: 'single', size: { width: 1200, depth: 900, slotHeight: 1000 }, params: {} })
|
|
108
107
|
})
|
|
109
108
|
|
|
@@ -115,3 +114,9 @@ test('the places words are in every language', () => {
|
|
|
115
114
|
assert.ok(t[key], `${lang} ${key}`)
|
|
116
115
|
}
|
|
117
116
|
})
|
|
117
|
+
|
|
118
|
+
/* A preset's name is the shared editor's (label.slot-layout.preset.*): figure-ui keeps no second copy of it. */
|
|
119
|
+
test('the preset names are the slot layout editor\'s, not a copy here', () => {
|
|
120
|
+
for (const lang of ['en', 'es', 'ja', 'ko', 'ms', 'zh'])
|
|
121
|
+
assert.deepEqual(Object.keys(JSON.parse(read(`../translations/${lang}.json`))).filter(key => key.includes('slot-preset')), [], lang)
|
|
122
|
+
})
|