@things-factory/figure-ui 10.1.200 → 10.1.202
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/editor.ts +7 -3
- package/client/modeller/fasten-form.ts +18 -35
- package/client/modeller/fasten-geometry.ts +67 -115
- package/client/modeller/figure-fasten-dialog.ts +4 -12
- package/client/modeller/figure-links.ts +6 -7
- package/client/modeller/joint-drag.ts +5 -5
- package/client/modeller/links.ts +35 -99
- package/client/modeller/refusals.ts +2 -6
- package/dist-client/modeller/editor.js +7 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +5 -8
- package/dist-client/modeller/fasten-form.js +16 -28
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +20 -19
- package/dist-client/modeller/fasten-geometry.js +54 -113
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +4 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +6 -7
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/joint-drag.js +5 -5
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/links.d.ts +19 -26
- package/dist-client/modeller/links.js +36 -94
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -1
- package/dist-client/modeller/refusals.js +2 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +1 -1
- package/test/browser/acceptance-page.js +11 -11
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/fix-alignment.check.mjs +3 -3
- package/test/browser/links.check.mjs +20 -8
- package/test/browser/modeller-page.js +3 -3
- package/test/browser/part-ids.check.mjs +2 -2
- package/test/browser/parts-page.js +2 -2
- package/test/browser/product-page.check.mjs +3 -3
- package/test/browser/product-page.js +1 -1
- package/test/browser/row-preview.check.mjs +2 -2
- package/test/browser/screen-page.js +3 -3
- package/test/browser/section-page.js +3 -3
- package/test/browser/section.check.mjs +4 -4
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +1 -1
- package/test/command-preview.test.ts +8 -8
- package/test/face-mapping.test.ts +2 -2
- package/test/fasten-drawing.test.ts +14 -2
- package/test/fasten-form.test.ts +11 -3
- package/test/fasten-geometry.test.ts +22 -31
- package/test/fix-alignment.test.ts +16 -4
- package/test/gizmo.test.ts +2 -2
- package/test/joint-drag.test.ts +13 -2
- package/test/joint-fan.test.ts +1 -1
- package/test/joint-frames.test.ts +45 -23
- package/test/joints.ts +1 -1
- package/test/links.test.ts +68 -68
- package/test/live.test.ts +2 -2
- package/test/preview-size.test.ts +1 -1
- package/test/refusals.test.ts +18 -20
- package/test/resize-with-placement.test.ts +6 -6
- package/test/revision.test.ts +4 -5
- package/test/section-panel.test.ts +1 -1
- package/translations/en.json +9 -11
- package/translations/es.json +9 -11
- package/translations/ja.json +9 -11
- package/translations/ko.json +9 -11
- package/translations/ms.json +9 -11
- package/translations/zh.json +9 -11
package/test/links.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를
|
|
|
41
41
|
test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', () => {
|
|
42
42
|
const asset = pair()
|
|
43
43
|
assert.equal(fastenedTo(asset, 'block'), null)
|
|
44
|
-
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block',
|
|
44
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block', to: { part: 'plate', face: 'top' } })
|
|
45
45
|
assert.equal(fastenedTo(fastened, 'block'), 'plate')
|
|
46
46
|
assert.equal(fastenedTo(applyFigureAuthoring(fastened, { kind: 'detach', part: 'block' }), 'block'), null)
|
|
47
47
|
const withDrum = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
@@ -69,7 +69,7 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
|
|
|
69
69
|
|
|
70
70
|
import { readFileSync } from 'node:fs'
|
|
71
71
|
import { fileURLToPath } from 'node:url'
|
|
72
|
-
import { alignRowsOf, choiceOf, followCommandOf
|
|
72
|
+
import { alignRowsOf, choiceOf, followCommandOf } from '../client/modeller/links.ts'
|
|
73
73
|
import { faceViewOf } from '../client/modeller/fasten-geometry.ts'
|
|
74
74
|
import { moving } from './joints.ts'
|
|
75
75
|
|
|
@@ -244,8 +244,8 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
|
|
|
244
244
|
let asset = createFigureAsset('Probe')
|
|
245
245
|
asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
246
246
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
247
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base',
|
|
248
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp',
|
|
247
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base', to: { plane: 'mounting-plane' } })
|
|
248
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: { part: 'base', face: 'top' }, align: { x: { mm: 120 } } })
|
|
249
249
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 40, max: 4000 }, y: { min: 16, max: 1600 }, z: { min: 40, max: 4000 } } })
|
|
250
250
|
assert.equal(proportionToFollow(asset), undefined, '한 비율이 아니면 권하지 않는다')
|
|
251
251
|
asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' })
|
|
@@ -271,20 +271,20 @@ test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만,
|
|
|
271
271
|
two = addFigureBox(two, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
272
272
|
two = addFigureBox(two, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
273
273
|
const right = { part: 'jamb', face: 'right' }
|
|
274
|
-
assert.deepEqual(alignAxesOf(two,
|
|
275
|
-
assert.deepEqual(alignAxesOf(two, '
|
|
274
|
+
assert.deepEqual(alignAxesOf(two, right), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
275
|
+
assert.deepEqual(alignAxesOf(two, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }])
|
|
276
276
|
/* A door on the jamb's right face: bottoms level, fronts level. The x choice is along the face and is not sent. */
|
|
277
|
-
assert.deepEqual(alignOf(two,
|
|
278
|
-
assert.deepEqual(alignOf(two,
|
|
279
|
-
assert.equal(alignOf(two, '
|
|
280
|
-
assert.equal(alignOf(two,
|
|
277
|
+
assert.deepEqual(alignOf(two, right, { x: 'max', y: 'min', z: 'max' }, {}), { y: 'min', z: 'max' })
|
|
278
|
+
assert.deepEqual(alignOf(two, { part: 'jamb', face: 'top' }, { x: 'mm', z: 'centre' }, { x: 120 }), { x: { mm: 120 } })
|
|
279
|
+
assert.equal(alignOf(two, 'plane', { x: 'min' }, {}), undefined, '바닥에는 맞출 모서리가 없다')
|
|
280
|
+
assert.equal(alignOf(two, { part: 'jamb', face: 'top' }, {}, {}), undefined)
|
|
281
281
|
|
|
282
282
|
/* What the form sends is what hangs the part on the edge: the door's pivot is on the jamb's front edge line. */
|
|
283
283
|
let asset = createFigureAsset('Door')
|
|
284
284
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
285
285
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
286
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
287
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
286
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
287
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: alignOf(asset, { part: 'jamb', face: 'right' }, { y: 'min', z: 'max' }, {}) })
|
|
288
288
|
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
289
289
|
const leaf = compileFigureAsset(asset).evaluate().geometry.find(g => g.id === 'leaf')!
|
|
290
290
|
assert.deepEqual(leaf.pose.t.map(Math.round), [500, 1000, 55], '문짝 아래가 문틀 아래에, 앞이 문틀 앞에')
|
|
@@ -295,13 +295,13 @@ test('the fastening form asks across the face in rows only where the drawing can
|
|
|
295
295
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
296
296
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
297
297
|
const right = { part: 'jamb', face: 'right' }
|
|
298
|
-
const view = faceViewOf(revisionOf(asset), 'leaf', right)
|
|
298
|
+
const view = faceViewOf(revisionOf(asset), 'leaf', right, 'meet')
|
|
299
299
|
assert.deepEqual(view?.axes, ['y', 'z'])
|
|
300
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
301
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
302
|
-
assert.deepEqual(alignRowsOf(asset,
|
|
300
|
+
assert.deepEqual(alignRowsOf(asset, right, view), [], 'the drawing sets both')
|
|
301
|
+
assert.deepEqual(alignRowsOf(asset, right, undefined), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }], 'with no drawing, both are asked')
|
|
302
|
+
assert.deepEqual(alignRowsOf(asset, right, view, { y: 'leaf.position.y' }), [{ axis: 'y', edges: true }], 'the axis that follows a position field')
|
|
303
303
|
const sized = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 100, max: 5000 }, y: { min: 100, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 2100, z: 150 } })
|
|
304
|
-
assert.deepEqual(alignRowsOf(sized,
|
|
304
|
+
assert.deepEqual(alignRowsOf(sized, right, faceViewOf(revisionOf(sized), 'leaf', right, 'meet')).map(r => r.axis), ['y', 'z'], 'the axes a share of the figure size may set')
|
|
305
305
|
})
|
|
306
306
|
|
|
307
307
|
test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서 거리로 채운다 (문짝이 문틀 앞 100 mm 에서 가운데로 옮겨졌다, 09-26)', async () => {
|
|
@@ -313,9 +313,9 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
|
|
|
313
313
|
const where = (a: FigureAsset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
|
|
314
314
|
for (const target of ['plane', { part: 'frame', face: 'top' }] as const) {
|
|
315
315
|
const kept = placeKeptOf(revisionOf(asset), 'leaf', target)
|
|
316
|
-
const align = alignOf(asset,
|
|
316
|
+
const align = alignOf(asset, target, kept.align, kept.alignMm)
|
|
317
317
|
const to = target === 'plane' ? { plane: 'mounting-plane' as const } : target
|
|
318
|
-
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
318
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to, ...(align ? { align } : {}) } as never)
|
|
319
319
|
const [before, after] = [where(asset), where(fastened)]
|
|
320
320
|
assert.deepEqual([after[0], after[2]], [before[0], before[2]], `${JSON.stringify(target)}: 앞뒤·좌우는 그대로`)
|
|
321
321
|
}
|
|
@@ -328,8 +328,8 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
|
|
|
328
328
|
let row = addFigureBox(createFigureAsset('Row'), { id: 'plate', size: { x: 1000, y: 50, z: 400 }, position: { x: 0, y: 700, z: 0 } })
|
|
329
329
|
row = addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 400 }, position: { x: 0, y: 760, z: 0 } })
|
|
330
330
|
row = addFigureBox(row, { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 800, z: 0 } })
|
|
331
|
-
row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller',
|
|
332
|
-
row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap',
|
|
331
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller', to: { part: 'plate', face: 'top' } })
|
|
332
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
|
|
333
333
|
assert.deepEqual([hangsFrom(row, 'roller', 'plate'), hangsFrom(row, 'cap', 'plate'), hangsFrom(row, 'plate', 'roller')], [true, true, false])
|
|
334
334
|
})
|
|
335
335
|
|
|
@@ -340,10 +340,10 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
|
|
|
340
340
|
asset = addFigureBox(asset, { id: 'palm', size: { x: 240, y: 70, z: 160 }, position: { x: 0, y: 35, z: 0 } })
|
|
341
341
|
for (const id of ['left', 'right']) asset = addFigureBox(asset, { id, size: { x: 30, y: 140, z: 100 }, position: { x: 0, y: 200, z: 0 } })
|
|
342
342
|
asset = addFigureBox(asset, { id: 'wrist', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 400, z: 0 } })
|
|
343
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm',
|
|
344
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left',
|
|
345
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right',
|
|
346
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist',
|
|
343
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm', to: { plane: 'mounting-plane' } })
|
|
344
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left', to: { part: 'palm', face: 'top' }, align: { x: 'min' } })
|
|
345
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right', to: { part: 'palm', face: 'top' }, align: { x: 'max' } })
|
|
346
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist', to: { part: 'palm', face: 'bottom' } })
|
|
347
347
|
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'grip', min: 0, max: 60, follow: '', multiplier: 1, offset: 0 }
|
|
348
348
|
const jointOf = (part: string) => asset.joints!.find(j => j.body1.part === part)!
|
|
349
349
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('left'), form, 'left') })
|
|
@@ -385,8 +385,8 @@ test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구
|
|
|
385
385
|
assert.equal(canFasten(asset, 'ball'), false, '공은 붙일 면이 없다')
|
|
386
386
|
assert.equal(canFasten(asset, 'frame'), true)
|
|
387
387
|
assert.deepEqual(facesOf(asset, 'cone'), ['bottom', 'top'])
|
|
388
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
389
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
388
|
+
assert.deepEqual(alignAxesOf(asset, { part: 'frame', face: 'opening-left' }), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
389
|
+
assert.deepEqual(alignAxesOf(asset, { part: 'cone', face: 'top' }), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '옆면이 평면이 아니면 모서리도 없다')
|
|
390
390
|
})
|
|
391
391
|
|
|
392
392
|
test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않은 부품에 — 붙인 면을 가로지르는 축으로 (빈틈 B, 2026-09-25)', async () => {
|
|
@@ -395,24 +395,30 @@ test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않
|
|
|
395
395
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
396
396
|
asset = addFigureBox(asset, { id: 'slat', size: { x: 60, y: 20, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
397
397
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '붙지 않은 부품은 되풀이를 권하지 않는다')
|
|
398
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
399
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat',
|
|
398
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
399
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', to: { part: 'frame', face: 'top' } })
|
|
400
400
|
assert.deepEqual(rowAxesOf(asset, 'frame'), [], '바닥에 선 부품은 채울 길이가 없다')
|
|
401
401
|
assert.deepEqual(rowAxesOf(asset, 'slat'), ['x', 'z'], '윗면을 가로지르는 두 축')
|
|
402
|
+
assert.deepEqual(rowAxesOf(applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', to: { part: 'frame', face: 'front' }, replace: true }), 'slat'), ['x', 'y'], '앞면이면 앞면을 가로지르는 두 축')
|
|
402
403
|
asset = applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'slat', axis: 'x', pitch: 100 })
|
|
403
404
|
assert.deepEqual(rowOf(asset, 'slat'), { axis: 'X', pitch: 100 })
|
|
404
405
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '이미 되풀이면 다시 권하지 않는다')
|
|
405
406
|
})
|
|
406
407
|
|
|
407
|
-
test('
|
|
408
|
+
test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자세의 실제 끝으로 맞춘다 (ADR-0140 3.4)', async () => {
|
|
408
409
|
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
409
410
|
let asset = createFigureAsset('Fan')
|
|
410
411
|
asset = addFigureBox(asset, { id: 'hub', size: { x: 120, y: 120, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
411
|
-
asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 } })
|
|
412
|
+
asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 30 } })
|
|
412
413
|
const front = { part: 'hub', face: 'front' }
|
|
413
|
-
assert.deepEqual(alignAxesOf(asset,
|
|
414
|
-
assert.deepEqual(alignOf(asset,
|
|
415
|
-
|
|
414
|
+
assert.deepEqual(alignAxesOf(asset, front), [{ axis: 'x', edges: true }, { axis: 'y', edges: true }])
|
|
415
|
+
assert.deepEqual(alignOf(asset, front, { x: 'max', y: 'mm' }, { y: 50 }), { x: 'max', y: { mm: 50 } })
|
|
416
|
+
/* Fastened so, its real reach is level with the hub's right side, turned as it is. */
|
|
417
|
+
const { compileFigureAsset, figureFaceOverhangOf } = await import('@hatiolab/figure-model')
|
|
418
|
+
const on = applyFigureAuthoring(asset, { kind: 'attach', part: 'blade', to: front, align: { x: 'max' } })
|
|
419
|
+
assert.equal(figureFaceOverhangOf(on, 'blade')!.state, 'larger', 'wider than the hub: past its left edge, level with its right')
|
|
420
|
+
assert.deepEqual(figureFaceOverhangOf(on, 'blade')!.sides.filter(s => s.axis === 'x').map(s => s.side), ['low'])
|
|
421
|
+
void compileFigureAsset
|
|
416
422
|
})
|
|
417
423
|
|
|
418
424
|
test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
|
|
@@ -421,7 +427,7 @@ test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선
|
|
|
421
427
|
let asset = createFigureAsset('Conveyor')
|
|
422
428
|
assert.equal(instanceRangeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
|
|
423
429
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
424
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
430
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
425
431
|
assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
|
|
426
432
|
const toDeclare = instanceRangeToDeclare(asset)!
|
|
427
433
|
assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
|
|
@@ -456,8 +462,8 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
|
|
|
456
462
|
let asset = createFigureAsset('Gantry')
|
|
457
463
|
asset = addFigureBox(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
|
|
458
464
|
asset = addFigureBox(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
|
|
459
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail',
|
|
460
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge',
|
|
465
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail', to: { plane: 'mounting-plane' } })
|
|
466
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
|
|
461
467
|
const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
|
|
462
468
|
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'x', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: 'rail:x', travelPlus: -160 }
|
|
463
469
|
const record = motionJoint(joint, form, 'bridge')
|
|
@@ -480,14 +486,14 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
|
|
|
480
486
|
let asset = createFigureAsset('Panel')
|
|
481
487
|
asset = addFigureBox(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
482
488
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
|
|
483
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel',
|
|
489
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', to: { plane: 'mounting-plane' } })
|
|
484
490
|
const front = { part: 'panel', face: 'front' }
|
|
485
|
-
assert.deepEqual(alignOf(asset,
|
|
491
|
+
assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
|
|
486
492
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
|
|
487
493
|
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
488
|
-
const align = alignOf(asset,
|
|
494
|
+
const align = alignOf(asset, front, { x: 'share' }, { x: 300 })!
|
|
489
495
|
assert.deepEqual(align, { x: { mm: { from: { of: 'instance', axis: 'x' }, times: 0.3 } } })
|
|
490
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp',
|
|
496
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: front, align })
|
|
491
497
|
const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
|
|
492
498
|
assert.equal(x(1000), 300)
|
|
493
499
|
assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
|
|
@@ -498,8 +504,8 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
498
504
|
let asset = createFigureAsset('Door')
|
|
499
505
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
500
506
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
501
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
502
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf',
|
|
507
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
508
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
503
509
|
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
504
510
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
505
511
|
assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
@@ -508,15 +514,15 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
|
|
|
508
514
|
test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
|
|
509
515
|
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
510
516
|
let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
511
|
-
assert.deepEqual(alignAxesOf(asset, '
|
|
517
|
+
assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
|
|
512
518
|
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2000, z: 150 } })
|
|
513
|
-
assert.deepEqual(alignAxesOf(asset, '
|
|
514
|
-
assert.deepEqual(alignOf(asset, '
|
|
519
|
+
assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: true }, { axis: 'z', edges: true }])
|
|
520
|
+
assert.deepEqual(alignOf(asset, 'plane', { x: 'min' }, {}), { x: 'min' })
|
|
515
521
|
})
|
|
516
522
|
|
|
517
523
|
test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우고, 그대로 보내면 같은 관절이다 (2026-09-25)', async () => {
|
|
518
524
|
const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
|
|
519
|
-
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b'
|
|
525
|
+
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b' } }
|
|
520
526
|
const joints = [
|
|
521
527
|
{ ...base, type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } },
|
|
522
528
|
{ ...base, type: 'revolute' as const, axis: 'Y' as const, lowerLimit: -90, upperLimit: 0, state: { id: 'open' } },
|
|
@@ -540,25 +546,21 @@ test('바닥에 선 부품도 무엇에 붙어 있는지 읽는다 — 그래야
|
|
|
540
546
|
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
541
547
|
asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
542
548
|
assert.equal(heldBy(asset, 'jamb'), null)
|
|
543
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
544
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head',
|
|
549
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' } })
|
|
550
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head', to: { part: 'jamb', face: 'top' } })
|
|
545
551
|
assert.deepEqual(heldBy(asset, 'jamb'), { plane: true }, '장착 평면')
|
|
546
552
|
assert.deepEqual(heldBy(asset, 'head'), { part: 'jamb' })
|
|
547
553
|
/* What the form then sends: a fastening that replaces the one there. */
|
|
548
|
-
assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb',
|
|
554
|
+
assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', to: { plane: 'mounting-plane' }, align: { x: { mm: -500 } }, replace: !!heldBy(asset, 'jamb') }))
|
|
549
555
|
})
|
|
550
556
|
|
|
551
|
-
test('벽 배치 — 장착 평면은 Z = 0,
|
|
557
|
+
test('벽 배치 — 장착 평면은 Z = 0, 벽에서 나와 서고 가로질러 X · Y 로 맞춘다(사용자 · 아키텍트 09-25, ADR-0140 3.11)', async () => {
|
|
552
558
|
const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/links.ts')
|
|
553
559
|
let a = addFigureBox(createFigureAsset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
554
|
-
assert.deepEqual(mountingPlaneOf(a), { normal: 'y',
|
|
560
|
+
assert.deepEqual(mountingPlaneOf(a), { normal: 'y', facings: ['meet', 'flush'] }, '배치 선언 전에는 바닥 — 올려 서거나 아래에 매달림')
|
|
555
561
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
|
|
556
|
-
assert.deepEqual(mountingPlaneOf(a), { normal: 'z',
|
|
557
|
-
assert.deepEqual(alignAxesOf(a, '
|
|
558
|
-
assert.deepEqual(ownFacesOf(a, 'plate', true), ['back'], 'on the plane, only the face the plane takes')
|
|
559
|
-
assert.deepEqual(ownFacesOf(a, 'plate', false), ['left', 'right', 'bottom', 'top', 'back', 'front', 'centre'], 'to a part, its six outer faces and its centre')
|
|
560
|
-
const floor = addFigureBox(createFigureAsset('Floor'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
561
|
-
assert.deepEqual(ownFacesOf(floor, 'plate', true), ['bottom', 'top'], 'on the floor, its bottom or its top')
|
|
562
|
+
assert.deepEqual(mountingPlaneOf(a), { normal: 'z', facings: ['meet'] })
|
|
563
|
+
assert.deepEqual(alignAxesOf(a, 'plane').map(x => x.axis), ['x', 'y'])
|
|
562
564
|
})
|
|
563
565
|
|
|
564
566
|
test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는 「위치와 크기」, 붙이기 칸은 「붙이기」 (09-26)', () => {
|
|
@@ -578,8 +580,8 @@ test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸
|
|
|
578
580
|
asset = addFigureBox(asset, { id: 'frame', size: { x: 1200, y: 60, z: 200 }, position: { x: 0, y: 30, z: 0 } })
|
|
579
581
|
asset = addFigureBox(asset, { id: 'door', size: { x: 1000, y: 2000, z: 40 }, position: { x: 0, y: 1060, z: 0 } })
|
|
580
582
|
asset = addFigureBox(asset, { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 2200, z: 0 } })
|
|
581
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame',
|
|
582
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door',
|
|
583
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
584
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door', to: { part: 'frame', face: 'top' } })
|
|
583
585
|
assert.deepEqual(visibilityControlsOf(asset), [], '조작값이 없으면 고를 것도 없다')
|
|
584
586
|
asset = moving(asset, 'door', { kind: 'slide', axis: 'x', id: 'gate-open', min: 0, max: 900 })
|
|
585
587
|
assert.deepEqual(visibilityControlsOf(asset).map(c => [c.id, c.min, c.max, c.unit]), [['gate-open', 0, 900, 'mm']])
|
|
@@ -629,20 +631,18 @@ import { wordsOf } from './words.ts'
|
|
|
629
631
|
|
|
630
632
|
test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은 롤러는 가운데 · 상대 · 간격 그대로', () => {
|
|
631
633
|
let asset = addFigureBox(createFigureAsset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
|
|
632
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed',
|
|
634
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', to: { plane: 'mounting-plane' } })
|
|
633
635
|
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 40, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
634
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller',
|
|
636
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', facing: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
|
|
635
637
|
const roller = fasteningOf(revisionOf(asset), 'roller')!
|
|
636
|
-
assert.equal(roller.
|
|
638
|
+
assert.equal(roller.facing, 'centre')
|
|
637
639
|
assert.equal(roller.target, 'bed')
|
|
638
640
|
assert.equal(roller.targetFace, 'top')
|
|
639
641
|
assert.equal(roller.gap, 40)
|
|
640
642
|
assert.equal(roller.align.x, 'min', '기록의 정렬')
|
|
641
643
|
const bed = fasteningOf(revisionOf(asset), 'bed')!
|
|
642
|
-
assert.deepEqual([bed.
|
|
644
|
+
assert.deepEqual([bed.facing, bed.toPlane], ['meet', true])
|
|
643
645
|
assert.equal(fasteningOf(revisionOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), 'b'), null, '안 붙은 부품은 기본값으로')
|
|
644
|
-
assert.equal(ownFacesOf(asset, 'roller', false).at(-1), 'centre', 'its own faces include its centre')
|
|
645
|
-
assert.ok(!ownFacesOf(asset, 'roller', true).includes('centre'), 'on the floor, by a face only')
|
|
646
646
|
})
|
|
647
647
|
|
|
648
648
|
test('a worked-out gap is kept by the fastening form: opening it on the part moves nothing (the robot dog, the forklift, 2026-10-13)', async () => {
|
|
@@ -652,7 +652,7 @@ test('a worked-out gap is kept by the fastening form: opening it on the part mov
|
|
|
652
652
|
Object.fromEntries((compileFigureAsset(asset).evaluate().geometry as unknown as Array<{ id: string; pose?: { t: number[] } }>).map(g => [g.id, g.pose?.t.map(v => Math.round(v * 1000) / 1000)]))
|
|
653
653
|
/* What the form sends when nothing in it is changed: the attach it would make of the part's own fastening. */
|
|
654
654
|
const reattach = (asset: FigureAsset, part: string, form: Parameters<typeof fastenGapOf>[0]) =>
|
|
655
|
-
applyFigureAuthoring(asset, { kind: 'attach', part,
|
|
655
|
+
applyFigureAuthoring(asset, { kind: 'attach', part, to: { plane: 'mounting-plane' }, ...fastenGapOf(form), replace: true } as never)
|
|
656
656
|
|
|
657
657
|
const dog = robotDogFigure().asset
|
|
658
658
|
const body = fasteningOf(revisionOf(dog), 'body')!
|
|
@@ -665,7 +665,7 @@ test('a worked-out gap is kept by the fastening form: opening it on the part mov
|
|
|
665
665
|
let lift = addFigureBox(createFigureAsset('Lift'), { id: 'body', size: { x: 1000, y: 800, z: 600 }, position: { x: 0, y: 500, z: 0 } })
|
|
666
666
|
lift.document.model.inputs.push({ id: 'size.y', unit: 'mm', min: 100, max: 20000, role: 'design' } as never)
|
|
667
667
|
lift.designInputs['size.y'] = 1800
|
|
668
|
-
lift = applyFigureAuthoring(lift, { kind: 'attach', part: 'body',
|
|
668
|
+
lift = applyFigureAuthoring(lift, { kind: 'attach', part: 'body', to: { plane: 'mounting-plane' }, gap: { from: { of: 'instance', axis: 'y' }, times: 0.05 } } as never)
|
|
669
669
|
const kept = fasteningOf(revisionOf(lift), 'body')!
|
|
670
670
|
assert.deepEqual(kept.gapKept, { from: { of: 'instance', axis: 'y' }, times: 0.05 })
|
|
671
671
|
assert.deepEqual(posesOf(reattach(lift, 'body', kept)), posesOf(lift))
|
package/test/live.test.ts
CHANGED
|
@@ -70,9 +70,9 @@ test('끄는 동안은 세션이 지금 든 자세 위에 그린다 — 다른
|
|
|
70
70
|
let asset = slider()
|
|
71
71
|
/* An arm on a post of its own, turning about the post's left edge -- so a swing moves it, and the slide does not. */
|
|
72
72
|
asset = addFigureBox(asset, { id: 'post', size: { x: 100, y: 100, z: 100 }, position: { x: 400, y: 50, z: 0 } })
|
|
73
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post',
|
|
73
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'post', to: { plane: 'mounting-plane' }, align: { x: { mm: 400 } } })
|
|
74
74
|
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 20, z: 20 }, position: { x: 400, y: 110, z: 0 } })
|
|
75
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm',
|
|
75
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', to: { part: 'post', face: 'top' }, align: { x: 'min' } })
|
|
76
76
|
asset = moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'swing', min: -180, max: 180 })
|
|
77
77
|
const armAt = (parts: { id: string; pose: { t: number[] } }[]) => parts.find(p => p.id === 'arm')!.pose.t.map(v => Math.round(v))
|
|
78
78
|
const committed = armAt(compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, swing: 90 } }).evaluate().geometry as never)
|
|
@@ -13,7 +13,7 @@ import { previewSizeWordsOf, rowCountsAt } from '../client/modeller/preview-size
|
|
|
13
13
|
function conveyor(): FigureAsset {
|
|
14
14
|
let asset = addFigureBox(createFigureAsset('Conveyor'), { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 50, z: 0 } })
|
|
15
15
|
asset = addFigurePrimitive(asset, { id: 'roller', kind: 'cylinder', dimensions: { radius: 30, length: 560 }, position: { x: 0, y: 0, z: 0 }, rotation: { x: 90, y: 0, z: 0 } })
|
|
16
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller',
|
|
16
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
|
|
17
17
|
asset = applyFigureAuthoring(asset, { kind: 'follow-size', part: 'frame', axes: ['x'] })
|
|
18
18
|
return applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
19
19
|
}
|
package/test/refusals.test.ts
CHANGED
|
@@ -61,8 +61,8 @@ function conveyor(): FigureAsset {
|
|
|
61
61
|
let a = fm.createFigureAsset('Conveyor')
|
|
62
62
|
a = fm.addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
63
63
|
a = fm.addFigureBox(a, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
64
|
-
a = act(a, { kind: 'attach', part: 'frame',
|
|
65
|
-
a = act(a, { kind: 'attach', part: 'roller',
|
|
64
|
+
a = act(a, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
65
|
+
a = act(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
|
|
66
66
|
return act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -100,10 +100,10 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
100
100
|
a = fm.addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
101
101
|
a = fm.addFigureBox(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
102
102
|
a = fm.addFigureBox(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
103
|
-
a = act(a, { kind: 'attach', part: 'leg',
|
|
104
|
-
a = act(a, { kind: 'attach', part: 'top',
|
|
105
|
-
a = act(a, { kind: 'attach', part: 'lamp',
|
|
106
|
-
a = act(a, { kind: 'attach', part: 'shade',
|
|
103
|
+
a = act(a, { kind: 'attach', part: 'leg', to: { plane: 'mounting-plane' } })
|
|
104
|
+
a = act(a, { kind: 'attach', part: 'top', to: { part: 'leg', face: 'top' } })
|
|
105
|
+
a = act(a, { kind: 'attach', part: 'lamp', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
|
|
106
|
+
a = act(a, { kind: 'attach', part: 'shade', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
|
|
107
107
|
const turn = (asset: typeof a, part: string, extra: object) => act(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } })
|
|
108
108
|
a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
|
|
109
109
|
a = turn(a, 'shade', { mimic: { joint: 'lamp', multiplier: 1 } })
|
|
@@ -121,15 +121,15 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
121
121
|
assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
|
|
122
122
|
assert.match(why(conveyor(), 'frame')!, /part-row-along .*"part":"frame".*"rows":"roller"/, '줄이 따라 놓인 부품')
|
|
123
123
|
let hung = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
124
|
-
hung = act(hung, { kind: 'attach', part: 'cap',
|
|
124
|
+
hung = act(hung, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
|
|
125
125
|
assert.match(why(hung, 'roller')!, /part-row-carries .*"part":"roller".*"others":"cap"/, '줄에 매달린 것')
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
test('공간 선언에 든 부품 — 한 번 눌러 지우고, 공간은 남은 부품에 다시 맞추며 그렇다고 한 줄 말한다(묻지 않는다, 2026-09-27)', () => {
|
|
129
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
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',
|
|
132
|
-
a = act(a, { kind: 'attach', part: 'stray',
|
|
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
133
|
a = act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
|
|
134
134
|
assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(a, { kind: 'remove-part', target: 'stray' }))), true)
|
|
135
135
|
assert.equal(inDeclaredRoom(refusalOf(() => fm.editFigurePart(conveyor(), { kind: 'remove-part', target: 'frame' }))), false, '줄은 이 길이 아니다')
|
|
@@ -144,12 +144,11 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
|
|
|
144
144
|
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
145
|
kind: 'attach',
|
|
146
146
|
part: 'post',
|
|
147
|
-
face: 'bottom',
|
|
148
147
|
to: { plane: 'mounting-plane' }
|
|
149
148
|
})
|
|
150
149
|
const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
|
|
151
150
|
const said = placementRefusalOf(refused, say)!
|
|
152
|
-
assert.
|
|
151
|
+
assert.equal(said, '장착 평면에 붙은 부품이 있어 놓이는 곳을 바꿀 수 없습니다: post. 먼저 이 부품을 떼어 주십시오.')
|
|
153
152
|
assert.equal(editRefusalOf(refused, say), said, '편집 거절의 말도 같다')
|
|
154
153
|
assert.equal(placementRefusalOf(refusalOf(() => act(box(), declaring({ min: 0, max: 4000 }))), say), null)
|
|
155
154
|
/* The placement list sends its command through the page: said there in words, and the list back on the placement it has (product-page.check.mjs). */
|
|
@@ -191,13 +190,13 @@ test('줄이 한도를 넘으면 몇 개가 되는지와 한도를 말한다 —
|
|
|
191
190
|
let a = fm.createFigureAsset('Conveyor')
|
|
192
191
|
a = fm.addFigureBox(a, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
193
192
|
a = fm.addFigureBox(a, { id: 'roller', size: { x: 10, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
194
|
-
a = act(a, { kind: 'attach', part: 'frame',
|
|
195
|
-
a = act(a, { kind: 'attach', part: 'roller',
|
|
193
|
+
a = act(a, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
|
|
194
|
+
a = act(a, { kind: 'attach', part: 'roller', to: { part: 'frame', face: 'top' } })
|
|
196
195
|
const many = editRefusalOf(refusalOf(() => act(a, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 12, count: 150 })), say)
|
|
197
196
|
assert.ok(many.includes('150') && many.includes('1~100') && !many.includes('GEOMETRY_DOMAIN'), many)
|
|
198
197
|
/* A saved figure whose row would expand past what a figure draws: ten thousand copies, each a roller and its cap. */
|
|
199
198
|
let capped = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
200
|
-
capped = structuredClone(act(capped, { kind: 'attach', part: 'cap',
|
|
199
|
+
capped = structuredClone(act(capped, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } }))
|
|
201
200
|
const m = capped.document.model as unknown as { constants: Array<{ id: string; value: number }> }
|
|
202
201
|
m.constants.find(c => c.id === 'limit.count.100')!.value = 10000
|
|
203
202
|
m.constants.find(c => c.id === 'd.mm.200')!.value = 0.2
|
|
@@ -209,15 +208,15 @@ test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 ·
|
|
|
209
208
|
const shelf = () => {
|
|
210
209
|
let a = fm.addFigureBox(fm.createFigureAsset('Shelf'), { id: 'board', size: { x: 400, y: 50, z: 1000 }, position: { x: 0, y: 0, z: 0 } })
|
|
211
210
|
a = fm.addFigureBox(a, { id: 'bin', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
212
|
-
a = act(a, { kind: 'attach', part: 'board',
|
|
213
|
-
return act(a, { kind: 'attach', part: 'bin',
|
|
211
|
+
a = act(a, { kind: 'attach', part: 'board', to: { plane: 'mounting-plane' } })
|
|
212
|
+
return act(a, { kind: 'attach', part: 'bin', to: { part: 'board', face: 'top' } })
|
|
214
213
|
}
|
|
215
214
|
/* Five 100 mm bins asked for on 400 mm. */
|
|
216
215
|
const fit = editRefusalOf(refusalOf(() => act(shelf(), { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 1, count: 5 })), keyed)
|
|
217
216
|
assert.match(fit, /row-does-not-fit .*"count":"5".*"length":"400".*"extent":"100"/)
|
|
218
217
|
/* A cap on the roller made a row along X, as the roller's row is. */
|
|
219
218
|
let capped = fm.addFigureBox(conveyor(), { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
220
|
-
capped = act(capped, { kind: 'attach', part: 'cap',
|
|
219
|
+
capped = act(capped, { kind: 'attach', part: 'cap', to: { part: 'roller', face: 'top' } })
|
|
221
220
|
assert.match(editRefusalOf(refusalOf(() => act(capped, { kind: 'make-repeat', part: 'cap', axis: 'x', pitch: 10 })), keyed), /row-same-axis .*"part":"cap","outer":"roller","axis":"X"/)
|
|
222
221
|
/* The bin's top holds the slot layout: made a row, the layout would stand on no one bin. */
|
|
223
222
|
const slotted = act(shelf(), {
|
|
@@ -243,7 +242,6 @@ test('옮겨 적은 거절 — 기록은 커널의 거절과 같은 모양이라
|
|
|
243
242
|
const a = act(fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } }), {
|
|
244
243
|
kind: 'attach',
|
|
245
244
|
part: 'post',
|
|
246
|
-
face: 'bottom',
|
|
247
245
|
to: { plane: 'mounting-plane' }
|
|
248
246
|
})
|
|
249
247
|
const refused = refusalOf(() => act(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' }))
|
|
@@ -271,7 +269,7 @@ test('단면 · 속 공간 — 모서리, 벽, 막힌 끝, 붙은 면을 거절
|
|
|
271
269
|
assert.match(editRefusalOf(refusalOf(() => act(box(), { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: { closed: 200 }, end: { closed: 200 } } })), keyed), /^figure\.text\.section-ends-too-thick/)
|
|
272
270
|
let tray = act(box(), { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
|
|
273
271
|
tray = fm.addFigureBox(tray, { id: 'divider', size: { x: 10, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
274
|
-
tray = act(tray, { kind: 'attach', part: 'divider',
|
|
272
|
+
tray = act(tray, { kind: 'attach', part: 'divider', to: { part: 'box', face: 'inside-bottom' } })
|
|
275
273
|
const faces = editRefusalOf(refusalOf(() => act(tray, { kind: 'set-cavity', part: 'box', cavity: { wall: 10, start: 'open', end: 'open' } })), say)
|
|
276
274
|
assert.ok(faces.includes(ko['figure.label.face-inside-bottom']), faces)
|
|
277
275
|
/* A section finding the screen has no words for is said with its code. */
|
|
@@ -309,7 +307,7 @@ test('도형의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로
|
|
|
309
307
|
})
|
|
310
308
|
|
|
311
309
|
test('레시피가 막히면 몇 번째 단계에서 왜 막혔는지 사람의 말로 — 단계 안의 거절도 값을 잃지 않는다 (2026-09-27)', () => {
|
|
312
|
-
const refused = refusalOf(() => fm.applyFigureProposal(box(), [{ kind: 'attach', part: 'nowhere',
|
|
310
|
+
const refused = refusalOf(() => fm.applyFigureProposal(box(), [{ kind: 'attach', part: 'nowhere', to: { plane: 'mounting-plane' } }, declaring({ min: 500, max: 4000 })] as never))
|
|
313
311
|
assert.match(recipeRefusalOf(refused, keyed)!, /^figure\.text\.recipe-step-refused .*"step":"1"/, '첫째 단계, 코드와 함께')
|
|
314
312
|
const sized = refusalOf(() => fm.applyFigureProposal(box(), [declaring({ min: 500, max: 4000 })] as never))
|
|
315
313
|
const told = recipeRefusalOf(sized, say)!
|
|
@@ -152,14 +152,14 @@ test('a fastened part keeps its position and turn rows, with the numbers the rul
|
|
|
152
152
|
|
|
153
153
|
test('size response and attachment are separate screen paths', () => {
|
|
154
154
|
const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
|
|
155
|
-
assert.equal(ko['figure.text.held-on-plane'].replace('{
|
|
155
|
+
assert.equal(ko['figure.text.held-on-plane'].replace('{place}', '바닥').replace('{gap}', '0'), '바닥에 붙어 있음 (간격 0 mm)')
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
-
test('a part fastened
|
|
158
|
+
test('a part fastened tilted off a quarter turn has no side figure-model can keep for a drag, so the size gizmo is withheld', async () => {
|
|
159
159
|
const { figureScaleAnchorOf } = await import('@hatiolab/figure-model')
|
|
160
160
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'hub', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
161
161
|
a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
|
|
162
|
-
a = applyFigureAuthoring(a, { kind: '
|
|
162
|
+
a = applyFigureAuthoring(a, { kind: 'set-joint', joint: { id: 'blade', type: 'fixed', body0: { part: 'hub', face: 'top' }, body1: { part: 'blade' }, origin: { childRotate: { z: 30 } } } })
|
|
163
163
|
assert.equal(figureScaleAnchorOf(a, 'blade'), undefined)
|
|
164
164
|
})
|
|
165
165
|
|
|
@@ -174,7 +174,7 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
|
|
|
174
174
|
const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box', gap: 'keep' }).steps)
|
|
175
175
|
const form = fasteningOf(revisionOf(kept), 'box')!
|
|
176
176
|
assert.equal(form.toPlane, true, 'on the plane')
|
|
177
|
-
assert.equal(form.
|
|
177
|
+
assert.equal(form.facing, 'meet')
|
|
178
178
|
assert.equal(form.gap, 100, 'the gap it keeps')
|
|
179
179
|
})
|
|
180
180
|
|
|
@@ -189,8 +189,8 @@ test('the fastening form keeps a place that follows a position field, and fasten
|
|
|
189
189
|
const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
|
|
190
190
|
const form = fasteningOf(revisionOf(kept), 'box')!
|
|
191
191
|
assert.deepEqual([form.align.x, form.alignInput.x, form.align.z, form.alignInput.z], ['input', 'box.position.x', 'input', 'box.position.z'])
|
|
192
|
-
const align = alignOf(kept, '
|
|
193
|
-
const again = applyFigureAuthoring(kept, { kind: 'attach', part: 'box',
|
|
192
|
+
const align = alignOf(kept, 'plane', form.align, form.alignMm, form.alignInput)
|
|
193
|
+
const again = applyFigureAuthoring(kept, { kind: 'attach', part: 'box', to: { plane: 'mounting-plane' }, ...(align ? { align } : {}), replace: true } as never)
|
|
194
194
|
assert.ok('box.position.x' in again.designInputs && 'box.position.z' in again.designInputs, 'the fields stay')
|
|
195
195
|
assert.deepEqual(JSON.stringify(again.joints), JSON.stringify(kept.joints), 'the same fastening')
|
|
196
196
|
})
|
package/test/revision.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { derivedHeld, derivedOf, type FigureRevision } from '../client/modeller/
|
|
|
20
20
|
import { alignChanges, boxOf, canMirror, gridStepOf } from '../client/modeller/align.ts'
|
|
21
21
|
import { jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
22
22
|
import { fasteningOf, measureNow, partSummaryOf, placeKeptOf } from '../client/modeller/links.ts'
|
|
23
|
-
import { faceViewOf
|
|
23
|
+
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'
|
|
@@ -96,8 +96,7 @@ function selectionQuestions(shown: FigureRevision, part: string) {
|
|
|
96
96
|
figureRowOf(asset, part, shown.inputs)
|
|
97
97
|
fasteningOf(shown, part)
|
|
98
98
|
placeKeptOf(shown, part, 'plane')
|
|
99
|
-
|
|
100
|
-
faceViewOf(shown, part, 'plane')
|
|
99
|
+
faceViewOf(shown, part, 'plane', 'meet')
|
|
101
100
|
figureFigureFacts(shown)
|
|
102
101
|
}
|
|
103
102
|
|
|
@@ -144,7 +143,7 @@ test('a value changed, read again as the next version, is two compiles at most:
|
|
|
144
143
|
function slide(): FigureAsset {
|
|
145
144
|
let a = addFigureBox(createFigureAsset('Slide'), { id: 'rail', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
146
145
|
a = addFigureBox(a, { id: 'carriage', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 150, z: 0 } })
|
|
147
|
-
a = applyFigureAuthoring(a, { kind: 'attach', part: 'carriage',
|
|
146
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'carriage', to: { part: 'rail', face: 'top' } })
|
|
148
147
|
a = moving(a, 'carriage', { kind: 'slide', axis: 'x', id: 'travel', min: 0, max: 400 })
|
|
149
148
|
return addFigureBox(a, { id: 'tag', size: { x: 50, y: 50, z: 50 }, position: { x: 300, y: 225, z: 0 } })
|
|
150
149
|
}
|
|
@@ -159,7 +158,7 @@ test('fastening keeps a part where it stands against what is drawn: a target mov
|
|
|
159
158
|
return w.open('x').then(() => {
|
|
160
159
|
w.apply([{ kind: 'set-state', input: 'travel', unit: 'mm', value: 250 }])
|
|
161
160
|
const before = boxOf(w.revisionNow()!, 'tag')!.centre.x
|
|
162
|
-
w.command({ kind: 'attach', part: 'tag',
|
|
161
|
+
w.command({ kind: 'attach', part: 'tag', to: { part: 'carriage', face: 'top' }, align: { x: { mm: placeKeptOf(shown, 'tag', { part: 'carriage', face: 'top' }).alignMm.x! } } })
|
|
163
162
|
assert.equal(Math.round(boxOf(w.revisionNow()!, 'tag')!.centre.x), Math.round(before))
|
|
164
163
|
})
|
|
165
164
|
})
|
|
@@ -164,7 +164,7 @@ test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서
|
|
|
164
164
|
test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
|
|
165
165
|
let asset = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: { closed: 15 }, end: 'open' } })
|
|
166
166
|
asset = addFigureBox(asset, { id: 'divider', size: { x: 10, y: 100, z: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
167
|
-
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'divider',
|
|
167
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'divider', to: { part: 'body', face: 'inside-bottom' as never } })
|
|
168
168
|
const said = refusal(() => applyFigureAuthoring(asset, { kind: 'set-cavity', part: 'body', cavity: { wall: 10, start: 'open', end: 'open' } }))
|
|
169
169
|
assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
|
|
170
170
|
})
|