@things-factory/figure-ui 10.1.129 → 10.1.141
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/align.ts +24 -18
- package/client/modeller/editor.ts +165 -253
- package/client/modeller/fasten-geometry.ts +12 -9
- package/client/modeller/figure-facts.ts +12 -14
- package/client/modeller/figure-links.ts +16 -12
- package/client/modeller/figure-preview.ts +13 -4
- package/client/modeller/figure-properties.ts +6 -5
- package/client/modeller/figure-verdict.ts +14 -8
- package/client/modeller/joint-fan.ts +15 -17
- package/client/modeller/links.ts +17 -23
- package/client/modeller/live.ts +6 -3
- package/client/modeller/motion-ends.ts +6 -7
- package/client/modeller/occupancy.ts +2 -3
- package/client/modeller/product-session.ts +15 -0
- package/client/modeller/revision.ts +66 -0
- package/client/pages/figure-modeller-page.ts +5 -18
- package/dist-client/modeller/align.d.ts +12 -7
- package/dist-client/modeller/align.js +21 -17
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +8 -34
- package/dist-client/modeller/editor.js +175 -250
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +11 -9
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-facts.d.ts +9 -1
- package/dist-client/modeller/figure-facts.js +11 -15
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -5
- package/dist-client/modeller/figure-links.js +14 -13
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +5 -2
- package/dist-client/modeller/figure-preview.js +13 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +3 -2
- package/dist-client/modeller/figure-properties.js +6 -6
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +4 -2
- package/dist-client/modeller/figure-verdict.js +12 -9
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/joint-fan.d.ts +8 -4
- package/dist-client/modeller/joint-fan.js +15 -16
- package/dist-client/modeller/joint-fan.js.map +1 -1
- package/dist-client/modeller/links.d.ts +6 -5
- package/dist-client/modeller/links.js +17 -24
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/live.d.ts +3 -2
- package/dist-client/modeller/live.js +7 -3
- package/dist-client/modeller/live.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +2 -2
- package/dist-client/modeller/motion-ends.js +5 -6
- package/dist-client/modeller/motion-ends.js.map +1 -1
- package/dist-client/modeller/occupancy.d.ts +2 -2
- package/dist-client/modeller/occupancy.js +2 -3
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +44 -39
- package/dist-client/modeller/product-session.js +17 -0
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/revision.d.ts +37 -0
- package/dist-client/modeller/revision.js +39 -0
- package/dist-client/modeller/revision.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +0 -6
- package/dist-client/pages/figure-modeller-page.js +6 -14
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +14 -12
- package/test/browser/modeller-page.js +52 -4
- package/test/browser/modeller-revision.check.mjs +144 -0
- package/test/command-preview.test.ts +9 -8
- package/test/fasten-drawing.test.ts +1 -1
- package/test/fasten-geometry.test.ts +10 -9
- package/test/fields.test.ts +1 -1
- package/test/first-minute.test.ts +1 -1
- package/test/gizmo.test.ts +1 -1
- package/test/joint-fan.test.ts +30 -14
- package/test/joint-frames.test.ts +9 -8
- package/test/links.test.ts +13 -12
- package/test/live.test.ts +3 -3
- package/test/parts-wiring.test.ts +1 -1
- package/test/properties.test.ts +6 -5
- package/test/release-panel.test.ts +3 -3
- package/test/resize-with-placement.test.ts +8 -6
- package/test/revision-of.ts +22 -0
- package/test/revision.test.ts +172 -0
- package/test/section-editor.test.ts +4 -3
- package/test/section-panel.test.ts +5 -4
- package/test/verdict.test.ts +1 -1
- package/client/modeller/evaluated.ts +0 -47
- package/dist-client/modeller/evaluated.d.ts +0 -12
- package/dist-client/modeller/evaluated.js +0 -42
- package/dist-client/modeller/evaluated.js.map +0 -1
- package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
- package/test/evaluated.test.ts +0 -40
- package/test/mode-switch.test.ts +0 -43
|
@@ -13,6 +13,7 @@ import { jointAxisUnit, jointAxisWord } from '../client/modeller/joint-axis.ts'
|
|
|
13
13
|
import { alignOf, endPlaceKeyOf, fasteningOf, fixedJointOf, motionFormOf, motionFormRefusalOf, motionJoint, motionOf, rowAxesOf, rowBlockOf } from '../client/modeller/links.ts'
|
|
14
14
|
import { jointDragOf } from '../client/modeller/joint-drag.ts'
|
|
15
15
|
import { jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
16
|
+
import { revisionOf } from './revision-of.ts'
|
|
16
17
|
|
|
17
18
|
type Dynamic = any
|
|
18
19
|
const set = (asset: FigureAsset, joint: Dynamic) => applyFigureAuthoring(asset, { kind: 'set-joint', joint } as FigureAuthoringAction)
|
|
@@ -71,7 +72,7 @@ const frameOf = (j: Dynamic, keepType = true) => ({
|
|
|
71
72
|
|
|
72
73
|
/** The fastening form's 붙이기, as figure-links builds it from what `fasteningOf` reads; nothing where it writes none. */
|
|
73
74
|
function fasteningSave(asset: FigureAsset, part: string): FigureAuthoringAction | null {
|
|
74
|
-
const f = fasteningOf(asset, part) as Dynamic
|
|
75
|
+
const f = fasteningOf(revisionOf(asset), part) as Dynamic
|
|
75
76
|
if (!f || f.atOrigin) return null
|
|
76
77
|
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: f.targetFace }
|
|
77
78
|
const align = alignOf(asset, part, aim, f.align, f.alignMm, !!f.twist, f.alignInput)
|
|
@@ -200,10 +201,10 @@ test('placed by an origin: said as the origin, no row offered, and the fastening
|
|
|
200
201
|
const asset = framed()
|
|
201
202
|
assert.equal(endPlaceKeyOf(jointOf(asset, 'tool').body0), 'figure.label.joint-origin')
|
|
202
203
|
assert.equal(endPlaceKeyOf(jointOf(asset, 'link').body0), 'figure.label.face-top')
|
|
203
|
-
assert.equal((fasteningOf(asset, 'tool') as Dynamic).atOrigin, true)
|
|
204
|
-
assert.equal((fasteningOf(asset, 'cap') as Dynamic).atOrigin, true)
|
|
205
|
-
assert.equal((fasteningOf(asset, 'cap') as Dynamic).face, 'centre')
|
|
206
|
-
assert.equal((fasteningOf(asset, 'link') as Dynamic).atOrigin, undefined)
|
|
204
|
+
assert.equal((fasteningOf(revisionOf(asset), 'tool') as Dynamic).atOrigin, true)
|
|
205
|
+
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).atOrigin, true)
|
|
206
|
+
assert.equal((fasteningOf(revisionOf(asset), 'cap') as Dynamic).face, 'centre')
|
|
207
|
+
assert.equal((fasteningOf(revisionOf(asset), 'link') as Dynamic).atOrigin, undefined)
|
|
207
208
|
assert.equal(fasteningSave(asset, 'tool'), null)
|
|
208
209
|
assert.deepEqual(rowAxesOf(asset, 'cap'), [])
|
|
209
210
|
assert.equal(rowBlockOf(asset, 'cap'), 'at-origin')
|
|
@@ -265,12 +266,12 @@ test('a fan stands about a vector axis, as it does about the same token', frames
|
|
|
265
266
|
a = box(a, 'arm', 400, 50, 50, 125)
|
|
266
267
|
a = set(a, { id: 'base', type: 'fixed', body0: { plane: 'mounting-plane' }, body1: { part: 'base', face: 'bottom' } })
|
|
267
268
|
const arm = (axis: unknown) => set(a, { id: 'arm', type: 'revolute', body0: { part: 'base', face: 'top' }, body1: { part: 'arm', face: 'bottom' }, axis, lowerLimit: -45, upperLimit: 90 })
|
|
268
|
-
const token = jointFanOf(arm('Y'), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
269
|
-
const vector = jointFanOf(arm({ x: 0, y: 2, z: 0 }), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
269
|
+
const token = jointFanOf(revisionOf(arm('Y')), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
270
|
+
const vector = jointFanOf(revisionOf(arm({ x: 0, y: 2, z: 0 })), 'arm') as never as { pose: { t: number[]; r: number[][] } }
|
|
270
271
|
assert.ok(token && vector, 'a fan for both')
|
|
271
272
|
const off = (p: number[][], q: number[][]) => Math.max(...p.flatMap((row, i) => row.map((v, j) => Math.abs(v - q[i]![j]!))))
|
|
272
273
|
assert.ok(off(token.pose.r, vector.pose.r) < 1e-9 && off([token.pose.t], [vector.pose.t]) < 1e-9)
|
|
273
|
-
const tilted = jointFanOf(arm({ x: 0, y: 3, z: 4 }), 'arm') as never as { pose: { r: number[][] } }
|
|
274
|
+
const tilted = jointFanOf(revisionOf(arm({ x: 0, y: 3, z: 4 })), 'arm') as never as { pose: { r: number[][] } }
|
|
274
275
|
assert.ok(tilted, 'a tilted axis has a fan too')
|
|
275
276
|
/* The fan lies in its local X–Z: its local Y is the axis. */
|
|
276
277
|
const normal = [0, 1, 2].map(i => tilted.pose.r[i]![1]!)
|
package/test/links.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { addFigureBox, addFigurePrimitive, applyFigureAuthoring, createFigureAss
|
|
|
11
11
|
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
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
|
+
import { revisionOf } from './revision-of.ts'
|
|
14
15
|
|
|
15
16
|
const pair = (): FigureAsset => {
|
|
16
17
|
let asset = createFigureAsset('Pair')
|
|
@@ -91,7 +92,7 @@ test('편집기는 세 칸의 명령을 세션의 이력 하나로 돌리고,
|
|
|
91
92
|
test('떼면 지금 크기로 고정한다 — 따르던 값을 잃지 않는다', () => {
|
|
92
93
|
const links = read('../client/modeller/figure-links.ts')
|
|
93
94
|
assert.ok(links.includes("this.command({ kind: 'unlink-dimension', part: this.partId, axis, mm: Math.max(1, Math.round(current)) })"))
|
|
94
|
-
assert.ok(links.includes('const current = dimensionNow(
|
|
95
|
+
assert.ok(links.includes('const current = dimensionNow(this.revision!, this.partId, axis) ?? boxOf(this.revision!, this.partId)?.dims[AXES.indexOf(axis)] ?? 100'))
|
|
95
96
|
})
|
|
96
97
|
|
|
97
98
|
test('세 칸의 말은 다섯 말 모두에 있다', () => {
|
|
@@ -170,8 +171,8 @@ const sizedPair = () => {
|
|
|
170
171
|
|
|
171
172
|
test('비례 — 인스턴스 크기의 몫을 따르는 치수는 비례로 읽고, 「남은 만큼 채우기」는 비례가 아니다', () => {
|
|
172
173
|
const asset = sizedPair()
|
|
173
|
-
assert.equal(dimensionNow(asset, 'flange', 'y'), 10, '원기둥의 Y 는 길이')
|
|
174
|
-
assert.equal(dimensionNow(asset, 'flange', 'x'), 20, '원기둥의 X 는 반지름')
|
|
174
|
+
assert.equal(dimensionNow(revisionOf(asset), 'flange', 'y'), 10, '원기둥의 Y 는 길이')
|
|
175
|
+
assert.equal(dimensionNow(revisionOf(asset), 'flange', 'x'), 20, '원기둥의 X 는 반지름')
|
|
175
176
|
const share = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'flange', axis: 'y', source: { of: 'instance', axis: 'y' }, times: 0.1 })
|
|
176
177
|
assert.deepEqual(followOf(share, 'flange', 'y'), { of: 'instance', axis: 'y', times: 0.1 })
|
|
177
178
|
|
|
@@ -259,10 +260,10 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
|
|
|
259
260
|
asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...lamp, type: 'prismatic', axis: 'Y', lowerLimit: 0, upperLimit: 50, state: { id: 'rise' } } })
|
|
260
261
|
asset = applyFigureAuthoring(asset, { kind: 'follow-size-in-proportion' })
|
|
261
262
|
assert.deepEqual(proportionToFollow(asset), { count: 0, kept: ['lamp.motion'] }, '다 따르고, mm 로 움직이는 것만 남는다')
|
|
262
|
-
assert.equal(Math.round(valueNow(asset, 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
|
|
263
|
+
assert.equal(Math.round(valueNow(revisionOf(asset), 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
|
|
263
264
|
const editor = read('../client/modeller/editor.ts')
|
|
264
265
|
assert.ok(editor.includes("this.linkCommand({ kind: 'follow-size-in-proportion' })"))
|
|
265
|
-
assert.ok(editor.includes("?? valueNow(
|
|
266
|
+
assert.ok(editor.includes("?? valueNow(shown, layout?.args[4])"), '길이를 따르는 간격도 숫자로 보인다')
|
|
266
267
|
})
|
|
267
268
|
|
|
268
269
|
test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
|
|
@@ -311,16 +312,16 @@ test('붙여도 제자리 — 폼은 지금 서 있는 자리를 가운데서
|
|
|
311
312
|
asset = addFigureBox(asset, { id: 'leaf', size: { x: 1400, y: 2200, z: 40 }, position: { x: 250, y: 1100, z: 100 } })
|
|
312
313
|
const where = (a: FigureAsset) => compileFigureAsset(a).evaluate().geometry.find(g => g.id === 'leaf')!.pose.t.map(v => Math.round(v))
|
|
313
314
|
for (const target of ['plane', { part: 'frame', face: 'top' }] as const) {
|
|
314
|
-
const kept = placeKeptOf(asset, 'leaf', target)
|
|
315
|
+
const kept = placeKeptOf(revisionOf(asset), 'leaf', target)
|
|
315
316
|
const align = alignOf(asset, 'leaf', target, kept.align, kept.alignMm)
|
|
316
317
|
const to = target === 'plane' ? { plane: 'mounting-plane' as const } : target
|
|
317
318
|
const fastened = applyFigureAuthoring(asset, { kind: 'attach', part: 'leaf', face: 'bottom', to, ...(align ? { align } : {}) } as never)
|
|
318
319
|
const [before, after] = [where(asset), where(fastened)]
|
|
319
320
|
assert.deepEqual([after[0], after[2]], [before[0], before[2]], `${JSON.stringify(target)}: 앞뒤·좌우는 그대로`)
|
|
320
321
|
}
|
|
321
|
-
assert.deepEqual(placeKeptOf(asset, 'leaf', { part: 'frame', face: 'top' }), { align: { x: 'mm', z: 'mm' }, alignMm: { x: -50, z: 100 }, alignInput: {} })
|
|
322
|
+
assert.deepEqual(placeKeptOf(revisionOf(asset), 'leaf', { part: 'frame', face: 'top' }), { align: { x: 'mm', z: 'mm' }, alignMm: { x: -50, z: 100 }, alignInput: {} })
|
|
322
323
|
const centred = addFigureBox(createFigureAsset('C'), { id: 'a', size: { x: 10, y: 10, z: 10 }, position: { x: 0, y: 5, z: 0 } })
|
|
323
|
-
assert.deepEqual(placeKeptOf(centred, 'a', 'plane'), { align: {}, alignMm: {}, alignInput: {} }, '가운데 있던 것은 가운데로')
|
|
324
|
+
assert.deepEqual(placeKeptOf(revisionOf(centred), 'a', 'plane'), { align: {}, alignMm: {}, alignInput: {} }, '가운데 있던 것은 가운데로')
|
|
324
325
|
|
|
325
326
|
/* The form fills it in whenever the aim changes, and does not offer a part that hangs from this one. */
|
|
326
327
|
const { hangsFrom } = await import('../client/modeller/links.ts')
|
|
@@ -648,17 +649,17 @@ test('붙이기 칸은 부품 제 붙임으로 열린다 — 가운데로 붙은
|
|
|
648
649
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'bed', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
649
650
|
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 } })
|
|
650
651
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'roller', face: 'centre', to: { part: 'bed', face: 'top' }, gap: 40, align: { x: 'min' } })
|
|
651
|
-
const roller = fasteningOf(asset, 'roller')!
|
|
652
|
+
const roller = fasteningOf(revisionOf(asset), 'roller')!
|
|
652
653
|
assert.equal(roller.face, 'centre')
|
|
653
654
|
assert.equal(roller.target, 'bed')
|
|
654
655
|
assert.equal(roller.targetFace, 'top')
|
|
655
656
|
assert.equal(roller.gap, 40)
|
|
656
657
|
assert.equal(roller.align.x, 'min', '기록의 정렬')
|
|
657
|
-
const bed = fasteningOf(asset, 'bed')!
|
|
658
|
+
const bed = fasteningOf(revisionOf(asset), 'bed')!
|
|
658
659
|
assert.deepEqual([bed.face, bed.toPlane], ['bottom', true])
|
|
659
|
-
assert.equal(fasteningOf(addFigureBox(createFigureAsset('Loose'), { id: 'b', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } }), 'b'), null, '안 붙은 부품은 기본값으로')
|
|
660
|
+
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, '안 붙은 부품은 기본값으로')
|
|
660
661
|
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-links.ts', import.meta.url)), 'utf8')
|
|
661
|
-
assert.ok(panel.includes('fasteningOf(this.
|
|
662
|
+
assert.ok(panel.includes('fasteningOf(this.revision, this.partId)'), '부품을 고르면 제 붙임으로 연다')
|
|
662
663
|
assert.ok(panel.includes("...(f.toPlane ? [] : (['centre'] as const))"), '내 면에 가운데가 있다')
|
|
663
664
|
})
|
|
664
665
|
|
package/test/live.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { addFigureBox, applyFigureAuthoring, compileFigureAsset, createFigureAss
|
|
|
11
11
|
|
|
12
12
|
import { liveOverridesOf, liveOverSession, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep } from '../client/modeller/live.ts'
|
|
13
13
|
import { moving } from './joints.ts'
|
|
14
|
+
import { revisionOf } from './revision-of.ts'
|
|
14
15
|
|
|
15
16
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
16
17
|
|
|
@@ -74,7 +75,7 @@ test('속성창 숫자 칸은 치는 동안 알리고, 이름표를 끌 수 있
|
|
|
74
75
|
assert.ok(editor.includes('@input=${(e: Event) => this.typing(field as never, e)}'))
|
|
75
76
|
assert.ok(editor.includes('@pointerdown=${(e: PointerEvent) => this.scrub(group.cells[axis] as never, e, unavailable)}'))
|
|
76
77
|
for (const event of ['@state-live=', '@states-live=', '@part-live=']) assert.ok(editor.includes(event), event)
|
|
77
|
-
assert.ok(editor.includes('liveOverSession(
|
|
78
|
+
assert.ok(editor.includes('liveOverSession(shown, this.live)'), '캔버스는 세션 위에 끄는 값을 그린다 (행동은 아래 시험)')
|
|
78
79
|
const run = editor.slice(editor.indexOf(' private run(action'), editor.indexOf(' private hold('))
|
|
79
80
|
assert.ok(run.includes('this.live = NOTHING_LIVE'), '기록하거나 거절되면 끄는 값을 버린다')
|
|
80
81
|
const material = read('../client/modeller/figure-material-panel.ts')
|
|
@@ -107,10 +108,9 @@ test('끄는 동안은 세션이 지금 든 자세 위에 그린다 — 다른
|
|
|
107
108
|
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 20, z: 20 }, position: { x: 400, y: 110, z: 0 } })
|
|
108
109
|
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'arm', face: 'bottom', to: { part: 'post', face: 'top' }, align: { x: 'min' } })
|
|
109
110
|
asset = moving(asset, 'arm', { kind: 'turn', axis: 'y', id: 'swing', min: -180, max: 180 })
|
|
110
|
-
const current = { ...asset.designInputs, ...asset.stateDefaults, swing: 90 }
|
|
111
111
|
const armAt = (parts: { id: string; pose: { t: number[] } }[]) => parts.find(p => p.id === 'arm')!.pose.t.map(v => Math.round(v))
|
|
112
112
|
const committed = armAt(compileFigureAsset({ ...asset, stateDefaults: { ...asset.stateDefaults, swing: 90 } }).evaluate().geometry as never)
|
|
113
|
-
const dragging = armAt(liveOverSession(asset,
|
|
113
|
+
const dragging = armAt(liveOverSession(revisionOf(asset, { swing: 90 }), { values: { shift: 30 } }) as never)
|
|
114
114
|
assert.deepEqual(dragging, committed, '사용자 2026-09-24: 「조작을 끝내면 완전히 다른 위치로 된다」 — swing 90 이 끄는 동안에도 남는다')
|
|
115
115
|
const overSaved = armAt(livePartsOf(asset, { values: { shift: 30 } }) as never)
|
|
116
116
|
assert.notDeepEqual(overSaved, committed, '전제: 저장된 기본값 위에 그리면 swing 이 0 으로 돌아간다')
|
|
@@ -29,7 +29,7 @@ test('복제 · 순서 · 삭제는 figure-model 의 부품 편집으로, 세션
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
test('작업대에서 가린 부품은 캔버스에서 빠지고, 다른 도형을 열면 풀린다', () => {
|
|
32
|
-
assert.ok(editor.includes('.parts=${this.drawnParts(view.geometry,
|
|
32
|
+
assert.ok(editor.includes('.parts=${this.drawnParts(view.geometry, live)}'))
|
|
33
33
|
const drawn = editor.slice(editor.indexOf(' private drawnParts('), editor.indexOf(' private hold('))
|
|
34
34
|
assert.ok(drawn.includes('parts.filter(body => !this.hiddenParts.has('), '끄는 값을 그릴 때도 가린 부품은 빠진다')
|
|
35
35
|
const reset = editor.slice(editor.indexOf('protected willUpdate'), editor.indexOf('private run('))
|
package/test/properties.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { addFigureBox, applyFigureAuthoring, createFigureAsset, inspectFigureAss
|
|
|
13
13
|
|
|
14
14
|
import { tagsOf, figureFigureFacts } from '../client/modeller/figure-facts.ts'
|
|
15
15
|
import { occupancyFromParts } from '../client/modeller/occupancy.ts'
|
|
16
|
+
import { revisionOf } from './revision-of.ts'
|
|
16
17
|
|
|
17
18
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
19
|
const probe = () => {
|
|
@@ -20,7 +21,7 @@ const probe = () => {
|
|
|
20
21
|
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
21
22
|
return asset
|
|
22
23
|
}
|
|
23
|
-
const facts = (asset: ReturnType<typeof probe>) => figureFigureFacts(
|
|
24
|
+
const facts = (asset: ReturnType<typeof probe>) => figureFigureFacts(revisionOf(asset))
|
|
24
25
|
|
|
25
26
|
test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
|
|
26
27
|
const read = facts(probe())
|
|
@@ -33,7 +34,7 @@ test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도
|
|
|
33
34
|
|
|
34
35
|
test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
|
|
35
36
|
const source = serializeFigureAsset(probe())
|
|
36
|
-
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source, 'ceiling'))
|
|
37
|
+
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(parseFigureAsset(source), 'ceiling'))
|
|
37
38
|
|
|
38
39
|
assert.equal(facts(declared).placement, 'ceiling')
|
|
39
40
|
assert.equal(facts(declared).declared, true)
|
|
@@ -42,7 +43,7 @@ test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판
|
|
|
42
43
|
|
|
43
44
|
test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
|
|
44
45
|
const source = serializeFigureAsset(probe())
|
|
45
|
-
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source))
|
|
46
|
+
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(parseFigureAsset(source)))
|
|
46
47
|
const mounted = applyFigureAuthoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
|
|
47
48
|
|
|
48
49
|
assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
|
|
@@ -84,7 +85,7 @@ test('발행한 뒤에도 행의 타입이 남는다 — 새로 짜면 잠긴
|
|
|
84
85
|
|
|
85
86
|
test('속성 탭의 말은 다섯 말 모두에 있다', () => {
|
|
86
87
|
const tab = read('../client/modeller/figure-properties.ts')
|
|
87
|
-
assert.ok(tab.includes('occupancyFromParts(this.
|
|
88
|
+
assert.ok(tab.includes('occupancyFromParts(this.revision.asset, placement)'), '고른 배치 기준으로 선언한다')
|
|
88
89
|
const keys = [...tab.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
89
90
|
for (const face of ['left', 'right', 'bottom', 'top', 'back', 'front']) keys.push(`figure.label.face-${face}`)
|
|
90
91
|
for (const one of ['floor', 'ceiling', 'center']) keys.push(`figure.label.placement-${one}`)
|
|
@@ -100,7 +101,7 @@ test('장착면은 도형이 평면에 닿는 부품으로 권한다 — 다리
|
|
|
100
101
|
let asset = fm.addFigureBox(fm.createFigureAsset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
|
|
101
102
|
asset = fm.addFigureBox(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
|
|
102
103
|
asset = fm.applyFigureAuthoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
|
|
103
|
-
const facts = figureFigureFacts(
|
|
104
|
+
const facts = figureFigureFacts(revisionOf(asset))
|
|
104
105
|
assert.deepEqual(facts.suggested, { part: 'leg', face: 'bottom' })
|
|
105
106
|
assert.equal(facts.parts[0], 'bed', '목록의 첫 부품은 바닥이다')
|
|
106
107
|
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
|
|
@@ -32,7 +32,7 @@ test('새로 만든 도형의 「공간 선언 없음」을 「부품에 맞추
|
|
|
32
32
|
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
33
33
|
assert.deepEqual(codes(asset), ['OCCUPANCY_UNDECLARED'], '전제: 선언 없는 도형은 막힌다')
|
|
34
34
|
|
|
35
|
-
const fitted = applyFigureAuthoring(asset, occupancyFromParts(
|
|
35
|
+
const fitted = applyFigureAuthoring(asset, occupancyFromParts(asset))
|
|
36
36
|
|
|
37
37
|
assert.deepEqual(codes(fitted), [])
|
|
38
38
|
assert.equal(fitted.placement ?? 'floor', 'floor', '처음 선언하는 도형은 바닥에 선다')
|
|
@@ -46,7 +46,7 @@ test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배
|
|
|
46
46
|
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
47
47
|
assert.ok(codes(asset).includes('PART_OUTSIDE_OCCUPANCY'), '전제: 넘은 부품은 막힌다')
|
|
48
48
|
|
|
49
|
-
const fitted = applyFigureAuthoring(asset, occupancyFromParts(
|
|
49
|
+
const fitted = applyFigureAuthoring(asset, occupancyFromParts(asset))
|
|
50
50
|
|
|
51
51
|
assert.deepEqual(codes(fitted), [])
|
|
52
52
|
assert.equal(fitted.placement, 'center', '이미 선언된 배치 기준을 바꾸지 않는다')
|
|
@@ -61,7 +61,7 @@ test('모델러가 발행 패널을 걸고, 패널이 내는 셋을 받는다',
|
|
|
61
61
|
for (const event of ['fit-base-to-parts', 'figure-released', 'figure-reverted']) {
|
|
62
62
|
assert.ok(page.includes(`@${event}=`), `패널이 내는 ${event} 를 받는 자리가 없다 — 눌러도 아무 일이 없다`)
|
|
63
63
|
}
|
|
64
|
-
assert.ok(/occupancyFromParts\(
|
|
64
|
+
assert.ok(/occupancyFromParts\(shown\.asset\)/.test(page), '「부품에 맞추기」가 공간 선언으로 이어진다')
|
|
65
65
|
/* 없으면 셸이 페이지를 내용 폭(편집기 660 + 패널 320)으로 세워 패널이 오른쪽 밖으로 나간다 — 945 px 에서 봤다. */
|
|
66
66
|
assert.ok(/:host \{[^}]*overflow: hidden/.test(page), '페이지가 주어진 폭 안에 든다')
|
|
67
67
|
})
|
|
@@ -7,6 +7,7 @@ import { createFigureProductSession } from '../client/modeller/product-session.j
|
|
|
7
7
|
import { resizeRefusalOf } from '../client/modeller/refusals.js'
|
|
8
8
|
import { resizeAxesOf } from '../client/modeller/links.js'
|
|
9
9
|
import { groupFields } from '../client/modeller/fields.js'
|
|
10
|
+
import { revisionOf } from './revision-of.ts'
|
|
10
11
|
|
|
11
12
|
/*
|
|
12
13
|
* ADR-0097: the modeller's "follow the size and keep where the figure is placed" is figure-model's plan, applied through
|
|
@@ -43,8 +44,9 @@ for (const mode of ['floor', 'ceiling', 'wall', 'center'] as const) {
|
|
|
43
44
|
await session.open('ux')
|
|
44
45
|
assert.equal(session.source(), after)
|
|
45
46
|
/* At twice the height the kept face is where it was. */
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/* A parsed asset is sealed (figure-model ADR-0119): the doubled height goes into a copy, which is compiled afresh. */
|
|
48
|
+
const parsed = parseFigureAsset(session.source())
|
|
49
|
+
const asset = { ...parsed, designInputs: { ...parsed.designInputs, 'size.y': parsed.designInputs['size.y']! * 2 } }
|
|
48
50
|
const g = compileFigureAsset(asset).evaluate().geometry.find(g => g.id === 'box')! as unknown as { pose: { t: number[] }; shape: { dimensions: Record<string, number> } }
|
|
49
51
|
const y = g.pose.t[1]!, h = g.shape.dimensions.length!
|
|
50
52
|
if (mode === 'floor') assert.equal(y - h / 2, 0)
|
|
@@ -144,7 +146,7 @@ test('the part component carries the face a size drag keeps, for each placement'
|
|
|
144
146
|
assert.equal(componentOf(body, {}, { width: 1000, height: 1000 })!.scaleAnchor, undefined, 'none given, none set')
|
|
145
147
|
}
|
|
146
148
|
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
147
|
-
assert.ok(editor.includes('.anchors=${this.anchorsOf(asset
|
|
149
|
+
assert.ok(editor.includes('.anchors=${this.anchorsOf(asset)}'))
|
|
148
150
|
})
|
|
149
151
|
|
|
150
152
|
/* Designer 2026-09-27 (acceptance round 2): what the screen shows for a part a rule places, and what it withholds. */
|
|
@@ -194,7 +196,7 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
|
|
|
194
196
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 20, y: 150, z: 30 } })
|
|
195
197
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
|
|
196
198
|
const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box', gap: 'keep' }).steps)
|
|
197
|
-
const form = fasteningOf(kept, 'box')!
|
|
199
|
+
const form = fasteningOf(revisionOf(kept), 'box')!
|
|
198
200
|
assert.equal(form.toPlane, true, 'on the plane')
|
|
199
201
|
assert.equal(form.face, 'bottom')
|
|
200
202
|
assert.equal(form.gap, 100, 'the gap it keeps')
|
|
@@ -202,7 +204,7 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
|
|
|
202
204
|
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
203
205
|
assert.ok(links.includes('@click=${() => this.openFastening(asset)}'))
|
|
204
206
|
const open = links.slice(links.indexOf(' private async openFastening('), links.indexOf(' /**\n * What holds the part, said as its state'))
|
|
205
|
-
assert.ok(open.includes('const own = fasteningOf(
|
|
207
|
+
assert.ok(open.includes('const own = fasteningOf(this.revision!, this.partId)'), 'the form is loaded from the fastening it has now')
|
|
206
208
|
assert.ok(open.includes("querySelector<HTMLElement>('section[fastening]')"), 'the fastening section is what comes into view')
|
|
207
209
|
assert.ok(!editor.includes("@open-fastening=${() => (this.tab = 'capability')}"), 'it does not switch to the capability tab')
|
|
208
210
|
assert.ok(links.includes('<section fastening ?spotlit=${this.spotlit}'))
|
|
@@ -217,7 +219,7 @@ test('the fastening form keeps a place that follows a position field, and fasten
|
|
|
217
219
|
let a = addFigureBox(createFigureAsset('UX'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 200, y: 50, z: 150 } })
|
|
218
220
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
|
|
219
221
|
const kept = applyFigureProposal(a, planFigureResize(a, { part: 'box' }).steps)
|
|
220
|
-
const form = fasteningOf(kept, 'box')!
|
|
222
|
+
const form = fasteningOf(revisionOf(kept), 'box')!
|
|
221
223
|
assert.deepEqual([form.align.x, form.alignInput.x, form.align.z, form.alignInput.z], ['input', 'box.position.x', 'input', 'box.position.z'])
|
|
222
224
|
const align = alignOf(kept, 'box', 'plane', form.align, form.alignMm, false, form.alignInput)
|
|
223
225
|
const again = applyFigureAuthoring(kept, { kind: 'attach', part: 'box', face: 'bottom', to: { plane: 'mounting-plane' }, ...(align ? { align } : {}), replace: true } as never)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A figure as the modeller holds it at one version: opened in an editor workspace, the values given set on it (a pose
|
|
3
|
+
* turned with the controls, a design value typed), and read as the product session reads it.
|
|
4
|
+
*/
|
|
5
|
+
import { createFigureEditorWorkspace, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
6
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
7
|
+
|
|
8
|
+
import { revisionFromView, type FigureRevision } from '../client/modeller/revision.ts'
|
|
9
|
+
|
|
10
|
+
export function revisionOf(asset: FigureAsset, values: Record<string, number> = {}): FigureRevision {
|
|
11
|
+
const w = createFigureEditorWorkspace()
|
|
12
|
+
const opened = w.open(serializeFigureAsset(asset))
|
|
13
|
+
const context = opened.context!
|
|
14
|
+
const changes = Object.entries(values).flatMap(([id, value]) => {
|
|
15
|
+
const field = opened.fields.find(f => f.id === id)
|
|
16
|
+
if (!field) throw Error(`no input ${id}`)
|
|
17
|
+
return field.value === value ? [] : [{ kind: field.role === 'state' ? ('set-state' as const) : ('set-design' as const), input: id, unit: field.unit, value }]
|
|
18
|
+
})
|
|
19
|
+
if (changes.length) w.apply({ context, origin: 'ui', expectedRevision: 0, changes })
|
|
20
|
+
const view = w.read() as Parameters<typeof revisionFromView>[0]
|
|
21
|
+
return revisionFromView(view, () => w.export({ context }).text)
|
|
22
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The figure on screen at one version (`revision.ts`): one object per version, from the product session, and everything
|
|
3
|
+
* the modeller works out from it kept with it. Its asset is the kernel's (ADR-0119 decision 6), so the screen's questions
|
|
4
|
+
* of it are answered without compiling or evaluating the figure again.
|
|
5
|
+
*
|
|
6
|
+
* The budgets are counts of whole-figure work (figure-model `measureFigureWork`), not times: the same on every machine.
|
|
7
|
+
* Selecting a part asks every question below; before the version was one object, a selection on the forklift compiled
|
|
8
|
+
* it twice and evaluated it three times on top of the session's own work (2026-10-10).
|
|
9
|
+
*/
|
|
10
|
+
import { test } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, figureFaceOverhangOf, figurePartFaceOf, figurePlaneAlignChanges, figureRoomOnlyInputsOf, figureRowOf, figureScaleAnchorOf, measureFigureWork, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
16
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
17
|
+
|
|
18
|
+
import { createFigureProductSession } from '../client/modeller/product-session.ts'
|
|
19
|
+
import { derivedHeld, derivedOf, type FigureRevision } from '../client/modeller/revision.ts'
|
|
20
|
+
import { alignChanges, boxOf, canMirror, gridStepOf } from '../client/modeller/align.ts'
|
|
21
|
+
import { jointFanOf } from '../client/modeller/joint-fan.ts'
|
|
22
|
+
import { fasteningOf, measureNow, partSummaryOf, placeKeptOf } from '../client/modeller/links.ts'
|
|
23
|
+
import { faceViewOf, myFaceFor } from '../client/modeller/fasten-geometry.ts'
|
|
24
|
+
import { motionEndsOf } from '../client/modeller/motion-ends.ts'
|
|
25
|
+
import { figureFigureFacts } from '../client/modeller/figure-facts.ts'
|
|
26
|
+
import { gizmoReachOf } from '../client/modeller/gizmo.ts'
|
|
27
|
+
import { moving } from './joints.ts'
|
|
28
|
+
import { revisionOf } from './revision-of.ts'
|
|
29
|
+
|
|
30
|
+
const arm = readFileSync(fileURLToPath(new URL('./fixtures/robot-arm-6dof.json', import.meta.url)), 'utf8')
|
|
31
|
+
|
|
32
|
+
async function opened(source: string) {
|
|
33
|
+
const session = createFigureProductSession({ load: async id => ({ id, source }), save: async request => ({ id: request.id ?? 'saved', source: request.source }) })
|
|
34
|
+
await session.open('probe')
|
|
35
|
+
return session
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('one version is one object, from the session; another version is another, and undo is another again', async () => {
|
|
39
|
+
const session = await opened(arm)
|
|
40
|
+
const first = session.revisionNow()!
|
|
41
|
+
assert.equal(session.revisionNow(), first)
|
|
42
|
+
assert.equal(first.asset, session.read().asset)
|
|
43
|
+
assert.ok(Object.isFrozen(first.asset))
|
|
44
|
+
assert.equal(first.text, session.source())
|
|
45
|
+
const field = first.fields.find(f => f.role !== 'state')!
|
|
46
|
+
session.apply([{ kind: 'set-design', input: field.id, unit: field.unit, value: field.value + 10 }])
|
|
47
|
+
const second = session.revisionNow()!
|
|
48
|
+
assert.notEqual(second, first)
|
|
49
|
+
assert.equal(second.inputs[field.id], field.value + 10)
|
|
50
|
+
session.undo()
|
|
51
|
+
assert.notEqual(session.revisionNow(), first, 'a version made again is a new object')
|
|
52
|
+
assert.equal(session.revisionNow()!.inputs[field.id], field.value)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('what is worked out from a version is kept with it, and goes with it', async () => {
|
|
56
|
+
const session = await opened(arm)
|
|
57
|
+
const first = session.revisionNow()!
|
|
58
|
+
let made = 0
|
|
59
|
+
const ask = (r: FigureRevision) => derivedOf(r, 'probe', () => ++made)
|
|
60
|
+
assert.equal(ask(first), 1)
|
|
61
|
+
assert.equal(ask(first), 1)
|
|
62
|
+
assert.equal(derivedHeld(first, 'probe'), 1)
|
|
63
|
+
const field = first.fields.find(f => f.role !== 'state')!
|
|
64
|
+
session.apply([{ kind: 'set-design', input: field.id, unit: field.unit, value: field.value + 10 }])
|
|
65
|
+
assert.equal(derivedHeld(session.revisionNow()!, 'probe'), undefined)
|
|
66
|
+
assert.equal(ask(session.revisionNow()!), 2)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('a version is readable while it is saved, and a save hands out the same text', async () => {
|
|
70
|
+
const session = await opened(arm)
|
|
71
|
+
const field = session.revisionNow()!.fields.find(f => f.role !== 'state')!
|
|
72
|
+
session.apply([{ kind: 'set-design', input: field.id, unit: field.unit, value: field.value + 10 }])
|
|
73
|
+
const before = session.revisionNow()!
|
|
74
|
+
const saving = session.save()
|
|
75
|
+
assert.ok(session.read().saving)
|
|
76
|
+
assert.equal(session.revisionNow(), before)
|
|
77
|
+
await saving
|
|
78
|
+
assert.equal(session.revisionNow()!.text, before.text)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/* Every question a selection asks of the version, as the editor and its panels ask them. */
|
|
82
|
+
function selectionQuestions(shown: FigureRevision, part: string) {
|
|
83
|
+
const asset = shown.asset
|
|
84
|
+
const reach = gizmoReachOf(asset as never, part)
|
|
85
|
+
figureScaleAnchorOf(asset, part)
|
|
86
|
+
boxOf(shown, part)
|
|
87
|
+
gridStepOf(shown)
|
|
88
|
+
jointFanOf(shown, part)
|
|
89
|
+
partSummaryOf(asset, part, key => key, 'ko')
|
|
90
|
+
figureRoomOnlyInputsOf(asset, part)
|
|
91
|
+
for (const axis of ['x', 'y', 'z'] as const) measureNow(shown, part, axis)
|
|
92
|
+
for (const axis of ['x', 'z'] as const) canMirror(shown, part, axis, reach)
|
|
93
|
+
figurePlaneAlignChanges(asset, part)
|
|
94
|
+
figureFaceOverhangOf(asset, part, shown.inputs)
|
|
95
|
+
figurePartFaceOf(asset, part, 'top', shown.inputs)
|
|
96
|
+
figureRowOf(asset, part, shown.inputs)
|
|
97
|
+
fasteningOf(shown, part)
|
|
98
|
+
placeKeptOf(shown, part, 'plane')
|
|
99
|
+
myFaceFor(shown, part, 'plane', 'meet')
|
|
100
|
+
faceViewOf(shown, part, 'plane')
|
|
101
|
+
figureFigureFacts(shown)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test('selecting a part compiles nothing and evaluates nothing: every question is answered from the version', async () => {
|
|
105
|
+
const session = await opened(arm)
|
|
106
|
+
const shown = session.revisionNow()!
|
|
107
|
+
const parts = shown.geometry.map(g => g.id).filter(id => !id.startsWith('['))
|
|
108
|
+
const work = measureFigureWork(() => {
|
|
109
|
+
for (const part of parts) selectionQuestions(session.revisionNow()!, part)
|
|
110
|
+
})
|
|
111
|
+
assert.deepEqual([work.compiles, work.evaluations], [0, 0], `${parts.length} parts`)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('the ends of a motion evaluate the figure at its two ends, and nothing is compiled', () => {
|
|
115
|
+
const shown = revisionOf(slide())
|
|
116
|
+
const work = measureFigureWork(() => motionEndsOf(shown, 'carriage'))
|
|
117
|
+
assert.ok(work.result, 'the carriage slides')
|
|
118
|
+
assert.deepEqual([work.compiles, work.evaluations], [0, 2])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/* A compile also evaluates the figure once (its stored values must run): three compiles, and the read's evaluation. */
|
|
122
|
+
test('a value changed, read again as the next version, is three compiles at most: the edit, its read and its asset', async () => {
|
|
123
|
+
const session = await opened(arm)
|
|
124
|
+
const field = session.revisionNow()!.fields.find(f => f.role !== 'state')!
|
|
125
|
+
const parts = session.revisionNow()!.geometry.map(g => g.id).filter(id => !id.startsWith('['))
|
|
126
|
+
const work = measureFigureWork(() => {
|
|
127
|
+
session.apply([{ kind: 'set-design', input: field.id, unit: field.unit, value: field.value + 10 }])
|
|
128
|
+
session.read()
|
|
129
|
+
for (const part of parts) selectionQuestions(session.revisionNow()!, part)
|
|
130
|
+
session.source()
|
|
131
|
+
})
|
|
132
|
+
assert.ok(work.compiles <= 3, `compiles ${work.compiles}`)
|
|
133
|
+
assert.ok(work.evaluations <= 4, `evaluations ${work.evaluations}`)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* The version is read as drawn: the pose set with the controls, which is also what a command sees (the workspace
|
|
138
|
+
* captures the live inputs before it runs one). Where the pose on screen differs from the saved one, fastening a part
|
|
139
|
+
* keeps it where it is drawn, and centring centres what is drawn (chief architect's ruling 2026-10-10).
|
|
140
|
+
*/
|
|
141
|
+
function slide(): FigureAsset {
|
|
142
|
+
let a = addFigureBox(createFigureAsset('Slide'), { id: 'rail', size: { x: 1000, y: 100, z: 200 }, position: { x: 0, y: 50, z: 0 } })
|
|
143
|
+
a = addFigureBox(a, { id: 'carriage', size: { x: 200, y: 100, z: 200 }, position: { x: 0, y: 150, z: 0 } })
|
|
144
|
+
a = applyFigureAuthoring(a, { kind: 'attach', part: 'carriage', face: 'bottom', to: { part: 'rail', face: 'top' } })
|
|
145
|
+
a = moving(a, 'carriage', { kind: 'slide', axis: 'x', id: 'travel', min: 0, max: 400 })
|
|
146
|
+
return addFigureBox(a, { id: 'tag', size: { x: 50, y: 50, z: 50 }, position: { x: 300, y: 225, z: 0 } })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test('fastening keeps a part where it stands against what is drawn: a target moved by its control is met where it is now', () => {
|
|
150
|
+
const kept = (shown: FigureRevision) => placeKeptOf(shown, 'tag', { part: 'carriage', face: 'top' }).alignMm.x
|
|
151
|
+
assert.equal(kept(revisionOf(slide())), 300)
|
|
152
|
+
assert.equal(kept(revisionOf(slide(), { travel: 250 })), 50, 'the carriage drawn 250 along: the tag is 50 past its centre')
|
|
153
|
+
/* What the attach command then does, from the version as drawn: the tag does not jump. */
|
|
154
|
+
const shown = revisionOf(slide(), { travel: 250 })
|
|
155
|
+
const w = createFigureProductSession({ load: async id => ({ id, source: serializeFigureAsset(slide()) }), save: async r => ({ id: 'x', source: r.source }) })
|
|
156
|
+
return w.open('x').then(() => {
|
|
157
|
+
w.apply([{ kind: 'set-state', input: 'travel', unit: 'mm', value: 250 }])
|
|
158
|
+
const before = boxOf(w.revisionNow()!, 'tag')!.centre.x
|
|
159
|
+
w.command({ kind: 'attach', part: 'tag', face: 'bottom', to: { part: 'carriage', face: 'top' }, align: { x: { mm: placeKeptOf(shown, 'tag', { part: 'carriage', face: 'top' }).alignMm.x! } } })
|
|
160
|
+
assert.equal(Math.round(boxOf(w.revisionNow()!, 'tag')!.centre.x), Math.round(before))
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('centring reads the box as drawn', () => {
|
|
165
|
+
const drawn = revisionOf(slide(), { travel: 250 })
|
|
166
|
+
const box = boxOf(drawn, 'carriage')!
|
|
167
|
+
assert.equal(Math.round(box.centre.x), 250)
|
|
168
|
+
assert.equal(Math.round(boxOf(revisionOf(slide()), 'carriage')!.centre.x), 0)
|
|
169
|
+
/* The tag stands on its own numbers; centring it goes by where it is drawn, which for it is the same either way. */
|
|
170
|
+
const changes = alignChanges(drawn, 'tag', gizmoReachOf(drawn.asset as never, 'tag'), 'centre-x')
|
|
171
|
+
assert.deepEqual(changes.map(c => [c.input, c.value]), [['tag.position.x', 0]])
|
|
172
|
+
})
|
|
@@ -14,6 +14,7 @@ import type { FigureAsset, FigureEvaluatedShape, FigureSectionPoint } from '@hat
|
|
|
14
14
|
|
|
15
15
|
import { apexOver, extentOf, sectionOf as sectionOfWorking, workingIssuesOf as issuesOfWorking, workingOf as workingOfShape, insertAfter, issueWords, issuesOf, markedBy, movePoint, nextPointId, pointLabel, removePoint } from '../client/modeller/section-drawing.ts'
|
|
16
16
|
import { livePartsOf } from '../client/modeller/live.ts'
|
|
17
|
+
import { revisionOf } from './revision-of.ts'
|
|
17
18
|
|
|
18
19
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
19
20
|
/* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
|
|
@@ -162,13 +163,13 @@ test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 —
|
|
|
162
163
|
const { liveOverSession } = await import('../client/modeller/live.ts')
|
|
163
164
|
const t = triangle()
|
|
164
165
|
const moved = pointsOf(t).map(p => (p.id === 'p3' ? { ...p, x: -30 } : p))
|
|
165
|
-
const drawn = liveOverSession(t, {
|
|
166
|
+
const drawn = liveOverSession(revisionOf(t), { values: {}, action: { kind: 'set-section', part: 'wedge', points: moved } })!
|
|
166
167
|
const wedge = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
|
|
167
168
|
assert.equal((wedge.shape.extrusion!.section as { points: FigureSectionPoint[] }).points.find(p => p.id === 'p3')!.x, -30)
|
|
168
169
|
/* Stage 1's corner, typed a second time: the radius drawn is the one typed, not the one applied before. */
|
|
169
170
|
const { addFigureBox } = await import('@hatiolab/figure-model')
|
|
170
171
|
const rounded = applyFigureAuthoring(addFigureBox(createFigureAsset('B'), { id: 'b', size: { x: 400, y: 100, z: 300 }, position: { x: 0, y: 50, z: 0 } }), { kind: 'set-section-corner', part: 'b', corner: 20 })
|
|
171
|
-
const again = liveOverSession(rounded, {
|
|
172
|
+
const again = liveOverSession(revisionOf(rounded), { values: {}, action: { kind: 'set-section-corner', part: 'b', corner: 80 } })!
|
|
172
173
|
const b = again.find(p => p.id === 'b') as unknown as { shape: FigureEvaluatedShape }
|
|
173
174
|
assert.deepEqual((b.shape.extrusion!.section as { corner: unknown }).corner, { x: 80, z: 80 })
|
|
174
175
|
})
|
|
@@ -327,7 +328,7 @@ test('편집 창에서 끝을 한 점으로: 켜면 점들의 가운데 위에
|
|
|
327
328
|
})
|
|
328
329
|
|
|
329
330
|
test('끝면에 붙은 부품이 있으면 한 점으로 모으기를 막고 그 면을 말한다 — 커널의 거절로 판정한다', () => {
|
|
330
|
-
has(editor, /
|
|
331
|
+
has(editor, /const apex = apexOver\(drawn\.section\.points, drawn\.axis\)[\s\S]*?session\.previewCommand\(\{ kind: 'set-section', part, apex \}\)/)
|
|
331
332
|
has(editor, /\.code === 'FEATURE_MAPPING_REQUIRED'\)/)
|
|
332
333
|
has(editorPanel, /const blocked = !apex && !this\.opened\?\.apex \? this\.pointBlocked : ''/)
|
|
333
334
|
has(editorPanel, /\?disabled=\$\{!!blocked\}/)
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../client/modeller/section-controls.ts'
|
|
25
25
|
import { livePartsOf } from '../client/modeller/live.ts'
|
|
26
26
|
import { editRefusalOf } from '../client/modeller/refusals.ts'
|
|
27
|
+
import { revisionOf } from './revision-of.ts'
|
|
27
28
|
|
|
28
29
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
29
30
|
/* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
|
|
@@ -171,10 +172,10 @@ test('치는 동안 그려진 도형이 칸의 글자를 덮어쓰지 않는다
|
|
|
171
172
|
test('면마다 벽 두께를 치는 동안에도 그려진다 — 명령이 없앤 입력(한 벽 두께)의 지금 값은 미리보기를 막지 않는다', async () => {
|
|
172
173
|
const { liveOverSession } = await import('../client/modeller/live.ts')
|
|
173
174
|
const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 30, start: { closed: 30 }, end: 'open' } })
|
|
174
|
-
/* The editor draws over every value as it stands: the one wall is among them. */
|
|
175
|
-
const
|
|
176
|
-
assert.ok('body.dimension.wall' in
|
|
177
|
-
const drawn = liveOverSession(
|
|
175
|
+
/* The editor draws over every value of the version as it stands: the one wall is among them. */
|
|
176
|
+
const shown = revisionOf(tray)
|
|
177
|
+
assert.ok('body.dimension.wall' in shown.inputs)
|
|
178
|
+
const drawn = liveOverSession(shown, {
|
|
178
179
|
values: {},
|
|
179
180
|
action: { kind: 'set-cavity', part: 'body', cavity: { wall: { 'x-': 100, 'x+': 30, 'z-': 30, 'z+': 30 }, start: { closed: 30 }, end: 'open' } }
|
|
180
181
|
})
|
package/test/verdict.test.ts
CHANGED
|
@@ -62,7 +62,7 @@ test('판정 탭은 발행 패널과 같은 두 판정에 「부품에 맞추기
|
|
|
62
62
|
|
|
63
63
|
test('판정과 이력은 편집기의 탭에 서고, 감추되 떼지 않는다', () => {
|
|
64
64
|
const editor = read('../client/modeller/editor.ts')
|
|
65
|
-
assert.ok(/<figure-verdict[\s\S]*?\.
|
|
65
|
+
assert.ok(/<figure-verdict[\s\S]*?\.revision=\$\{this\.session\.revisionNow\(\)\}/.test(page), '편집 중인 원본을 넘긴다')
|
|
66
66
|
assert.ok(/<figure-verdict[\s\S]*?\.figureType=/.test(page), '타입을 바꾸면 다시 판정한다')
|
|
67
67
|
assert.ok(/<figure-verdict\s+slot="verdict"/.test(page) && /<figure-history-panel\s+slot="history"/.test(page))
|
|
68
68
|
assert.ok(editor.includes(`<div panel flush ?off=\${tab !== 'verdict'}><slot name="verdict"></slot></div>`), '고르지 않은 탭은 감춘다 — 판정은 마지막 답을 지킨다')
|