@things-factory/figure-ui 10.1.75 → 10.1.76
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/figure-assistant.ts +3 -3
- package/client/graphql/{v3-store.ts → store.ts} +6 -6
- package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
- package/client/modeller/{v3-align.ts → align.ts} +14 -14
- package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
- package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
- package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
- package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
- package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
- package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
- package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
- package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
- package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
- package/client/modeller/figure-history-panel.ts +1 -1
- package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
- package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
- package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
- package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
- package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
- package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
- package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
- package/client/modeller/gate-findings.ts +2 -2
- package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
- package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
- package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
- package/client/modeller/{v3-links.ts → links.ts} +59 -59
- package/client/modeller/{v3-live.ts → live.ts} +9 -9
- package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
- package/client/modeller/modeller-route.ts +1 -1
- package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
- package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
- package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
- package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
- package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
- package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
- package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
- package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
- package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
- package/client/route.ts +1 -5
- package/dist-client/figure-assistant.js +3 -3
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/store.d.ts +3 -0
- package/dist-client/graphql/store.js +39 -0
- package/dist-client/graphql/store.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +51 -0
- package/dist-client/modeller/ai-proposal.js +64 -0
- package/dist-client/modeller/ai-proposal.js.map +1 -0
- package/dist-client/modeller/align.d.ts +30 -0
- package/dist-client/modeller/align.js +75 -0
- package/dist-client/modeller/align.js.map +1 -0
- package/dist-client/modeller/canvas.d.ts +72 -0
- package/dist-client/modeller/canvas.js +379 -0
- package/dist-client/modeller/canvas.js.map +1 -0
- package/dist-client/modeller/clips.d.ts +39 -0
- package/dist-client/modeller/clips.js +58 -0
- package/dist-client/modeller/clips.js.map +1 -0
- package/dist-client/modeller/controls.d.ts +28 -0
- package/dist-client/modeller/controls.js +28 -0
- package/dist-client/modeller/controls.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +203 -0
- package/dist-client/modeller/editor.js +1526 -0
- package/dist-client/modeller/editor.js.map +1 -0
- package/dist-client/modeller/fields.d.ts +45 -0
- package/dist-client/modeller/fields.js +83 -0
- package/dist-client/modeller/fields.js.map +1 -0
- package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
- package/dist-client/modeller/figure-capability-panel.js +141 -0
- package/dist-client/modeller/figure-capability-panel.js.map +1 -0
- package/dist-client/modeller/figure-clips.d.ts +31 -0
- package/dist-client/modeller/figure-clips.js +268 -0
- package/dist-client/modeller/figure-clips.js.map +1 -0
- package/dist-client/modeller/figure-controls.d.ts +36 -0
- package/dist-client/modeller/figure-controls.js +226 -0
- package/dist-client/modeller/figure-controls.js.map +1 -0
- package/dist-client/modeller/figure-facts.d.ts +35 -0
- package/dist-client/modeller/figure-facts.js +55 -0
- package/dist-client/modeller/figure-facts.js.map +1 -0
- package/dist-client/modeller/figure-first-save.d.ts +20 -0
- package/dist-client/modeller/figure-first-save.js +118 -0
- package/dist-client/modeller/figure-first-save.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +1 -1
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +77 -0
- package/dist-client/modeller/figure-links.js +649 -0
- package/dist-client/modeller/figure-links.js.map +1 -0
- package/dist-client/modeller/figure-material-panel.d.ts +39 -0
- package/dist-client/modeller/figure-material-panel.js +323 -0
- package/dist-client/modeller/figure-material-panel.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +69 -0
- package/dist-client/modeller/figure-parts.js +559 -0
- package/dist-client/modeller/figure-parts.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +34 -0
- package/dist-client/modeller/figure-preview.js +346 -0
- package/dist-client/modeller/figure-preview.js.map +1 -0
- package/dist-client/modeller/figure-properties.d.ts +45 -0
- package/dist-client/modeller/figure-properties.js +239 -0
- package/dist-client/modeller/figure-properties.js.map +1 -0
- package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
- package/dist-client/modeller/figure-recipe-catalog.js +399 -0
- package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +64 -0
- package/dist-client/modeller/figure-verdict.js +787 -0
- package/dist-client/modeller/figure-verdict.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +17 -0
- package/dist-client/modeller/first-save.js +26 -0
- package/dist-client/modeller/first-save.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +2 -2
- package/dist-client/modeller/gate-findings.js +2 -2
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +72 -0
- package/dist-client/modeller/gizmo.js +111 -0
- package/dist-client/modeller/gizmo.js.map +1 -0
- package/dist-client/modeller/help.d.ts +18 -0
- package/dist-client/modeller/help.js +37 -0
- package/dist-client/modeller/help.js.map +1 -0
- package/dist-client/modeller/joint-drag.d.ts +19 -0
- package/dist-client/modeller/joint-drag.js +97 -0
- package/dist-client/modeller/joint-drag.js.map +1 -0
- package/dist-client/modeller/joint-fan.d.ts +9 -0
- package/dist-client/modeller/joint-fan.js +276 -0
- package/dist-client/modeller/joint-fan.js.map +1 -0
- package/dist-client/modeller/keys.d.ts +22 -0
- package/dist-client/modeller/keys.js +26 -0
- package/dist-client/modeller/keys.js.map +1 -0
- package/dist-client/modeller/links.d.ts +267 -0
- package/dist-client/modeller/links.js +480 -0
- package/dist-client/modeller/links.js.map +1 -0
- package/dist-client/modeller/live.d.ts +37 -0
- package/dist-client/modeller/live.js +68 -0
- package/dist-client/modeller/live.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +120 -0
- package/dist-client/modeller/maturity.js +313 -0
- package/dist-client/modeller/maturity.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +1 -1
- package/dist-client/modeller/modeller-route.js +1 -1
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +18 -0
- package/dist-client/modeller/occupancy.js +24 -0
- package/dist-client/modeller/occupancy.js.map +1 -0
- package/dist-client/modeller/palette.d.ts +7 -0
- package/dist-client/modeller/palette.js +46 -0
- package/dist-client/modeller/palette.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +47 -0
- package/dist-client/modeller/preview-model.js +68 -0
- package/dist-client/modeller/preview-model.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +510 -0
- package/dist-client/modeller/product-session.js +157 -0
- package/dist-client/modeller/product-session.js.map +1 -0
- package/dist-client/modeller/recipe-source.d.ts +8 -0
- package/dist-client/modeller/recipe-source.js +14 -0
- package/dist-client/modeller/recipe-source.js.map +1 -0
- package/dist-client/modeller/recipes.d.ts +37 -0
- package/dist-client/modeller/recipes.js +103 -0
- package/dist-client/modeller/recipes.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +68 -0
- package/dist-client/modeller/refusals.js +142 -0
- package/dist-client/modeller/refusals.js.map +1 -0
- package/dist-client/modeller/renderer.d.ts +40 -0
- package/dist-client/modeller/renderer.js +73 -0
- package/dist-client/modeller/renderer.js.map +1 -0
- package/dist-client/modeller/stage-model.d.ts +39 -0
- package/dist-client/modeller/stage-model.js +147 -0
- package/dist-client/modeller/stage-model.js.map +1 -0
- package/dist-client/modeller/thumbnail.d.ts +3 -0
- package/dist-client/modeller/thumbnail.js +75 -0
- package/dist-client/modeller/thumbnail.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +1 -1
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +102 -0
- package/dist-client/pages/figure-modeller-page.js +548 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +1 -5
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/product-cutover.md +25 -0
- package/package.json +5 -5
- package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
- package/test/{v3-align.test.ts → align.test.ts} +8 -8
- package/test/browser/modeller-page.js +30 -30
- package/test/browser/modeller.check.mjs +14 -14
- package/test/browser/serve.mjs +1 -1
- package/test/browser/thumbnail-page.js +6 -6
- package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
- package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
- package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
- package/test/figure-assistant-surface.test.ts +3 -3
- package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
- package/test/fixtures/joint-column.json +2 -2
- package/test/fixtures/joint-roller.json +2 -2
- package/test/fixtures/robot-arm-6dof.json +2 -2
- package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
- package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
- package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
- package/test/joints.ts +6 -6
- package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
- package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
- package/test/{v3-links.test.ts → links.test.ts} +201 -201
- package/test/{v3-live.test.ts → live.test.ts} +18 -18
- package/test/{v3-material.test.ts → material.test.ts} +12 -12
- package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
- package/test/modeller-route.test.ts +5 -6
- package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
- package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
- package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
- package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
- package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
- package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
- package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
- package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
- package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
- package/test/renderer.test.ts +24 -0
- package/test/resolve-ts-siblings.mjs +1 -1
- package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
- package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
- package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
- package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
- package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
- package/test/{v3-view.test.ts → view.test.ts} +2 -2
- package/things-factory.config.js +2 -3
- package/translations/en.json +2 -2
- package/translations/ja.json +2 -2
- package/translations/ko.json +2 -2
- package/translations/ms.json +2 -2
- package/translations/zh.json +2 -2
- package/docs/v3-product-cutover.md +0 -25
- package/test/v3-renderer.test.ts +0 -24
- /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
- /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
- /package/client/modeller/{v3-help.ts → help.ts} +0 -0
- /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
- /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* **부품이 무엇에 묶여 있나 — 크기 따르기 · 붙이기 · 움직임** (2026-09-24).
|
|
3
3
|
*
|
|
4
4
|
* 세 칸이 보여 주는 것은 figure-model 명령이 남긴 그래프를 거꾸로 읽은 것이다. 명령을 실제로 적용한 뒤 읽어서,
|
|
5
5
|
* 화면이 명령과 같은 말을 하는지 잰다.
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
import { test } from 'node:test'
|
|
8
8
|
import assert from 'node:assert/strict'
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
10
|
+
import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAsset } from '@hatiolab/figure-model'
|
|
11
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
|
-
import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/
|
|
13
|
+
import { acceptsOf, canFasten, canFollow, dimensionNow, instanceRangeOf, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow, fastenedTo, followableAxes, followOf, hasInstanceSize, positionIsShare, holdsThings, motionOf, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from '../client/modeller/links.ts'
|
|
14
14
|
|
|
15
|
-
const pair = ():
|
|
16
|
-
let asset =
|
|
17
|
-
asset =
|
|
18
|
-
asset =
|
|
15
|
+
const pair = (): FigureAsset => {
|
|
16
|
+
let asset = createFigureAsset('Pair')
|
|
17
|
+
asset = addFigureBox(asset, { id: 'plate', size: { x: 600, y: 100, z: 400 }, position: { x: 0, y: 50, z: 0 } })
|
|
18
|
+
asset = addFigureBox(asset, { id: 'block', size: { x: 200, y: 60, z: 120 }, position: { x: 0, y: 130, z: 0 } })
|
|
19
19
|
const model = asset.document.model
|
|
20
20
|
for (const axis of ['x', 'y', 'z']) {
|
|
21
21
|
model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 100, max: 20000, role: 'design' } as never)
|
|
@@ -27,23 +27,23 @@ const pair = (): V3Asset => {
|
|
|
27
27
|
test('크기 — 제 치수 · 인스턴스 크기 · 다른 부품의 치수를 가려 읽는다', () => {
|
|
28
28
|
const asset = pair()
|
|
29
29
|
assert.deepEqual(followOf(asset, 'plate', 'x'), { of: 'own' })
|
|
30
|
-
const byInstance =
|
|
30
|
+
const byInstance = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'plate', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
31
31
|
assert.deepEqual(followOf(byInstance, 'plate', 'x'), { of: 'instance', axis: 'x', times: 1 })
|
|
32
|
-
const byPart =
|
|
32
|
+
const byPart = applyFigureAuthoring(byInstance, { kind: 'link-dimension', part: 'block', axis: 'x', source: { of: 'part', part: 'plate', axis: 'x' }, times: 0.5 })
|
|
33
33
|
assert.deepEqual(followOf(byPart, 'block', 'x'), { of: 'part', part: 'plate', axis: 'x', times: 0.5 }, '배율이 끼어도 원천을 찾고, 배율도 읽는다')
|
|
34
|
-
const back =
|
|
34
|
+
const back = applyFigureAuthoring(byPart, { kind: 'unlink-dimension', part: 'block', axis: 'x', mm: 300 })
|
|
35
35
|
assert.deepEqual(followOf(back, 'block', 'x'), { of: 'own' }, '떼면 다시 제 치수')
|
|
36
36
|
assert.equal(hasInstanceSize(asset, 'y'), true)
|
|
37
|
-
assert.equal(hasInstanceSize(
|
|
37
|
+
assert.equal(hasInstanceSize(createFigureAsset('Bare'), 'y'), false)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
test('붙이기 — 누구에게 붙었나, 붙일 수 있는 모양인가', () => {
|
|
41
41
|
const asset = pair()
|
|
42
42
|
assert.equal(fastenedTo(asset, 'block'), null)
|
|
43
|
-
const fastened =
|
|
43
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'block', face: 'bottom', to: { part: 'plate', face: 'top' } })
|
|
44
44
|
assert.equal(fastenedTo(fastened, 'block'), 'plate')
|
|
45
|
-
assert.equal(fastenedTo(
|
|
46
|
-
const withDrum =
|
|
45
|
+
assert.equal(fastenedTo(applyFigureAuthoring(fastened, { kind: 'detach', part: 'block' }), 'block'), null)
|
|
46
|
+
const withDrum = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 50, length: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
47
47
|
assert.equal(canFasten(withDrum, 'block'), true)
|
|
48
48
|
assert.equal(canFasten(withDrum, 'drum'), true, '원기둥도 면으로 붙인다(figure-model 0.1.47)')
|
|
49
49
|
assert.equal(canFollow(withDrum, 'drum'), true, '원기둥은 반지름(X)과 길이(Y)를 따를 수 있다')
|
|
@@ -62,13 +62,13 @@ test('움직임 — 미끄러짐 · 돌기, 축, 무엇이 모느냐를 읽는
|
|
|
62
62
|
const turning = moving(asset, 'block', { kind: 'turn', axis: 'y', id: 'swing', min: -90, max: 90, start: 0 })
|
|
63
63
|
assert.deepEqual([motionOf(turning, 'block')!.kind, motionOf(turning, 'block')!.axis], ['turn', 'y'])
|
|
64
64
|
const joint = sliding.joints!.find(j => j.body1.part === 'block')!
|
|
65
|
-
const still =
|
|
65
|
+
const still = applyFigureAuthoring(sliding, { kind: 'set-joint', joint: { id: joint.id, type: 'fixed', body0: joint.body0, body1: joint.body1 } })
|
|
66
66
|
assert.equal(motionOf(still, 'block'), undefined, 'a fixed joint does not move')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
import { readFileSync } from 'node:fs'
|
|
70
70
|
import { fileURLToPath } from 'node:url'
|
|
71
|
-
import { choiceOf } from '../client/modeller/
|
|
71
|
+
import { choiceOf } from '../client/modeller/links.ts'
|
|
72
72
|
import { moving } from './joints.ts'
|
|
73
73
|
|
|
74
74
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
@@ -81,21 +81,21 @@ test('고르개의 값은 지금 따르는 것을 가리킨다', () => {
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
test('편집기는 세 칸의 명령을 세션의 이력 하나로 돌리고, 만날 수 있는 거절을 사람의 말로 한다', () => {
|
|
84
|
-
const editor = read('../client/modeller/
|
|
85
|
-
assert.match(editor, /@
|
|
84
|
+
const editor = read('../client/modeller/editor.ts')
|
|
85
|
+
assert.match(editor, /@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
|
|
86
86
|
assert.match(editor, /this\.session!\.command\(action\)/)
|
|
87
87
|
assert.match(editor, /code === 'DIMENSION_CYCLE'[\s\S]*?figure\.text\.follow-would-loop/)
|
|
88
88
|
assert.match(editor, /code === 'ATTACH_CYCLE'[\s\S]*?figure\.text\.fasten-would-loop/)
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
|
|
92
|
-
const links = read('../client/modeller/figure-
|
|
92
|
+
const links = read('../client/modeller/figure-links.ts')
|
|
93
93
|
assert.ok(links.includes("this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })"))
|
|
94
94
|
assert.ok(links.includes('const current = dimensionNow(asset, this.partId, axis) ?? this.dims?.[AXES.indexOf(axis)] ?? 100'))
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
test('세 칸의 말은 다섯 말 모두에 있다', () => {
|
|
98
|
-
const links = read('../client/modeller/figure-
|
|
98
|
+
const links = read('../client/modeller/figure-links.ts')
|
|
99
99
|
const keys = [...links.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
100
100
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms']) {
|
|
101
101
|
const table = JSON.parse(read(`../translations/${lang}.json`))
|
|
@@ -106,25 +106,25 @@ test('세 칸의 말은 다섯 말 모두에 있다', () => {
|
|
|
106
106
|
|
|
107
107
|
test('도형 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
|
|
108
108
|
let asset = pair()
|
|
109
|
-
asset =
|
|
109
|
+
asset = addFigureBox(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
|
|
110
110
|
const model = asset.document.model
|
|
111
111
|
model.nodes.push({ id: 'lamp.lift', op: 'add@1', args: ['size.y', 'lamp.position.y'], outputs: { value: 'lamp.lift.value' } } as never)
|
|
112
112
|
model.nodes.find(n => n.id === 'lamp.pose')!.args[1] = 'lamp.lift.value'
|
|
113
113
|
assert.deepEqual(positionsWorkedOut(asset, 'lamp'), ['y'])
|
|
114
114
|
assert.deepEqual(positionsWorkedOut(asset, 'plate'), [], '제 숫자로 놓인 부품에는 없다')
|
|
115
|
-
const fixed =
|
|
115
|
+
const fixed = applyFigureAuthoring(asset, { kind: 'fix-position', part: 'lamp', axis: 'y' })
|
|
116
116
|
assert.deepEqual(positionsWorkedOut(fixed, 'lamp'), [])
|
|
117
117
|
assert.equal(fixed.designInputs['lamp.position.y'], 110, '지금 자리 그대로 — 100 + 10')
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
test('자리 — 부품의 면에 둔 집는 자리·놓는 자리를 그 부품에서 읽는다', () => {
|
|
121
121
|
let asset = pair()
|
|
122
|
-
asset =
|
|
122
|
+
asset = addFigurePrimitive(asset, { id: 'drum', kind: 'cylinder', dimensions: { radius: 100, length: 300 }, position: { x: 0, y: 400, z: 0 } } as never)
|
|
123
123
|
assert.deepEqual(placeFacesOf(asset, 'block'), ['left', 'right', 'bottom', 'top', 'back', 'front'])
|
|
124
124
|
assert.deepEqual(placeFacesOf(asset, 'drum'), [], '둥근 몸에는 자리를 권하지 않는다 — 명령이 거절할 것이다')
|
|
125
125
|
|
|
126
126
|
assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip')
|
|
127
|
-
asset =
|
|
127
|
+
asset = applyFigureAuthoring(asset, { kind: 'add-binding', id: placeIdFor(asset, 'block', 'grip'), label: '집는 자리', role: 'grip', part: 'block', face: 'top', accepts: acceptsOf(' BOX, TOTE,, BOX '), capacity: 2 })
|
|
128
128
|
assert.equal(placeIdFor(asset, 'block', 'grip'), 'block-grip-2', '같은 부품에 둘째 자리는 다른 id')
|
|
129
129
|
assert.deepEqual(
|
|
130
130
|
placesOn(asset, 'block').map(p => [p.id, p.role, p.face, p.accepts, p.capacity]),
|
|
@@ -141,18 +141,18 @@ test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것
|
|
|
141
141
|
asset.document.capabilities.names = ['FlowNode']
|
|
142
142
|
assert.equal(holdsThings(asset), true, 'FlowNode 는 Capacity 를 품는다')
|
|
143
143
|
|
|
144
|
-
const links = read('../client/modeller/figure-
|
|
144
|
+
const links = read('../client/modeller/figure-links.ts')
|
|
145
145
|
assert.match(links, /holdsThings\(asset\)/)
|
|
146
146
|
assert.match(links, /kind: 'set-capabilities', names/)
|
|
147
|
-
const editor = read('../client/modeller/
|
|
147
|
+
const editor = read('../client/modeller/editor.ts')
|
|
148
148
|
/* A slice and `includes`, not a lazy regex over the whole editor: that hangs when it does not match. */
|
|
149
|
-
const start = editor.indexOf('<figure-
|
|
150
|
-
const block = editor.slice(start, editor.indexOf('</figure-
|
|
151
|
-
assert.ok(start >= 0 && block.includes('@
|
|
149
|
+
const start = editor.indexOf('<figure-links')
|
|
150
|
+
const block = editor.slice(start, editor.indexOf('</figure-links>', start))
|
|
151
|
+
assert.ok(start >= 0 && block.includes('@part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}'), '자리 절의 능력 선언이 편집기에 닿는다')
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「아무거나」가 없다', () => {
|
|
155
|
-
const links = read('../client/modeller/figure-
|
|
155
|
+
const links = read('../client/modeller/figure-links.ts')
|
|
156
156
|
assert.match(links, /\?disabled=\$\{this\.locked \|\| !accepts\.length \|\| !capacityOk\}/)
|
|
157
157
|
assert.deepEqual(acceptsOf(' , ,'), [])
|
|
158
158
|
})
|
|
@@ -160,9 +160,9 @@ test('받는 종류가 없으면 자리를 만들지 않는다 — 형식에 「
|
|
|
160
160
|
/* ── Proportional rules (user 2026-09-24: ROBOT_ARM at twice the size looked broken) ───────────── */
|
|
161
161
|
|
|
162
162
|
const sizedPair = () => {
|
|
163
|
-
let asset =
|
|
164
|
-
asset =
|
|
165
|
-
asset =
|
|
163
|
+
let asset = createFigureAsset('Probe')
|
|
164
|
+
asset = addFigureBox(asset, { id: 'base', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
165
|
+
asset = addFigurePrimitive(asset, { id: 'flange', kind: 'cylinder', dimensions: { radius: 20, length: 10 }, position: { x: 0, y: 95, z: 0 } })
|
|
166
166
|
asset.document.model.inputs.push({ id: 'size.y', unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
167
167
|
asset.designInputs['size.y'] = 100
|
|
168
168
|
return asset
|
|
@@ -172,7 +172,7 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
|
|
|
172
172
|
const asset = sizedPair()
|
|
173
173
|
assert.equal(dimensionNow(asset, 'flange', 'y'), 10, '원기둥의 Y 는 길이')
|
|
174
174
|
assert.equal(dimensionNow(asset, 'flange', 'x'), 20, '원기둥의 X 는 반지름')
|
|
175
|
-
const share =
|
|
175
|
+
const share = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.1 })
|
|
176
176
|
assert.deepEqual(followOf(share, 'flange', 'y'), { of: 'instance', axis: 'y', times: 0.1 })
|
|
177
177
|
|
|
178
178
|
/* A span: the size less what the others take -- a sum, not a share (ROBOT_ARM's tool flange). */
|
|
@@ -183,7 +183,7 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
|
|
|
183
183
|
m.nodes.push({ id: 'flange.dimension.y', op: 'mul@1', args: ['flange.free.value', m.constants.find(c => c.value === 1)?.id ?? 'k.one'], outputs: { value: 'flange.dimension.y.value' } })
|
|
184
184
|
if (!m.constants.some(c => c.value === 1)) m.constants.push({ id: 'k.one', unit: 'ratio', value: 1 })
|
|
185
185
|
/* Read as what it is since the plus is read (2026-09-25): the size less 90 -- following the size, not a share of it. */
|
|
186
|
-
assert.deepEqual(followOf(fill as
|
|
186
|
+
assert.deepEqual(followOf(fill as FigureAsset, 'flange', 'y'), { of: 'instance', axis: 'y', times: 1, plus: -90 })
|
|
187
187
|
|
|
188
188
|
/* Before any link the shape reads the measure directly; a worked-out one there is not 「고정」 (ROBOT_ARM on :3300). */
|
|
189
189
|
const direct = structuredClone(asset)
|
|
@@ -191,23 +191,23 @@ test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽
|
|
|
191
191
|
d.constants.push({ id: 'k.gaps', unit: 'mm', value: -90 })
|
|
192
192
|
d.nodes.push({ id: 'flange.free', op: 'add@1', args: ['size.y', 'k.gaps'], outputs: { value: 'flange.free.value' } })
|
|
193
193
|
d.nodes.find(n => n.id === 'flange.shape')!.args[1] = 'flange.free.value'
|
|
194
|
-
assert.deepEqual(followOf(direct as
|
|
194
|
+
assert.deepEqual(followOf(direct as FigureAsset, 'flange', 'y'), { of: 'expression' })
|
|
195
195
|
assert.deepEqual(followOf(asset, 'flange', 'y'), { of: 'own' }, '제 숫자는 고정')
|
|
196
196
|
})
|
|
197
197
|
|
|
198
198
|
test('자리를 크기에 비례로 두면 그렇게 읽고, 다시 권하지 않는다', () => {
|
|
199
199
|
let asset = sizedPair()
|
|
200
|
-
asset =
|
|
200
|
+
asset = applyFigureAuthoring(asset, { kind: 'scale-position', part: 'flange', axis: 'y' })
|
|
201
201
|
assert.equal(positionIsShare(asset, 'flange', 'y'), true)
|
|
202
202
|
assert.equal(positionIsShare(asset, 'base', 'y'), false)
|
|
203
|
-
const links = read('../client/modeller/figure-
|
|
203
|
+
const links = read('../client/modeller/figure-links.ts')
|
|
204
204
|
assert.ok(links.includes("hasInstanceSize(asset, axis) && !positionIsShare(asset, this.partId, axis)"))
|
|
205
205
|
assert.ok(links.includes("this.command({ kind: 'scale-position', part: this.partId, axis })"))
|
|
206
206
|
assert.ok(links.includes("const times = Math.round((current / size) * 1e6) / 1e6"), '비례는 지금 치수의 몫')
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로 바꾸면 다시 재어 넓힌다', () => {
|
|
210
|
-
assert.equal(instanceRangeOf(
|
|
210
|
+
assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
|
|
211
211
|
let asset = sizedPair()
|
|
212
212
|
for (const axis of ['x', 'z'] as const) {
|
|
213
213
|
asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
@@ -220,9 +220,9 @@ test('놓일 수 있는 크기 — 남은 만큼 채우던 부품을 비례로
|
|
|
220
220
|
assert.deepEqual(range.declared.y, { min: 90, max: 1000 })
|
|
221
221
|
assert.deepEqual(range.measured.y, { min: 1, max: 1000 }, '기본 범위: 1 mm 에서 열 배')
|
|
222
222
|
assert.equal(rangeDiffers(range.declared, range.measured), true)
|
|
223
|
-
asset =
|
|
223
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: range.measured, replace: true })
|
|
224
224
|
assert.equal(rangeDiffers(instanceRangeOf(asset)!.declared, instanceRangeOf(asset)!.measured), false, '다시 재면 같다')
|
|
225
|
-
const editor = read('../client/modeller/
|
|
225
|
+
const editor = read('../client/modeller/editor.ts')
|
|
226
226
|
assert.ok(editor.includes("this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })"))
|
|
227
227
|
})
|
|
228
228
|
|
|
@@ -237,30 +237,30 @@ test('놓일 수 있는 크기 — 저작자가 끝을 적어 좁힌다(갠트
|
|
|
237
237
|
const next = rangeWith(declared, 'x', 'min', 150)!
|
|
238
238
|
assert.deepEqual(next.x, { min: 150, max: 20000 })
|
|
239
239
|
assert.deepEqual(declared.x, { min: 1, max: 20000 }, '선언된 범위는 그대로')
|
|
240
|
-
asset =
|
|
240
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: next, replace: true })
|
|
241
241
|
assert.deepEqual(instanceRangeOf(asset)!.declared.x, { min: 150, max: 20000 })
|
|
242
|
-
assert.throws(() =>
|
|
243
|
-
const editor = read('../client/modeller/
|
|
242
|
+
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)를 벗어나면 거절')
|
|
243
|
+
const editor = read('../client/modeller/editor.ts')
|
|
244
244
|
assert.ok(editor.includes("const next = rangeWith(range.declared, axis, bound, (e.target as HTMLInputElement).valueAsNumber)"))
|
|
245
245
|
assert.ok(editor.includes("if (next) this.linkCommand({ kind: 'declare-instance-size', range: next, replace: true })"))
|
|
246
246
|
})
|
|
247
247
|
|
|
248
248
|
test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
|
|
249
|
-
let asset =
|
|
250
|
-
asset =
|
|
251
|
-
asset =
|
|
252
|
-
asset =
|
|
253
|
-
asset =
|
|
254
|
-
asset =
|
|
249
|
+
let asset = createFigureAsset('Probe')
|
|
250
|
+
asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
251
|
+
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
252
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'base', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
253
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'base', face: 'top' }, align: { x: { mm: 120 } } })
|
|
254
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 40, max: 4000 }, y: { min: 16, max: 1600 }, z: { min: 40, max: 4000 } } })
|
|
255
255
|
assert.equal(proportionToFollow(asset), undefined, '한 비율이 아니면 권하지 않는다')
|
|
256
|
-
asset =
|
|
256
|
+
asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' })
|
|
257
257
|
assert.deepEqual(proportionToFollow(asset), { count: 3 + 3 + 1, kept: [] }, '치수 여섯과 자리 하나')
|
|
258
258
|
const lamp = asset.joints!.find(j => j.id === 'lamp')!
|
|
259
|
-
asset =
|
|
260
|
-
asset =
|
|
259
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...lamp, type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } } })
|
|
260
|
+
asset = applyFigureAuthoring(asset, { kind: 'follow-size-in-proportion' })
|
|
261
261
|
assert.deepEqual(proportionToFollow(asset), { count: 0, kept: ['lamp.motion'] }, '다 따르고, mm 로 움직이는 것만 남는다')
|
|
262
262
|
assert.equal(Math.round(valueNow(asset, 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
|
|
263
|
-
const editor = read('../client/modeller/
|
|
263
|
+
const editor = read('../client/modeller/editor.ts')
|
|
264
264
|
assert.ok(editor.includes("this.linkCommand({ kind: 'follow-size-in-proportion' })"))
|
|
265
265
|
assert.ok(editor.includes("?? valueNow(asset, layout?.args[4])"), '길이를 따르는 간격도 숫자로 보인다')
|
|
266
266
|
})
|
|
@@ -270,7 +270,7 @@ test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언
|
|
|
270
270
|
assert.equal(turnsAboutAJoint(asset), false)
|
|
271
271
|
asset = moving(asset, 'flange', { kind: 'turn', axis: 'y', id: 'flange.turn', min: 0, max: 90 })
|
|
272
272
|
assert.equal(turnsAboutAJoint(asset), true)
|
|
273
|
-
const editor = read('../client/modeller/
|
|
273
|
+
const editor = read('../client/modeller/editor.ts')
|
|
274
274
|
const rule = editor.slice(editor.indexOf(' private renderSizingRule('), editor.indexOf(' /** Choose a part,'))
|
|
275
275
|
assert.ok(rule.includes("asset.sizing === 'uniform'") && rule.includes("'figure.label.sizing-uniform'"), '선언하면 규칙을 말한다')
|
|
276
276
|
assert.ok(rule.includes("turnsAboutAJoint(asset) ? html`<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}"), '관절이 있으면 권한다')
|
|
@@ -279,10 +279,10 @@ test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언
|
|
|
279
279
|
|
|
280
280
|
|
|
281
281
|
test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만, 바닥에는 모서리가 없다 (문을 틀 모서리에 달기, 09-25)', async () => {
|
|
282
|
-
const { alignAxesOf, alignOf } = await import('../client/modeller/
|
|
283
|
-
let two =
|
|
284
|
-
two =
|
|
285
|
-
two =
|
|
282
|
+
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
283
|
+
let two = createFigureAsset('Two')
|
|
284
|
+
two = addFigureBox(two, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
285
|
+
two = addFigureBox(two, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
286
286
|
const right = { part: 'jamb', face: 'right' }
|
|
287
287
|
assert.deepEqual(alignAxesOf(two, 'leaf', right), [{ axis: 'y', edges: true }, { axis: 'z', edges: true }])
|
|
288
288
|
assert.deepEqual(alignAxesOf(two, 'leaf', 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }])
|
|
@@ -293,63 +293,63 @@ test('붙일 때 면 위 어디에 — 그 면을 가로지르는 두 축만,
|
|
|
293
293
|
assert.equal(alignOf(two, 'leaf', { part: 'jamb', face: 'top' }, {}, {}), undefined)
|
|
294
294
|
|
|
295
295
|
/* What the form sends is what hangs the part on the edge: the door's pivot is on the jamb's front edge line. */
|
|
296
|
-
let asset =
|
|
297
|
-
asset =
|
|
298
|
-
asset =
|
|
299
|
-
asset =
|
|
300
|
-
asset =
|
|
301
|
-
const {
|
|
302
|
-
const leaf =
|
|
296
|
+
let asset = createFigureAsset('Door')
|
|
297
|
+
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 1050, z: 0 } })
|
|
298
|
+
asset = addFigureBox(asset, { id: 'leaf', size: { x: 900, y: 2000, z: 40 }, position: { x: 600, y: 1000, z: 0 } })
|
|
299
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
300
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: alignOf(asset, 'leaf', { part: 'jamb', face: 'right' }, { y: 'min', z: 'max' }, {}) })
|
|
301
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
302
|
+
const leaf = compileFigureAsset(asset).evaluate().geometry.find(g => g.id === 'leaf')!
|
|
303
303
|
assert.deepEqual(leaf.pose.t.map(Math.round), [500, 1000, 55], '문짝 아래가 문틀 아래에, 앞이 문틀 앞에')
|
|
304
304
|
})
|
|
305
305
|
|
|
306
306
|
test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서 거리로 채운다 (문짝이 문틀 앞 100 mm 에서 가운데로 옮겨졌다, 09-26)', async () => {
|
|
307
|
-
const { alignOf, placeKeptOf } = await import('../client/modeller/
|
|
308
|
-
const {
|
|
309
|
-
let asset =
|
|
310
|
-
asset =
|
|
311
|
-
asset =
|
|
312
|
-
const where = (a:
|
|
307
|
+
const { alignOf, placeKeptOf } = await import('../client/modeller/links.ts')
|
|
308
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
309
|
+
let asset = createFigureAsset('Door')
|
|
310
|
+
asset = addFigureBox(asset, { id: 'frame', size: { x: 1600, y: 100, z: 150 }, position: { x: 300, y: 50, z: 0 } })
|
|
311
|
+
asset = addFigureBox(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } })
|
|
312
|
+
const where = (a: FigureAsset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
|
|
313
313
|
for (const target of ['plane', { part: 'frame', face: 'top' }] as const) {
|
|
314
314
|
const kept = placeKeptOf(asset, 'leaf', target)
|
|
315
315
|
const align = alignOf(asset, 'leaf', target, kept.align, kept.alignMm)
|
|
316
316
|
const to = target === 'plane' ? { plane: 'mounting-plane' as const } : target
|
|
317
|
-
const fastened =
|
|
317
|
+
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'bottom', to, ...(align ? { align } : {}) } as never)
|
|
318
318
|
const [before, after] = [where(asset), where(fastened)]
|
|
319
319
|
assert.deepEqual([after[0], after[2]], [before[0], before[2]], `${JSON.stringify(target)}: 앞뒤·좌우는 그대로`)
|
|
320
320
|
}
|
|
321
321
|
assert.deepEqual(placeKeptOf(asset, 'leaf', { part: 'frame', face: 'top' }), { align: { x: 'mm', z: 'mm' }, alignMm: { x: -50, z: 100 } })
|
|
322
|
-
const centred =
|
|
322
|
+
const centred = addFigureBox(createFigureAsset('C'), { id: 'a', size: { x: 10, y: 10, z: 10 }, position: { x: 0, y: 5, z: 0 } })
|
|
323
323
|
assert.deepEqual(placeKeptOf(centred, 'a', 'plane'), { align: {}, alignMm: {} }, '가운데 있던 것은 가운데로')
|
|
324
324
|
|
|
325
325
|
/* The form fills it in whenever the aim changes, and does not offer a part that hangs from this one. */
|
|
326
|
-
const { hangsFrom } = await import('../client/modeller/
|
|
327
|
-
let row =
|
|
328
|
-
row =
|
|
329
|
-
row =
|
|
330
|
-
row =
|
|
331
|
-
row =
|
|
326
|
+
const { hangsFrom } = await import('../client/modeller/links.ts')
|
|
327
|
+
let row = addFigureBox(createFigureAsset('Row'), { id: 'plate', size: { x: 1000, y: 50, z: 400 }, position: { x: 0, y: 700, z: 0 } })
|
|
328
|
+
row = addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 400 }, position: { x: 0, y: 760, z: 0 } })
|
|
329
|
+
row = addFigureBox(row, { id: 'cap', size: { x: 20, y: 20, z: 20 }, position: { x: 0, y: 800, z: 0 } })
|
|
330
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'plate', face: 'top' } })
|
|
331
|
+
row = applyFigureAuthoring(row, { kind: 'attach', part: 'cap', face: 'bottom', to: { part: 'roller', face: 'top' } })
|
|
332
332
|
assert.deepEqual([hangsFrom(row, 'roller', 'plate'), hangsFrom(row, 'cap', 'plate'), hangsFrom(row, 'plate', 'roller')], [true, true, false])
|
|
333
|
-
const form = read('../client/modeller/figure-
|
|
333
|
+
const form = read('../client/modeller/figure-links.ts')
|
|
334
334
|
assert.equal(form.match(/this\.aimAt\(asset, \{ \.\.\.f, (toPlane|target|targetFace):/g)?.length, 3, '바닥 · 상대 부품 · 상대 면을 바꿀 때마다')
|
|
335
335
|
assert.ok(form.includes('!hangsFrom(asset, one, this.partId)'))
|
|
336
336
|
})
|
|
337
337
|
|
|
338
338
|
test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작값이 있는 관절만 고르고, 기록에는 배율과 더하기만 (집게 두 손가락, 09-25)', async () => {
|
|
339
|
-
const { leadersFor, motionJoint } = await import('../client/modeller/
|
|
340
|
-
const {
|
|
341
|
-
let asset =
|
|
342
|
-
asset =
|
|
343
|
-
for (const id of ['left', 'right']) asset =
|
|
344
|
-
asset =
|
|
345
|
-
asset =
|
|
346
|
-
asset =
|
|
347
|
-
asset =
|
|
348
|
-
asset =
|
|
339
|
+
const { leadersFor, motionJoint } = await import('../client/modeller/links.ts')
|
|
340
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
341
|
+
let asset = createFigureAsset('Gripper')
|
|
342
|
+
asset = addFigureBox(asset, { id: 'palm', size: { x: 240, y: 70, z: 160 }, position: { x: 0, y: 35, z: 0 } })
|
|
343
|
+
for (const id of ['left', 'right']) asset = addFigureBox(asset, { id, size: { x: 30, y: 140, z: 100 }, position: { x: 0, y: 200, z: 0 } })
|
|
344
|
+
asset = addFigureBox(asset, { id: 'wrist', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 400, z: 0 } })
|
|
345
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'palm', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
346
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'left', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'min' } })
|
|
347
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'right', face: 'bottom', to: { part: 'palm', face: 'top' }, align: { x: 'max' } })
|
|
348
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'wrist', face: 'top', to: { part: 'palm', face: 'bottom' } })
|
|
349
349
|
const form = { kind: 'slide' as const, axis: 'x' as const, id: 'grip', min: 0, max: 60, follow: '', multiplier: 1, offset: 0 }
|
|
350
350
|
const jointOf = (part: string) => asset.joints!.find(j => j.body1.part === part)!
|
|
351
|
-
asset =
|
|
352
|
-
asset =
|
|
351
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('left'), form, 'left') })
|
|
352
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: motionJoint(jointOf('wrist'), { ...form, kind: 'turn', axis: 'y', id: 'twist', min: -90, max: 90 }, 'wrist') })
|
|
353
353
|
|
|
354
354
|
assert.deepEqual(leadersFor(asset, 'right', 'slide').map(j => j.id), ['left'], '미끄러지는 관절은 미끄러지는 관절을 따른다')
|
|
355
355
|
assert.deepEqual(leadersFor(asset, 'right', 'turn').map(j => j.id), ['wrist'])
|
|
@@ -359,28 +359,28 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
|
|
|
359
359
|
assert.deepEqual(follower.mimic, { joint: 'left', multiplier: -1 })
|
|
360
360
|
assert.equal(follower.state, undefined, '따라가는 관절은 제 조작값이 없다')
|
|
361
361
|
assert.equal(follower.lowerLimit, undefined)
|
|
362
|
-
asset =
|
|
362
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: follower })
|
|
363
363
|
assert.deepEqual(leadersFor(asset, 'wrist', 'slide').map(j => j.id), ['left'], '따라가는 관절은 앞장서지 않는다')
|
|
364
364
|
|
|
365
|
-
const x = (id: string, grip: number) => Math.round(
|
|
365
|
+
const x = (id: string, grip: number) => Math.round(compileFigureAsset(asset).evaluate({ grip }).geometry.find(g => g.id === id)!.pose.t[0]!)
|
|
366
366
|
assert.equal(x('left', 40) - x('left', 0), 40)
|
|
367
367
|
assert.equal(x('right', 40) - x('right', 0), -40, '오른손은 거울로 따라 닫힌다')
|
|
368
|
-
const { motionOf } = await import('../client/modeller/
|
|
368
|
+
const { motionOf } = await import('../client/modeller/links.ts')
|
|
369
369
|
assert.deepEqual(motionOf(asset, 'right')?.follows, { joint: 'left', multiplier: -1 }, '화면은 무엇을 따르는지 읽는다')
|
|
370
370
|
})
|
|
371
371
|
|
|
372
372
|
test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없음, 원뿔대는 위아래뿐 (2026-09-25)', async () => {
|
|
373
|
-
const { facesOf, faceAxisOf, canFasten, alignAxesOf } = await import('../client/modeller/
|
|
374
|
-
let asset =
|
|
375
|
-
asset =
|
|
373
|
+
const { facesOf, faceAxisOf, canFasten, alignAxesOf } = await import('../client/modeller/links.ts')
|
|
374
|
+
let asset = createFigureAsset('Doorway')
|
|
375
|
+
asset = addFigurePrimitive(asset, {
|
|
376
376
|
id: 'frame',
|
|
377
377
|
kind: 'portal',
|
|
378
378
|
dimensions: { width: 1100, height: 2200, depth: 150, jamb: 100, head: 100 },
|
|
379
379
|
position: { x: 0, y: 0, z: 0 }
|
|
380
380
|
})
|
|
381
|
-
asset =
|
|
382
|
-
asset =
|
|
383
|
-
asset =
|
|
381
|
+
asset = addFigurePrimitive(asset, { id: 'ball', kind: 'sphere', dimensions: { radiusX: 50, radiusY: 50, radiusZ: 50 }, position: { x: 0, y: 0, z: 0 } })
|
|
382
|
+
asset = addFigurePrimitive(asset, { id: 'cone', kind: 'frustum', dimensions: { radiusTop: 20, radiusBottom: 60, height: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
383
|
+
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
384
384
|
assert.deepEqual(facesOf(asset, 'frame'), ['left', 'right', 'bottom', 'top', 'back', 'front', 'opening-left', 'opening-right', 'opening-top'])
|
|
385
385
|
assert.equal(faceAxisOf(asset, 'frame', 'opening-top'), 'y')
|
|
386
386
|
assert.deepEqual(facesOf(asset, 'ball'), [])
|
|
@@ -392,25 +392,25 @@ test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구
|
|
|
392
392
|
})
|
|
393
393
|
|
|
394
394
|
test('되풀이는 붙어 있고 움직이지 않고 아무것도 매달지 않은 부품에 — 붙인 면을 가로지르는 축으로 (빈틈 B, 2026-09-25)', async () => {
|
|
395
|
-
const { rowAxesOf, rowOf } = await import('../client/modeller/
|
|
396
|
-
let asset =
|
|
397
|
-
asset =
|
|
398
|
-
asset =
|
|
395
|
+
const { rowAxesOf, rowOf } = await import('../client/modeller/links.ts')
|
|
396
|
+
let asset = createFigureAsset('Conveyor')
|
|
397
|
+
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
398
|
+
asset = addFigureBox(asset, { id: 'slat', size: { x: 60, y: 20, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
399
399
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '붙지 않은 부품은 되풀이를 권하지 않는다')
|
|
400
|
-
asset =
|
|
401
|
-
asset =
|
|
400
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
401
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'slat', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
402
402
|
assert.deepEqual(rowAxesOf(asset, 'frame'), [], '바닥에 선 부품은 채울 길이가 없다')
|
|
403
403
|
assert.deepEqual(rowAxesOf(asset, 'slat'), ['x', 'z'], '윗면을 가로지르는 두 축')
|
|
404
|
-
asset =
|
|
404
|
+
asset = applyFigureAuthoring(asset, { kind: 'make-repeat', part: 'slat', axis: 'x', pitch: 100 })
|
|
405
405
|
assert.deepEqual(rowOf(asset, 'slat'), { axis: 'X', pitch: 100 })
|
|
406
406
|
assert.deepEqual(rowAxesOf(asset, 'slat'), [], '이미 되풀이면 다시 권하지 않는다')
|
|
407
407
|
})
|
|
408
408
|
|
|
409
409
|
test('비튼 부품에는 모서리 맞춤을 내놓지도 보내지도 않는다 — 가운데서 거리만 (빈틈 F, 2026-09-25)', async () => {
|
|
410
|
-
const { alignAxesOf, alignOf } = await import('../client/modeller/
|
|
411
|
-
let asset =
|
|
412
|
-
asset =
|
|
413
|
-
asset =
|
|
410
|
+
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
411
|
+
let asset = createFigureAsset('Fan')
|
|
412
|
+
asset = addFigureBox(asset, { id: 'hub', size: { x: 120, y: 120, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
413
|
+
asset = addFigureBox(asset, { id: 'blade', size: { x: 300, y: 60, z: 8 }, position: { x: 0, y: 0, z: 0 } })
|
|
414
414
|
const front = { part: 'hub', face: 'front' }
|
|
415
415
|
assert.deepEqual(alignAxesOf(asset, 'blade', front, true), [{ axis: 'x', edges: false }, { axis: 'y', edges: false }])
|
|
416
416
|
assert.deepEqual(alignOf(asset, 'blade', front, { x: 'max', y: 'mm' }, { y: 50 }, true), { y: { mm: 50 } }, '끝 맞춤은 빠지고 거리만 간다')
|
|
@@ -418,58 +418,58 @@ test('비튼 부품에는 모서리 맞춤을 내놓지도 보내지도 않는
|
|
|
418
418
|
})
|
|
419
419
|
|
|
420
420
|
test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
|
|
421
|
-
const { instanceRangeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/
|
|
422
|
-
const {
|
|
423
|
-
let asset =
|
|
421
|
+
const { instanceRangeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
|
|
422
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
423
|
+
let asset = createFigureAsset('Conveyor')
|
|
424
424
|
assert.equal(instanceRangeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
|
|
425
|
-
asset =
|
|
426
|
-
asset =
|
|
425
|
+
asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
426
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
427
427
|
assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
|
|
428
428
|
const toDeclare = instanceRangeToDeclare(asset)!
|
|
429
429
|
assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
|
|
430
|
-
asset =
|
|
430
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })
|
|
431
431
|
assert.ok(instanceRangeOf(asset), '선언 뒤에는 놓일 수 있는 크기가 보인다')
|
|
432
432
|
assert.equal(instanceRangeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
|
|
433
433
|
assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
|
|
434
|
-
asset =
|
|
435
|
-
const at = (size: number) => (
|
|
434
|
+
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
|
|
435
|
+
const at = (size: number) => (compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as { width: number }).width
|
|
436
436
|
assert.equal(at(3000), 3000, '보드가 3000 으로 놓으면 틀도 3000')
|
|
437
437
|
})
|
|
438
438
|
|
|
439
439
|
test('따르는 치수의 더하기를 읽는다 — 문짝 폭 = 개구부 − 틈 (사이징, 2026-09-25)', async () => {
|
|
440
|
-
const { followOf, choiceOf } = await import('../client/modeller/
|
|
441
|
-
let asset =
|
|
442
|
-
asset =
|
|
443
|
-
asset =
|
|
444
|
-
asset =
|
|
440
|
+
const { followOf, choiceOf } = await import('../client/modeller/links.ts')
|
|
441
|
+
let asset = createFigureAsset('Door')
|
|
442
|
+
asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
443
|
+
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
444
|
+
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'leaf', axis: 'x', source: { of: 'part', part: 'head', axis: 'x' }, plus: -210 })
|
|
445
445
|
assert.deepEqual(followOf(asset, 'leaf', 'x'), { of: 'part', part: 'head', axis: 'x', plus: -210 })
|
|
446
446
|
assert.equal(choiceOf(followOf(asset, 'leaf', 'x')), 'part:head:x', '선택은 그대로 — 더하기는 따로')
|
|
447
|
-
const {
|
|
447
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
448
448
|
const wide = { ...asset, designInputs: { ...asset.designInputs, 'head.size.x': 1500 } }
|
|
449
|
-
assert.equal((
|
|
450
|
-
asset =
|
|
451
|
-
asset =
|
|
449
|
+
assert.equal((compileFigureAsset(wide).evaluate().geometry.find(g => g.id === 'leaf')!.shape.dimensions as { width: number }).width, 1290, '상인방 1500 이면 문짝 1290')
|
|
450
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 500, max: 5000 }, y: { min: 500, max: 5000 }, z: { min: 50, max: 500 } }, at: { x: 1100, y: 2200, z: 150 } })
|
|
451
|
+
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'head', axis: 'x', source: { of: 'instance', axis: 'x' }, times: 0.5, plus: 20 })
|
|
452
452
|
assert.deepEqual(followOf(asset, 'head', 'x'), { of: 'instance', axis: 'x', times: 0.5, plus: 20 })
|
|
453
453
|
})
|
|
454
454
|
|
|
455
455
|
test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이 길어지면 브리지가 끝까지 간다 (사이징, 2026-09-25)', async () => {
|
|
456
|
-
const { motionJoint, motionOf } = await import('../client/modeller/
|
|
457
|
-
const {
|
|
458
|
-
let asset =
|
|
459
|
-
asset =
|
|
460
|
-
asset =
|
|
461
|
-
asset =
|
|
462
|
-
asset =
|
|
456
|
+
const { motionJoint, motionOf } = await import('../client/modeller/links.ts')
|
|
457
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
458
|
+
let asset = createFigureAsset('Gantry')
|
|
459
|
+
asset = addFigureBox(asset, { id: 'rail', size: { x: 3000, y: 120, z: 120 }, position: { x: 0, y: 0, z: 0 } })
|
|
460
|
+
asset = addFigureBox(asset, { id: 'bridge', size: { x: 160, y: 160, z: 1300 }, position: { x: 0, y: 0, z: 0 } })
|
|
461
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'rail', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
462
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bridge', face: 'bottom', to: { part: 'rail', face: 'top' }, align: { x: 'min' } })
|
|
463
463
|
const joint = asset.joints!.find(j => j.body1.part === 'bridge')!
|
|
464
464
|
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 }
|
|
465
465
|
const record = motionJoint(joint, form, 'bridge')
|
|
466
466
|
assert.deepEqual(record.travel, { source: { of: 'part', part: 'rail', axis: 'x' }, plus: -160 })
|
|
467
467
|
assert.equal(record.lowerLimit, undefined, '거리는 레일이 정한다 — mm 범위는 없다')
|
|
468
|
-
asset =
|
|
468
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: record })
|
|
469
469
|
assert.deepEqual(motionOf(asset, 'bridge')?.travel, { part: 'rail', axis: 'x', plus: -160 }, '화면은 무엇을 따르는지 읽는다')
|
|
470
470
|
const end = (railLength: number) => {
|
|
471
471
|
const a = { ...asset, designInputs: { ...asset.designInputs, 'rail.size.x': railLength } }
|
|
472
|
-
const g = (id: string) =>
|
|
472
|
+
const g = (id: string) => compileFigureAsset(a).evaluate({ x: 1 }).geometry.find(x => x.id === id)! as unknown as { pose: { t: number[] }; shape: { dimensions: { width: number } } }
|
|
473
473
|
return Math.round(g('bridge').pose.t[0]! + 80 - (g('rail').pose.t[0]! + railLength / 2))
|
|
474
474
|
}
|
|
475
475
|
assert.equal(end(3000), 0, '끝까지(1) 가면 브리지 끝이 레일 끝')
|
|
@@ -477,47 +477,47 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
|
|
|
477
477
|
})
|
|
478
478
|
|
|
479
479
|
test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
|
|
480
|
-
const { alignOf } = await import('../client/modeller/
|
|
481
|
-
const {
|
|
482
|
-
let asset =
|
|
483
|
-
asset =
|
|
484
|
-
asset =
|
|
485
|
-
asset =
|
|
480
|
+
const { alignOf } = await import('../client/modeller/links.ts')
|
|
481
|
+
const { compileFigureAsset } = await import('@hatiolab/figure-model')
|
|
482
|
+
let asset = createFigureAsset('Panel')
|
|
483
|
+
asset = addFigureBox(asset, { id: 'panel', size: { x: 1000, y: 800, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
484
|
+
asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
|
|
485
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
486
486
|
const front = { part: 'panel', face: 'front' }
|
|
487
487
|
assert.deepEqual(alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
|
|
488
|
-
asset =
|
|
489
|
-
asset =
|
|
488
|
+
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 } })
|
|
489
|
+
asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
|
|
490
490
|
const align = alignOf(asset, 'lamp', front, { x: 'share' }, { x: 300 })!
|
|
491
491
|
assert.deepEqual(align, { x: { mm: { from: { of: 'instance', axis: 'x' }, times: 0.3 } } })
|
|
492
|
-
asset =
|
|
493
|
-
const x = (size: number) => Math.round(
|
|
492
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', face: 'back', to: front, align })
|
|
493
|
+
const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
|
|
494
494
|
assert.equal(x(1000), 300)
|
|
495
495
|
assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
|
|
496
496
|
})
|
|
497
497
|
|
|
498
498
|
test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
|
|
499
|
-
const { instanceRangeToDeclare } = await import('../client/modeller/
|
|
500
|
-
let asset =
|
|
501
|
-
asset =
|
|
502
|
-
asset =
|
|
503
|
-
asset =
|
|
504
|
-
asset =
|
|
499
|
+
const { instanceRangeToDeclare } = await import('../client/modeller/links.ts')
|
|
500
|
+
let asset = createFigureAsset('Door')
|
|
501
|
+
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
502
|
+
asset = addFigureBox(asset, { id: 'leaf', size: { x: 890, y: 2095, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
503
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
504
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'left', to: { part: 'jamb', face: 'right' }, align: { y: 'min', z: 'max' }, gap: 5 })
|
|
505
505
|
const j = asset.joints!.find(o => o.id === 'leaf')!
|
|
506
|
-
asset =
|
|
506
|
+
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...j, type: 'revolute', axis: 'Y', lowerLimit: -90, upperLimit: 0, state: { id: 'open' } } })
|
|
507
507
|
assert.deepEqual(instanceRangeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
|
|
508
508
|
})
|
|
509
509
|
|
|
510
510
|
test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
|
|
511
|
-
const { alignAxesOf, alignOf } = await import('../client/modeller/
|
|
512
|
-
let asset =
|
|
511
|
+
const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
|
|
512
|
+
let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
513
513
|
assert.deepEqual(alignAxesOf(asset, 'post', 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
|
|
514
|
-
asset =
|
|
514
|
+
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 } })
|
|
515
515
|
assert.deepEqual(alignAxesOf(asset, 'post', 'plane'), [{ axis: 'x', edges: true }, { axis: 'z', edges: true }])
|
|
516
516
|
assert.deepEqual(alignOf(asset, 'post', 'plane', { x: 'min' }, {}), { x: 'min' })
|
|
517
517
|
})
|
|
518
518
|
|
|
519
519
|
test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우고, 그대로 보내면 같은 관절이다 (2026-09-25)', async () => {
|
|
520
|
-
const { motionFormOf, motionJoint } = await import('../client/modeller/
|
|
520
|
+
const { motionFormOf, motionJoint } = await import('../client/modeller/links.ts')
|
|
521
521
|
const base = { id: 'j', body0: { part: 'a', face: 'top' as const }, body1: { part: 'b', face: 'bottom' as const } }
|
|
522
522
|
const joints = [
|
|
523
523
|
{ ...base, type: 'prismatic' as const, axis: 'Z' as const, lowerLimit: 0, upperLimit: 2500, state: { id: 'extend' } },
|
|
@@ -537,28 +537,28 @@ test('움직임은 그 자리에서 고친다 — 폼에 지금 관절을 채우
|
|
|
537
537
|
})
|
|
538
538
|
|
|
539
539
|
test('바닥에 선 부품도 무엇에 붙어 있는지 읽는다 — 그래야 옮겨 붙일 수 있다 (문설주, 2026-09-25)', async () => {
|
|
540
|
-
const { heldBy } = await import('../client/modeller/
|
|
541
|
-
let asset =
|
|
542
|
-
asset =
|
|
543
|
-
asset =
|
|
540
|
+
const { heldBy } = await import('../client/modeller/links.ts')
|
|
541
|
+
let asset = createFigureAsset('Door')
|
|
542
|
+
asset = addFigureBox(asset, { id: 'jamb', size: { x: 100, y: 2100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
543
|
+
asset = addFigureBox(asset, { id: 'head', size: { x: 1100, y: 100, z: 150 }, position: { x: 0, y: 0, z: 0 } })
|
|
544
544
|
assert.equal(heldBy(asset, 'jamb'), null)
|
|
545
|
-
asset =
|
|
546
|
-
asset =
|
|
545
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
546
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'head', face: 'bottom', to: { part: 'jamb', face: 'top' } })
|
|
547
547
|
assert.deepEqual(heldBy(asset, 'jamb'), { plane: true }, '장착 평면')
|
|
548
548
|
assert.deepEqual(heldBy(asset, 'head'), { part: 'jamb' })
|
|
549
549
|
/* What the form then sends: a fastening that replaces the one there. */
|
|
550
|
-
assert.doesNotThrow(() =>
|
|
550
|
+
assert.doesNotThrow(() => applyFigureAuthoring(asset, { kind: 'attach', part: 'jamb', face: 'bottom', to: { plane: 'mounting-plane' }, align: { x: { mm: -500 } }, replace: !!heldBy(asset, 'jamb') }))
|
|
551
551
|
})
|
|
552
552
|
|
|
553
553
|
test('벽 배치 — 장착 평면은 Z = 0, 등으로 붙이고 가로질러 X · Y 로 맞춘다(사용자 · 아키텍트 09-25)', async () => {
|
|
554
|
-
const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/
|
|
555
|
-
let a =
|
|
554
|
+
const { mountingPlaneOf, alignAxesOf } = await import('../client/modeller/links.ts')
|
|
555
|
+
let a = addFigureBox(createFigureAsset('Lamp'), { id: 'plate', size: { x: 200, y: 300, z: 20 }, position: { x: 0, y: 0, z: 0 } })
|
|
556
556
|
assert.deepEqual(mountingPlaneOf(a), { normal: 'y', faces: ['bottom', 'top'] }, '배치 선언 전에는 바닥')
|
|
557
|
-
a =
|
|
557
|
+
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: (await import('@hatiolab/figure-model')).proposeFigureOccupancy(a), placement: 'wall' as never })
|
|
558
558
|
assert.deepEqual(mountingPlaneOf(a), { normal: 'z', faces: ['back'] })
|
|
559
559
|
assert.deepEqual(alignAxesOf(a, 'plate', 'plane').map(x => x.axis), ['x', 'y'])
|
|
560
|
-
const form = read('../client/modeller/figure-
|
|
561
|
-
assert.ok(form.includes('(!f.toPlane || plane.faces.includes(face as
|
|
560
|
+
const form = read('../client/modeller/figure-links.ts')
|
|
561
|
+
assert.ok(form.includes('(!f.toPlane || plane.faces.includes(face as FigureFace))'), '평면에 붙일 때는 평면이 받는 면만')
|
|
562
562
|
})
|
|
563
563
|
|
|
564
564
|
test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는 「위치와 크기」, 붙이기 칸은 「붙이기」 (09-26)', () => {
|
|
@@ -572,22 +572,22 @@ test('부품 칸의 두 제목이 같은 말이 아니다 — 자리 숫자는
|
|
|
572
572
|
* 고르는 칸이 없었다(09-26 기준 측정).
|
|
573
573
|
*/
|
|
574
574
|
test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸다', async () => {
|
|
575
|
-
const { shownWhenOf, visibilityControlsOf } = await import('../client/modeller/
|
|
576
|
-
const {
|
|
577
|
-
let asset =
|
|
578
|
-
asset =
|
|
579
|
-
asset =
|
|
580
|
-
asset =
|
|
581
|
-
asset =
|
|
582
|
-
asset =
|
|
575
|
+
const { shownWhenOf, visibilityControlsOf } = await import('../client/modeller/links.ts')
|
|
576
|
+
const { editFigurePart } = await import('@hatiolab/figure-model')
|
|
577
|
+
let asset = createFigureAsset('Gate')
|
|
578
|
+
asset = addFigureBox(asset, { id: 'frame', size: { x: 1200, y: 60, z: 200 }, position: { x: 0, y: 30, z: 0 } })
|
|
579
|
+
asset = addFigureBox(asset, { id: 'door', size: { x: 1000, y: 2000, z: 40 }, position: { x: 0, y: 1060, z: 0 } })
|
|
580
|
+
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', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
582
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'door', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
583
583
|
assert.deepEqual(visibilityControlsOf(asset), [], '조작값이 없으면 고를 것도 없다')
|
|
584
584
|
asset = moving(asset, 'door', { kind: 'slide', axis: 'x', id: 'gate-open', min: 0, max: 900 })
|
|
585
585
|
assert.deepEqual(visibilityControlsOf(asset).map(c => [c.id, c.min, c.max, c.unit]), [['gate-open', 0, 900, 'mm']])
|
|
586
586
|
assert.equal(shownWhenOf(asset, 'lamp'), null, '늘 보인다')
|
|
587
|
-
const shown =
|
|
587
|
+
const shown = editFigurePart(asset, { kind: 'set-visibility', target: 'lamp', shownWhen: { input: 'gate-open', atLeast: 9 } })
|
|
588
588
|
assert.deepEqual(shownWhenOf(shown, 'lamp'), { input: 'gate-open', atLeast: 9 })
|
|
589
|
-
assert.equal(shownWhenOf(
|
|
590
|
-
const form = read('../client/modeller/figure-
|
|
589
|
+
assert.equal(shownWhenOf(editFigurePart(shown, { kind: 'set-visibility', target: 'lamp', shownWhen: null }), 'lamp'), null)
|
|
590
|
+
const form = read('../client/modeller/figure-links.ts')
|
|
591
591
|
const section = form.slice(form.indexOf(' private renderShown('), form.indexOf(' private renderMotion('))
|
|
592
592
|
assert.ok(section.includes("kind: 'set-visibility'"), '폼은 그 명령을 낸다')
|
|
593
593
|
assert.ok(form.includes('${this.renderShown(asset)}'), '움직임 칸에')
|
|
@@ -595,10 +595,10 @@ test('보이는 때 — 명령이 남긴 것을 읽고, 폼은 그 명령을 낸
|
|
|
595
595
|
|
|
596
596
|
/* 문틀은 폭 · 높이 · 깊이로 — 상자처럼 크기를 따르고, 손잡이로 키운다 (figure-model portal-profile@2, 09-26). */
|
|
597
597
|
test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따를 수 있고, 손잡이로 키울 수 있다', async () => {
|
|
598
|
-
const { paletteRecipe } = await import('../client/modeller/
|
|
599
|
-
const { gizmoReachOf } = await import('../client/modeller/
|
|
600
|
-
const {
|
|
601
|
-
const asset = add(
|
|
598
|
+
const { paletteRecipe } = await import('../client/modeller/palette.ts')
|
|
599
|
+
const { gizmoReachOf } = await import('../client/modeller/gizmo.ts')
|
|
600
|
+
const { addFigurePrimitive: add } = await import('@hatiolab/figure-model')
|
|
601
|
+
const asset = add(createFigureAsset('Frame'), paletteRecipe('portal', 'frame'))
|
|
602
602
|
assert.deepEqual(followableAxes(asset, 'frame'), ['x', 'y', 'z'])
|
|
603
603
|
const reach = gizmoReachOf(asset as never, 'frame')
|
|
604
604
|
assert.equal(reach.scale, true)
|
|
@@ -608,11 +608,11 @@ test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따
|
|
|
608
608
|
|
|
609
609
|
/* 크기를 따라감 한 번 — 치수 칸의 단추 하나, 크기를 대신 정했으면 한 줄로 말한다 (ADR 판정 Q1, 09-26). */
|
|
610
610
|
test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
|
|
611
|
-
const form = read('../client/modeller/figure-
|
|
611
|
+
const form = read('../client/modeller/figure-links.ts')
|
|
612
612
|
const dims = form.slice(form.indexOf(' private renderDimensions('), form.indexOf(' private renderDimensions(') + 1200)
|
|
613
613
|
assert.ok(dims.includes("this.command({ kind: 'follow-size', part: this.partId })"))
|
|
614
614
|
assert.ok(dims.includes("followOf(asset, this.partId, axis).of === 'instance'"), '다 따르면 단추가 없다')
|
|
615
|
-
const editor = read('../client/modeller/
|
|
615
|
+
const editor = read('../client/modeller/editor.ts')
|
|
616
616
|
assert.ok(editor.includes("if (action.kind === 'follow-size' && !hadSize)"), '크기를 대신 정했을 때만')
|
|
617
617
|
assert.ok(editor.includes('<div role="status" class="notice">'))
|
|
618
618
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
@@ -621,10 +621,10 @@ test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한
|
|
|
621
621
|
|
|
622
622
|
/* 통 — 속 빈 상자를 팔레트에서, 세 축 모두 크기를 따르고, 손잡이로 키우고, 안쪽 면에 붙인다 (G4, 09-26). */
|
|
623
623
|
test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따를 수 있고, 손잡이로 키울 수 있다', async () => {
|
|
624
|
-
const { paletteRecipe } = await import('../client/modeller/
|
|
625
|
-
const { gizmoReachOf } = await import('../client/modeller/
|
|
626
|
-
const {
|
|
627
|
-
const asset = add(
|
|
624
|
+
const { paletteRecipe } = await import('../client/modeller/palette.ts')
|
|
625
|
+
const { gizmoReachOf } = await import('../client/modeller/gizmo.ts')
|
|
626
|
+
const { addFigurePrimitive: add } = await import('@hatiolab/figure-model')
|
|
627
|
+
const asset = add(createFigureAsset('Tote'), paletteRecipe('hollow-box' as never, 'tote'))
|
|
628
628
|
assert.deepEqual(followableAxes(asset, 'tote'), ['x', 'y', 'z'])
|
|
629
629
|
assert.deepEqual(gizmoReachOf(asset as never, 'tote').sizes, ['tote.dimension.width', 'tote.dimension.height', 'tote.dimension.depth'])
|
|
630
630
|
const ko = JSON.parse(read('../translations/ko.json'))
|
|
@@ -635,13 +635,13 @@ test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따
|
|
|
635
635
|
* The fastening form opens on the part's own fastening (walk 1, 2026-09-26): a roller fastened by its centre was shown
|
|
636
636
|
* as fastened by its bottom, and fastening again from there broke its turning in place.
|
|
637
637
|
*/
|
|
638
|
-
import { fasteningOf } from '../client/modeller/
|
|
638
|
+
import { fasteningOf } from '../client/modeller/links.ts'
|
|
639
639
|
|
|
640
640
|
test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은 롤러는 가운데 · 상대 · 간격 그대로', () => {
|
|
641
|
-
let asset =
|
|
642
|
-
asset =
|
|
643
|
-
asset =
|
|
644
|
-
asset =
|
|
641
|
+
let asset = addFigureBox(createFigureAsset('Rollers'), { id: 'bed', size: { x: 2000, y: 400, z: 600 }, position: { x: 0, y: 200, z: 0 } })
|
|
642
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
643
|
+
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 } })
|
|
644
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
|
|
645
645
|
const roller = fasteningOf(asset, 'roller')!
|
|
646
646
|
assert.equal(roller.face, 'centre')
|
|
647
647
|
assert.equal(roller.target, 'bed')
|
|
@@ -650,8 +650,8 @@ test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은
|
|
|
650
650
|
assert.equal(roller.align.x, 'min', '기록의 정렬')
|
|
651
651
|
const bed = fasteningOf(asset, 'bed')!
|
|
652
652
|
assert.deepEqual([bed.face, bed.toPlane], ['bottom', true])
|
|
653
|
-
assert.equal(fasteningOf(
|
|
654
|
-
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-
|
|
653
|
+
assert.equal(fasteningOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } }), 'b'), null, '안 붙은 부품은 기본값으로')
|
|
654
|
+
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-links.ts', import.meta.url)), 'utf8')
|
|
655
655
|
assert.ok(panel.includes('fasteningOf(this.asset, this.partId)'), '부품을 고르면 제 붙임으로 연다')
|
|
656
656
|
assert.ok(panel.includes("...(f.toPlane ? [] : (['centre'] as const))"), '내 면에 가운데가 있다')
|
|
657
657
|
})
|