@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,11 +1,11 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
4
|
+
import { createFigureProductSession, type FigureProductRecord } from '../client/modeller/product-session.js'
|
|
5
5
|
|
|
6
6
|
const source = (name = 'sample') =>
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
serializeFigureAsset(
|
|
8
|
+
addFigureBox(createFigureAsset(name), {
|
|
9
9
|
id: 'body',
|
|
10
10
|
size: { x: 100, y: 100, z: 100 },
|
|
11
11
|
position: { x: 0, y: 50, z: 0 }
|
|
@@ -25,7 +25,7 @@ const store = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
test('existing model -> new clears geometry, ID, history and derived state', async () => {
|
|
28
|
-
const s =
|
|
28
|
+
const s = createFigureProductSession(store)
|
|
29
29
|
await s.open('A')
|
|
30
30
|
assert.equal(s.read().geometry.length, 1)
|
|
31
31
|
const v = s.startNew()
|
|
@@ -33,18 +33,18 @@ test('existing model -> new clears geometry, ID, history and derived state', asy
|
|
|
33
33
|
assert.equal(v.geometry.length, 0)
|
|
34
34
|
assert.equal(v.canUndo, false)
|
|
35
35
|
assert.equal(v.dirty, false)
|
|
36
|
-
assert.equal(JSON.parse(s.source()).version, 'figure-
|
|
36
|
+
assert.equal(JSON.parse(s.source()).version, 'figure-asset-1')
|
|
37
37
|
})
|
|
38
38
|
test('late load cannot overwrite another model or a new figure', async () => {
|
|
39
|
-
const pending = deferred<
|
|
40
|
-
const s =
|
|
39
|
+
const pending = deferred<FigureProductRecord>()
|
|
40
|
+
const s = createFigureProductSession({ ...store, load: id => (id === 'A' ? pending.promise : store.load(id)) })
|
|
41
41
|
const first = s.open('A')
|
|
42
42
|
await s.open('B')
|
|
43
43
|
pending.resolve({ id: 'A', source: source('A') })
|
|
44
44
|
await first
|
|
45
45
|
assert.equal(s.read().id, 'B')
|
|
46
|
-
const p = deferred<
|
|
47
|
-
t =
|
|
46
|
+
const p = deferred<FigureProductRecord>(),
|
|
47
|
+
t = createFigureProductSession({ ...store, load: () => p.promise })
|
|
48
48
|
const loading = t.open('A')
|
|
49
49
|
t.startNew()
|
|
50
50
|
p.resolve({ id: 'A', source: source() })
|
|
@@ -53,7 +53,7 @@ test('late load cannot overwrite another model or a new figure', async () => {
|
|
|
53
53
|
assert.equal(t.read().geometry.length, 0)
|
|
54
54
|
})
|
|
55
55
|
test('failed load hides previous geometry and cannot save under the failed ID', async () => {
|
|
56
|
-
const s =
|
|
56
|
+
const s = createFigureProductSession({ ...store, load: async id => ({ id, source: id === 'bad' ? '{}' : source() }) })
|
|
57
57
|
await s.open('A')
|
|
58
58
|
await s.open('bad')
|
|
59
59
|
assert.equal(s.read().geometry.length, 0)
|
|
@@ -62,7 +62,7 @@ test('failed load hides previous geometry and cannot save under the failed ID',
|
|
|
62
62
|
await assert.rejects(s.save())
|
|
63
63
|
})
|
|
64
64
|
test('UI and AI use a shared revision and history; stale/cross-model AI is rejected', async () => {
|
|
65
|
-
const s =
|
|
65
|
+
const s = createFigureProductSession(store)
|
|
66
66
|
await s.open('A')
|
|
67
67
|
const before = s.read(),
|
|
68
68
|
changes = [{ kind: 'set-design' as const, input: 'body.size.y', unit: 'mm' as const, value: 150 }]
|
|
@@ -76,7 +76,7 @@ test('UI and AI use a shared revision and history; stale/cross-model AI is rejec
|
|
|
76
76
|
})
|
|
77
77
|
test('save carries exact baseline and acknowledges only matching response', async () => {
|
|
78
78
|
let baseline: string | undefined
|
|
79
|
-
const s =
|
|
79
|
+
const s = createFigureProductSession({
|
|
80
80
|
...store,
|
|
81
81
|
save: async q => {
|
|
82
82
|
baseline = q.expectedSource
|
|
@@ -93,8 +93,8 @@ test('save carries exact baseline and acknowledges only matching response', asyn
|
|
|
93
93
|
assert.equal(s.read().dirty, true)
|
|
94
94
|
})
|
|
95
95
|
test('pending save locks edits/navigation and duplicate create', async () => {
|
|
96
|
-
const p = deferred<
|
|
97
|
-
s =
|
|
96
|
+
const p = deferred<FigureProductRecord>(),
|
|
97
|
+
s = createFigureProductSession({ ...store, save: () => p.promise })
|
|
98
98
|
s.startNew()
|
|
99
99
|
const text = s.source(),
|
|
100
100
|
saving = s.save()
|
|
@@ -106,7 +106,7 @@ test('pending save locks edits/navigation and duplicate create', async () => {
|
|
|
106
106
|
assert.equal(s.read().id, 'new')
|
|
107
107
|
})
|
|
108
108
|
test('save failure preserves edits and allows retry; wrong acknowledgement never clears dirty', async () => {
|
|
109
|
-
const s =
|
|
109
|
+
const s = createFigureProductSession({ ...store, save: async () => ({ id: 'wrong', source: source() }) })
|
|
110
110
|
await s.open('A')
|
|
111
111
|
s.apply([{ kind: 'set-design', input: 'body.size.y', unit: 'mm', value: 200 }])
|
|
112
112
|
await assert.rejects(s.save())
|
|
@@ -116,7 +116,7 @@ test('save failure preserves edits and allows retry; wrong acknowledgement never
|
|
|
116
116
|
})
|
|
117
117
|
test('retry after a lost create response reuses the same creation identity', async () => {
|
|
118
118
|
const keys: string[] = []
|
|
119
|
-
const s =
|
|
119
|
+
const s = createFigureProductSession({
|
|
120
120
|
...store,
|
|
121
121
|
save: async q => {
|
|
122
122
|
keys.push(q.creationId!)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
4
|
+
import { prepareFigureWrite, compareAndSwapFigure } from '../../figure-service/server/service/figure/figure-write.js'
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite'
|
|
6
6
|
|
|
7
|
-
test('
|
|
8
|
-
const source =
|
|
9
|
-
const saved =
|
|
7
|
+
test('saves preserve the graph and clear unmeasured previous scores', () => {
|
|
8
|
+
const source = serializeFigureAsset(createFigureAsset('빈 자산'))
|
|
9
|
+
const saved = prepareFigureWrite(source)
|
|
10
10
|
assert.equal(saved.source, source)
|
|
11
11
|
assert.equal(saved.name, '빈 자산')
|
|
12
12
|
/* 점수는 저장할 때 재지 않으므로 비운다. */
|
|
@@ -16,26 +16,26 @@ test('V3 saves preserve the graph and clear unmeasured previous scores', () => {
|
|
|
16
16
|
assert.equal(saved.groups, 0)
|
|
17
17
|
assert.equal(saved.state, 'draft')
|
|
18
18
|
})
|
|
19
|
-
test('
|
|
20
|
-
const asset =
|
|
19
|
+
test('예산은 실제로 그리는 것을 센다 — 상자 하나는 삼각형 12 다', () => {
|
|
20
|
+
const asset = addFigureBox(createFigureAsset('상자'), {
|
|
21
21
|
id: 'body',
|
|
22
22
|
size: { x: 100, y: 100, z: 100 },
|
|
23
23
|
position: { x: 0, y: 50, z: 0 }
|
|
24
24
|
})
|
|
25
|
-
const saved =
|
|
25
|
+
const saved = prepareFigureWrite(serializeFigureAsset(asset))
|
|
26
26
|
assert.equal(saved.triangles, 12)
|
|
27
27
|
assert.equal(saved.groups, 1)
|
|
28
28
|
})
|
|
29
29
|
test('source concurrency check requires exact baseline; no implicit overwrite', () => {
|
|
30
|
-
const source =
|
|
31
|
-
assert.throws(() =>
|
|
32
|
-
assert.throws(() =>
|
|
33
|
-
assert.equal(
|
|
30
|
+
const source = serializeFigureAsset(createFigureAsset('A'))
|
|
31
|
+
assert.throws(() => prepareFigureWrite(source, { source }))
|
|
32
|
+
assert.throws(() => prepareFigureWrite(source, { source }, 'changed'))
|
|
33
|
+
assert.equal(prepareFigureWrite(source, { source }, source).source, source)
|
|
34
34
|
})
|
|
35
|
-
test('old/invalid/duplicate-key sources never become a
|
|
36
|
-
assert.throws(() =>
|
|
37
|
-
assert.throws(() =>
|
|
38
|
-
assert.throws(() =>
|
|
35
|
+
test('old/invalid/duplicate-key sources never become a saved draft', () => {
|
|
36
|
+
assert.throws(() => prepareFigureWrite('{"version":2,"parts":[]}'))
|
|
37
|
+
assert.throws(() => prepareFigureWrite('{"version":1,"version":2}'))
|
|
38
|
+
assert.throws(() => prepareFigureWrite('{'))
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
test('SQLite compare-and-swap rejects a stale editor and another tenant', async () => {
|
|
@@ -49,10 +49,10 @@ test('SQLite compare-and-swap rejects a stale editor and another tenant', async
|
|
|
49
49
|
.run('next', where.id, where.domain.id, where.source)
|
|
50
50
|
return { affected: Number(result.changes) }
|
|
51
51
|
}
|
|
52
|
-
await assert.rejects(
|
|
53
|
-
await
|
|
54
|
-
await assert.rejects(
|
|
55
|
-
await assert.rejects(
|
|
52
|
+
await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-B', 'original'))
|
|
53
|
+
await compareAndSwapFigure(update, 'A', 'tenant-A', 'original')
|
|
54
|
+
await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-A', 'original'))
|
|
55
|
+
await assert.rejects(compareAndSwapFigure(update, 'A', 'tenant-A', undefined))
|
|
56
56
|
assert.equal(db.prepare('SELECT source FROM figures WHERE id=?').get('A')!.source, 'next')
|
|
57
57
|
} finally {
|
|
58
58
|
db.close()
|
|
@@ -9,18 +9,18 @@ import assert from 'node:assert/strict'
|
|
|
9
9
|
import { readFileSync } from 'node:fs'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { addFigureBox, applyFigureAuthoring, createFigureAsset, inspectFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
|
|
14
|
-
import { tagsOf,
|
|
15
|
-
import { occupancyFromParts } from '../client/modeller/
|
|
14
|
+
import { tagsOf, figureFigureFacts } from '../client/modeller/figure-facts.ts'
|
|
15
|
+
import { occupancyFromParts } from '../client/modeller/occupancy.ts'
|
|
16
16
|
|
|
17
17
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
18
18
|
const probe = () => {
|
|
19
|
-
let asset =
|
|
20
|
-
asset =
|
|
19
|
+
let asset = createFigureAsset('Probe')
|
|
20
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
21
21
|
return asset
|
|
22
22
|
}
|
|
23
|
-
const facts = (asset: ReturnType<typeof probe>) =>
|
|
23
|
+
const facts = (asset: ReturnType<typeof probe>) => figureFigureFacts(serializeFigureAsset(asset))
|
|
24
24
|
|
|
25
25
|
test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도 「없음」이다', () => {
|
|
26
26
|
const read = facts(probe())
|
|
@@ -32,25 +32,25 @@ test('탭이 읽는 사실 — 선언 전에는 배치 기준도 장착면도
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('배치 기준을 고르면 그 기준으로 공간을 선언한다 — 판정도 풀린다', () => {
|
|
35
|
-
const source =
|
|
36
|
-
const declared =
|
|
35
|
+
const source = serializeFigureAsset(probe())
|
|
36
|
+
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source, 'ceiling'))
|
|
37
37
|
|
|
38
38
|
assert.equal(facts(declared).placement, 'ceiling')
|
|
39
39
|
assert.equal(facts(declared).declared, true)
|
|
40
|
-
assert.ok(!
|
|
40
|
+
assert.ok(!inspectFigureAsset(declared).violations.some(v => v.code === 'OCCUPANCY_UNDECLARED'))
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
test('장착면을 정하면 탭이 그 부품과 면을 다시 읽고, 해제하면 없어진다', () => {
|
|
44
|
-
const source =
|
|
45
|
-
const declared =
|
|
46
|
-
const mounted =
|
|
44
|
+
const source = serializeFigureAsset(probe())
|
|
45
|
+
const declared = applyFigureAuthoring(parseFigureAsset(source), occupancyFromParts(source))
|
|
46
|
+
const mounted = applyFigureAuthoring(declared, { kind: 'set-mounting-face', part: 'body', face: 'bottom' })
|
|
47
47
|
|
|
48
48
|
assert.deepEqual(facts(mounted).mounting, { part: 'body', face: 'bottom' })
|
|
49
|
-
assert.equal(facts(
|
|
49
|
+
assert.equal(facts(applyFigureAuthoring(mounted, { kind: 'clear-mounting-face' })).mounting, undefined)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
test('이름을 바꾸면 자산의 이름이 바뀐다 — 저장하면 목록 이름이 이것을 따른다', () => {
|
|
53
|
-
assert.equal(facts(
|
|
53
|
+
assert.equal(facts(applyFigureAuthoring(probe(), { kind: 'rename', name: 'Roller conveyor' })).name, 'Roller conveyor')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test('태그 줄은 쉼표로 나뉘어 목록이 된다', () => {
|
|
@@ -58,14 +58,14 @@ test('태그 줄은 쉼표로 나뉘어 목록이 된다', () => {
|
|
|
58
58
|
assert.deepEqual(tagsOf(''), [], '빈 줄은 전부 지우는 것이다')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
const page = read('../client/pages/figure-
|
|
61
|
+
const page = read('../client/pages/figure-modeller-page.ts')
|
|
62
62
|
|
|
63
63
|
test('화면이 속성 탭을 걸고, 명령은 세션의 이력 하나로, 분류 · 태그는 행으로 간다', () => {
|
|
64
|
-
const editor = read('../client/modeller/
|
|
64
|
+
const editor = read('../client/modeller/editor.ts')
|
|
65
65
|
assert.ok(/renderTab\('properties', 'tune'/.test(editor), '속성 탭은 편집기의 오른쪽 칸에')
|
|
66
|
-
assert.ok(/<figure-
|
|
66
|
+
assert.ok(/<figure-properties\s+slot="figure"/.test(page), '도형 속성은 부품을 고르지 않았을 때 속성 탭에 선다')
|
|
67
67
|
assert.ok(editor.includes('<slot name="figure"></slot>'))
|
|
68
|
-
assert.ok(/@
|
|
68
|
+
assert.ok(/@command=\$\{this\.onCommand\}/.test(page) && /@catalog=\$\{this\.onCatalog\}/.test(page))
|
|
69
69
|
const run = page.slice(page.indexOf('private runCommand'), page.indexOf('private onCatalog'))
|
|
70
70
|
assert.ok(/this\.session\.command\(actionOf\(\)\)/.test(run), '명령은 세션을 지난다 — 되돌리기가 먹는다')
|
|
71
71
|
const catalog = page.slice(page.indexOf('private onCatalog'), page.indexOf('private released'))
|
|
@@ -83,7 +83,7 @@ test('발행한 뒤에도 행의 타입이 남는다 — 새로 짜면 잠긴
|
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
test('속성 탭의 말은 다섯 말 모두에 있다', () => {
|
|
86
|
-
const tab = read('../client/modeller/figure-
|
|
86
|
+
const tab = read('../client/modeller/figure-properties.ts')
|
|
87
87
|
assert.ok(tab.includes('occupancyFromParts(this.source, placement)'), '고른 배치 기준으로 선언한다')
|
|
88
88
|
const keys = [...tab.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1])
|
|
89
89
|
for (const face of ['left', 'right', 'bottom', 'top', 'back', 'front']) keys.push(`figure.label.face-${face}`)
|
|
@@ -97,12 +97,12 @@ test('속성 탭의 말은 다섯 말 모두에 있다', () => {
|
|
|
97
97
|
/* Walk 1 (2026-09-26): the mounting face offered is where the figure meets its plane -- a conveyor's legs, not its bed. */
|
|
98
98
|
test('장착면은 도형이 평면에 닿는 부품으로 권한다 — 다리 위 바닥이 아니라 다리', async () => {
|
|
99
99
|
const fm = await import('@hatiolab/figure-model')
|
|
100
|
-
let asset = fm.
|
|
101
|
-
asset = fm.
|
|
102
|
-
asset = fm.
|
|
103
|
-
const facts =
|
|
100
|
+
let asset = fm.addFigureBox(fm.createFigureAsset('Conveyor'), { id: 'bed', size: { x: 3000, y: 700, z: 800 }, position: { x: 0, y: 1050, z: 0 } })
|
|
101
|
+
asset = fm.addFigureBox(asset, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: -1400, y: 350, z: -300 } })
|
|
102
|
+
asset = fm.applyFigureAuthoring(asset, { kind: 'attach', part: 'leg', face: 'top', to: { part: 'bed', face: 'bottom' }, align: { x: 'min', z: 'min' } })
|
|
103
|
+
const facts = figureFigureFacts(fm.serializeFigureAsset(asset))
|
|
104
104
|
assert.deepEqual(facts.suggested, { part: 'leg', face: 'bottom' })
|
|
105
105
|
assert.equal(facts.parts[0], 'bed', '목록의 첫 부품은 바닥이다')
|
|
106
|
-
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-
|
|
106
|
+
const panel = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
|
|
107
107
|
assert.ok(panel.includes('[this.pickedPart, mounting?.part, facts.suggested?.part, facts.parts[0]]'), '고른 것 · 정한 것 다음이 권하는 것')
|
|
108
108
|
})
|
|
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict'
|
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
|
|
10
|
-
import { recipeFieldOf, recipeReady, recipesMatching, recipeValueOf, recipeWord, recipeWorkings, templatesOf } from '../client/modeller/
|
|
10
|
+
import { recipeFieldOf, recipeReady, recipesMatching, recipeValueOf, recipeWord, recipeWorkings, templatesOf } from '../client/modeller/recipes.ts'
|
|
11
11
|
|
|
12
12
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
13
13
|
/* The product's templates, as the server reads them. */
|
|
@@ -62,22 +62,22 @@ test('작동 방식은 레시피에서 셈한다 — 미닫이 문: 두 문짝
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
test('모델러 — 부품 칸의 단추가 카탈로그를 열고, 넣으면 한 단계로 들어가고 창이 닫힌다; 목록은 페이지가 서버에서 한 번 읽는다', () => {
|
|
65
|
-
const parts = read('../client/modeller/figure-
|
|
65
|
+
const parts = read('../client/modeller/figure-parts.ts')
|
|
66
66
|
assert.ok(parts.includes("this.ask('open-recipe-catalog', {})"))
|
|
67
|
-
const editor = read('../client/modeller/
|
|
67
|
+
const editor = read('../client/modeller/editor.ts')
|
|
68
68
|
const insert = editor.slice(editor.indexOf(' private insertRecipe('), editor.indexOf(' /** The design values of the asset being edited. */'))
|
|
69
|
-
assert.ok(insert.includes('
|
|
69
|
+
assert.ok(insert.includes('figureRecipeIdFor(asset, checked)') && insert.includes('this.session!.applyRecipe(steps)') && insert.includes('this.catalogOpen = false'))
|
|
70
70
|
assert.ok(insert.includes('this.recipeError = recipeRefusalOf(error, (key, params) => i18next.t(key, params)) ?? this.said(error)'), '거절은 사람의 말로, 막힌 단계와 함께 — 커널의 영어가 아니라 (2026-09-27)')
|
|
71
|
-
const page = read('../client/pages/figure-
|
|
71
|
+
const page = read('../client/pages/figure-modeller-page.ts')
|
|
72
72
|
assert.ok(page.includes("availableRecipes(i18next.language || 'en')") && page.includes('.templates=${this.templates}'))
|
|
73
|
-
const catalog = read('../client/modeller/figure-
|
|
74
|
-
assert.ok(catalog.includes('
|
|
73
|
+
const catalog = read('../client/modeller/figure-recipe-catalog.ts')
|
|
74
|
+
assert.ok(catalog.includes('describeFigureRecipe(t.recipe)'), '부품 수는 셈한다')
|
|
75
75
|
assert.ok(catalog.includes("${this.guide ? html`<ox-markdown"), '설명이 없으면 아무것도 두지 않는다 — 「준비 중」 없음')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
test('넣기는 카탈로그에서 온다 — 편집기는 카탈로그 요소에서 insert-recipe 를 듣는다 (잘못 옮겨진 연결, 09-26)', () => {
|
|
79
|
-
const editor = read('../client/modeller/
|
|
80
|
-
const catalog = editor.slice(editor.indexOf('<figure-
|
|
79
|
+
const editor = read('../client/modeller/editor.ts')
|
|
80
|
+
const catalog = editor.slice(editor.indexOf('<figure-recipe-catalog'), editor.indexOf('</figure-recipe-catalog>'))
|
|
81
81
|
assert.ok(catalog.includes('@insert-recipe=${(e: CustomEvent) => this.insertRecipe(e.detail.recipe, e.detail.values)}'))
|
|
82
82
|
})
|
|
83
83
|
|
|
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict'
|
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
|
|
9
|
-
import { sizeRangeRefusalOf } from '../client/modeller/
|
|
9
|
+
import { sizeRangeRefusalOf } from '../client/modeller/refusals.ts'
|
|
10
10
|
|
|
11
11
|
const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
|
|
12
12
|
const say = (key: string, params: Record<string, string>) => ko[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
|
|
@@ -28,14 +28,14 @@ test('다른 거절은 건드리지 않는다', () => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀 다시 권한다 — 다시 재기로는 풀리지 않는다 (사이징, 2026-09-25)', async () => {
|
|
31
|
-
const { narrowingRetryOf } = await import('../client/modeller/
|
|
32
|
-
const {
|
|
33
|
-
let asset =
|
|
34
|
-
asset =
|
|
31
|
+
const { narrowingRetryOf } = await import('../client/modeller/refusals.ts')
|
|
32
|
+
const { addFigureBox, applyFigureAuthoring, createFigureAsset } = await import('@hatiolab/figure-model')
|
|
33
|
+
let asset = addFigureBox(createFigureAsset('Panel'), { id: 'panel', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 0, z: 0 } })
|
|
34
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 1, max: 4000 }, y: { min: 1, max: 4000 }, z: { min: 1, max: 4000 } }, at: { x: 400, y: 400, z: 400 } })
|
|
35
35
|
const link = { kind: 'link-dimension' as const, part: 'panel', axis: 'x' as const, source: { of: 'instance' as const, axis: 'x' as const }, plus: -50 }
|
|
36
36
|
let error: unknown
|
|
37
37
|
try {
|
|
38
|
-
|
|
38
|
+
applyFigureAuthoring(asset, link)
|
|
39
39
|
} catch (e) {
|
|
40
40
|
error = e
|
|
41
41
|
}
|
|
@@ -44,24 +44,24 @@ test('범위가 받을 수 없는 연결은 같은 연결을 범위를 좁혀
|
|
|
44
44
|
assert.ok(offer, '권한다')
|
|
45
45
|
assert.match(offer.label, /figure.button.narrow-range-from .*"axis":"X"/)
|
|
46
46
|
assert.deepEqual(offer.retry, { ...link, narrowRange: true })
|
|
47
|
-
assert.doesNotThrow(() =>
|
|
47
|
+
assert.doesNotThrow(() => applyFigureAuthoring(asset, offer.retry as never), '권한 것은 받아들여진다')
|
|
48
48
|
assert.equal(narrowingRetryOf({ kind: 'attach' }, error, say), null, '연결이 아닌 명령에는 권하지 않는다')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그 이름을 한 줄로 말한다; 남는 거절은 공간 선언과 줄 (아키텍트 판정 2026-09-27)', async () => {
|
|
52
|
-
const { partRefusalOf, removalNoticeOf, editRefusalOf } = await import('../client/modeller/
|
|
52
|
+
const { partRefusalOf, removalNoticeOf, editRefusalOf } = await import('../client/modeller/refusals.ts')
|
|
53
53
|
const fm = await import('@hatiolab/figure-model')
|
|
54
54
|
const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
55
|
-
let a = fm.
|
|
56
|
-
a = fm.
|
|
57
|
-
a = fm.
|
|
58
|
-
a = fm.
|
|
59
|
-
a = fm.
|
|
60
|
-
a = fm.
|
|
61
|
-
a = fm.
|
|
62
|
-
a = fm.
|
|
63
|
-
a = fm.
|
|
64
|
-
const turn = (asset: typeof a, part: string, extra: object) => fm.
|
|
55
|
+
let a = fm.createFigureAsset('Bench')
|
|
56
|
+
a = fm.addFigureBox(a, { id: 'leg', size: { x: 60, y: 700, z: 60 }, position: { x: 0, y: 0, z: 0 } })
|
|
57
|
+
a = fm.addFigureBox(a, { id: 'top', size: { x: 1200, y: 40, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
58
|
+
a = fm.addFigureBox(a, { id: 'lamp', size: { x: 80, y: 80, z: 80 }, position: { x: 0, y: 0, z: 0 } })
|
|
59
|
+
a = fm.addFigureBox(a, { id: 'shade', size: { x: 40, y: 40, z: 40 }, position: { x: 0, y: 0, z: 0 } })
|
|
60
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'leg', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
61
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'top', face: 'bottom', to: { part: 'leg', face: 'top' } })
|
|
62
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'lamp', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: -300 } } })
|
|
63
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'shade', face: 'bottom', to: { part: 'top', face: 'top' }, align: { x: { mm: 300 } } })
|
|
64
|
+
const turn = (asset: typeof a, part: string, extra: object) => fm.applyFigureAuthoring(asset, { kind: 'set-joint', joint: { ...asset.joints!.find(j => j.body1.part === part)!, type: 'revolute', axis: 'Y', ...extra } as never })
|
|
65
65
|
a = turn(a, 'lamp', { state: { id: 'lamp-turn' } })
|
|
66
66
|
a = turn(a, 'shade', { mimic: { joint: 'lamp', multiplier: 1 } })
|
|
67
67
|
|
|
@@ -73,10 +73,10 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
73
73
|
assert.match(lamp, /removal-own-control .*"part":"shade".*"control":"shade".*"min":"-180".*"max":"180"/, '따르던 것은 제 조작값을 받는다')
|
|
74
74
|
assert.equal(removalNoticeOf(fm.removePart(a, { kind: 'remove-part', target: 'shade' }).notice, say), null, '아무것도 풀지 않았으면 말하지 않는다')
|
|
75
75
|
|
|
76
|
-
const declared = fm.
|
|
76
|
+
const declared = fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
|
|
77
77
|
const why = (asset: typeof a, part: string) => {
|
|
78
78
|
try {
|
|
79
|
-
fm.
|
|
79
|
+
fm.editFigurePart(asset, { kind: 'remove-part', target: part })
|
|
80
80
|
return 'removed'
|
|
81
81
|
} catch (e) {
|
|
82
82
|
return partRefusalOf(e, part, say)
|
|
@@ -84,12 +84,12 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
84
84
|
}
|
|
85
85
|
assert.match(why(declared, 'shade')!, /part-in-declared-space .*"part":"shade"/, '잎인데 공간 선언에 들어 있다')
|
|
86
86
|
|
|
87
|
-
let row = fm.
|
|
88
|
-
row = fm.
|
|
89
|
-
row = fm.
|
|
90
|
-
row = fm.
|
|
91
|
-
row = fm.
|
|
92
|
-
row = fm.
|
|
87
|
+
let row = fm.createFigureAsset('Conveyor')
|
|
88
|
+
row = fm.addFigureBox(row, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
|
|
89
|
+
row = fm.addFigureBox(row, { id: 'roller', size: { x: 60, y: 60, z: 560 }, position: { x: 0, y: 0, z: 0 } })
|
|
90
|
+
row = fm.applyFigureAuthoring(row, { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
91
|
+
row = fm.applyFigureAuthoring(row, { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } })
|
|
92
|
+
row = fm.applyFigureAuthoring(row, { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 200 })
|
|
93
93
|
assert.match(why(row, 'frame')!, /part-row-along .*"part":"frame".*"rows":"roller"/, '줄이 따라 놓인 부품')
|
|
94
94
|
|
|
95
95
|
/* A refusal no sentence knows is still said as a refusal, with its code -- never the kernel's English. */
|
|
@@ -99,37 +99,37 @@ test('지우기는 매단 것 · 따라 움직이는 것을 풀어 남기고 그
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
test('공간 선언에 든 부품 — 지우고 공간을 남은 부품에 맞추는 길을 한 번에 권한다(차단기의 떠도는 상자, 09-25)', async () => {
|
|
102
|
-
const { inDeclaredRoom } = await import('../client/modeller/
|
|
102
|
+
const { inDeclaredRoom } = await import('../client/modeller/refusals.ts')
|
|
103
103
|
const fm = await import('@hatiolab/figure-model')
|
|
104
|
-
let a = fm.
|
|
105
|
-
a = fm.
|
|
106
|
-
a = fm.
|
|
107
|
-
a = fm.
|
|
108
|
-
a = fm.
|
|
104
|
+
let a = fm.addFigureBox(fm.createFigureAsset('Gate'), { id: 'housing', size: { x: 300, y: 1000, z: 300 }, position: { x: 0, y: 0, z: 0 } })
|
|
105
|
+
a = fm.addFigureBox(a, { id: 'stray', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
106
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'housing', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
107
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'stray', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
108
|
+
a = fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'floor' })
|
|
109
109
|
let refused: unknown
|
|
110
110
|
try {
|
|
111
|
-
fm.
|
|
111
|
+
fm.editFigurePart(a, { kind: 'remove-part', target: 'stray' })
|
|
112
112
|
} catch (e) {
|
|
113
113
|
refused = e
|
|
114
114
|
}
|
|
115
115
|
assert.equal(inDeclaredRoom(refused), true)
|
|
116
116
|
assert.equal(inDeclaredRoom(Object.assign(new Error('PART_REFERENCED: top: top holds leg; take it off first'), { code: 'PART_REFERENCED' })), false, '매단 것은 이 길이 아니다')
|
|
117
|
-
const after = fm.
|
|
117
|
+
const after = fm.editFigurePart(a, { kind: 'remove-part', target: 'stray', refitRoom: true })
|
|
118
118
|
assert.equal(after.document.model.nodes.some(n => n.id === 'stray'), false)
|
|
119
119
|
assert.ok(after.occupancy?.bounds, '공간은 남은 부품으로 다시 선언된다')
|
|
120
|
-
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/
|
|
120
|
+
const editor = readFileSync(fileURLToPath(new URL('../client/modeller/editor.ts', import.meta.url)), 'utf8')
|
|
121
121
|
assert.ok(editor.includes("if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }"))
|
|
122
122
|
assert.ok(ko['figure.button.remove-and-refit-room'].includes('{part}'))
|
|
123
123
|
})
|
|
124
124
|
|
|
125
125
|
test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평면이 받는 면을 말한다(벽 배치, 09-25)', async () => {
|
|
126
|
-
const { placementRefusalOf } = await import('../client/modeller/
|
|
126
|
+
const { placementRefusalOf } = await import('../client/modeller/refusals.ts')
|
|
127
127
|
const fm = await import('@hatiolab/figure-model')
|
|
128
|
-
let a = fm.
|
|
129
|
-
a = fm.
|
|
128
|
+
let a = fm.addFigureBox(fm.createFigureAsset('Post'), { id: 'post', size: { x: 100, y: 1000, z: 100 }, position: { x: 0, y: 0, z: 0 } })
|
|
129
|
+
a = fm.applyFigureAuthoring(a, { kind: 'attach', part: 'post', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
130
130
|
let refused: unknown
|
|
131
131
|
try {
|
|
132
|
-
fm.
|
|
132
|
+
fm.applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: fm.proposeFigureOccupancy(a), placement: 'wall' as never })
|
|
133
133
|
} catch (e) {
|
|
134
134
|
refused = e
|
|
135
135
|
}
|
|
@@ -137,14 +137,14 @@ test('배치를 바꿀 수 없는 까닭 — 평면에 붙은 부품과 새 평
|
|
|
137
137
|
assert.ok(said.includes('post (bottom)') && said.includes('벽 기반') && said.includes('back'), said)
|
|
138
138
|
assert.equal(placementRefusalOf(new Error('x'), say), null)
|
|
139
139
|
/* The placement list sends its command through the page, not the editor: the page says it in words too, and the list goes back. */
|
|
140
|
-
const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-
|
|
140
|
+
const page = readFileSync(fileURLToPath(new URL('../client/pages/figure-modeller-page.ts', import.meta.url)), 'utf8')
|
|
141
141
|
assert.ok(page.includes('placementRefusalOf(error, (key, params) => i18next.t(key, params))'), '페이지 경로도 사람의 말로')
|
|
142
|
-
const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-
|
|
142
|
+
const props = readFileSync(fileURLToPath(new URL('../client/modeller/figure-properties.ts', import.meta.url)), 'utf8')
|
|
143
143
|
assert.ok(props.includes('select.value = current'), '거절되면 목록이 원래 배치로 돌아온다')
|
|
144
144
|
})
|
|
145
145
|
|
|
146
146
|
test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커널의 영어가 아니다 (Figure 레인 :3300, 2026-09-26)', async () => {
|
|
147
|
-
const { inputRangeRefusalOf } = await import('../client/modeller/
|
|
147
|
+
const { inputRangeRefusalOf } = await import('../client/modeller/refusals.ts')
|
|
148
148
|
const range = (message: string) => Object.assign(new Error(message), { code: 'INPUT_RANGE' })
|
|
149
149
|
const size = inputRangeRefusalOf(range('INPUT_RANGE: size.x: 6021100 outside [300,1200]'), say)!
|
|
150
150
|
assert.ok(size.includes('X') && size.includes('300') && size.includes('1,200') && size.includes('6,021,100'), size)
|
|
@@ -156,7 +156,7 @@ test('범위 밖 값은 받을 수 있는 범위를 숫자로 말한다 — 커
|
|
|
156
156
|
})
|
|
157
157
|
|
|
158
158
|
test('레시피가 막히면 몇 번째 단계에서 왜 막혔는지 사람의 말로 — 커널의 영어가 아니라 (2026-09-27)', async () => {
|
|
159
|
-
const { recipeRefusalOf } = await import('../client/modeller/
|
|
159
|
+
const { recipeRefusalOf } = await import('../client/modeller/refusals.ts')
|
|
160
160
|
const say = (key: string, p: Record<string, string>) => `${key} ${JSON.stringify(p)}`
|
|
161
161
|
const refusal = Object.assign(new Error('EDIT_TARGET: steps[2]: attach: door is wider than the opening'), { code: 'EDIT_TARGET', path: 'steps[2]' })
|
|
162
162
|
const told = recipeRefusalOf(refusal, say)!
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* **화면에서 발행할 수 있다** (ADR-0087 5단계의 선행 조건, 2026-09-24).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 「32 개 발행 통과」는 스크립트가 서버의 문을 직접 부른 것이지 화면이 아니었다. 그리고 새로 만든
|
|
4
|
+
* 모델러에는 발행 패널이 없었다 — 저장은 되고 발행 · 판본 · 복원 · 판정을 볼 자리가 없었다. 복사본 검증의
|
|
5
|
+
* 「32 개 발행 통과」는 스크립트가 서버의 문을 직접 부른 것이지 화면이 아니었다. 그리고 새로 만든 도형은 전부
|
|
6
6
|
* 「공간 선언 없음」으로 발행이 막히는데, 그 선언을 할 단추가 화면 어디에도 없었다. 5단계에서 `figure-modeller`
|
|
7
7
|
* 가 이 화면으로 바뀌면 새 도형은 영영 발행할 수 없었다.
|
|
8
8
|
*
|
|
@@ -15,37 +15,37 @@ import { readFileSync } from 'node:fs'
|
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
addFigureBox,
|
|
19
|
+
applyFigureAuthoring,
|
|
20
|
+
createFigureAsset,
|
|
21
|
+
inspectFigureAsset,
|
|
22
|
+
proposeFigureOccupancy,
|
|
23
|
+
serializeFigureAsset
|
|
24
24
|
} from '@hatiolab/figure-model'
|
|
25
25
|
|
|
26
|
-
import { occupancyFromParts } from '../client/modeller/
|
|
26
|
+
import { occupancyFromParts } from '../client/modeller/occupancy.ts'
|
|
27
27
|
|
|
28
|
-
const codes = (asset: Parameters<typeof
|
|
28
|
+
const codes = (asset: Parameters<typeof inspectFigureAsset>[0]) => inspectFigureAsset(asset).violations.map(v => v.code)
|
|
29
29
|
|
|
30
|
-
test('새로 만든
|
|
31
|
-
let asset =
|
|
32
|
-
asset =
|
|
30
|
+
test('새로 만든 도형의 「공간 선언 없음」을 「부품에 맞추기」 한 번이 푼다', () => {
|
|
31
|
+
let asset = createFigureAsset('Probe')
|
|
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 =
|
|
35
|
+
const fitted = applyFigureAuthoring(asset, occupancyFromParts(serializeFigureAsset(asset)))
|
|
36
36
|
|
|
37
37
|
assert.deepEqual(codes(fitted), [])
|
|
38
38
|
assert.equal(fitted.occupancy?.placement, 'floor', '처음 선언하는 도형은 바닥에 선다')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배치 기준은 지킨다', () => {
|
|
42
|
-
let asset =
|
|
43
|
-
asset =
|
|
44
|
-
asset =
|
|
45
|
-
asset =
|
|
42
|
+
let asset = createFigureAsset('Probe')
|
|
43
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
44
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }), placement: 'center' })
|
|
45
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
46
46
|
assert.ok(codes(asset).includes('PART_OUTSIDE_OCCUPANCY'), '전제: 넘은 부품은 막힌다')
|
|
47
47
|
|
|
48
|
-
const fitted =
|
|
48
|
+
const fitted = applyFigureAuthoring(asset, occupancyFromParts(serializeFigureAsset(asset)))
|
|
49
49
|
|
|
50
50
|
assert.deepEqual(codes(fitted), [])
|
|
51
51
|
assert.equal(fitted.occupancy?.placement, 'center', '이미 선언된 배치 기준을 바꾸지 않는다')
|
|
@@ -54,7 +54,7 @@ test('선언한 공간을 넘은 부품도 같은 한 번으로 풀리고, 배
|
|
|
54
54
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
55
55
|
|
|
56
56
|
test('모델러가 발행 패널을 걸고, 패널이 내는 셋을 받는다', () => {
|
|
57
|
-
const page = read('../client/pages/figure-
|
|
57
|
+
const page = read('../client/pages/figure-modeller-page.ts')
|
|
58
58
|
|
|
59
59
|
assert.ok(/<figure-history-panel/.test(page), '패널을 건다')
|
|
60
60
|
for (const event of ['fit-base-to-parts', 'figure-released', 'figure-reverted']) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {test} from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import {replaceFigureMeshes} from '../client/modeller/renderer.ts'
|
|
5
|
+
import type {FigureRenderPart} from '../client/modeller/renderer.ts'
|
|
6
|
+
const box=(id:string):FigureRenderPart=>({id,shape:{provider:'box',dimensions:{width:100,height:200,depth:300}},pose:{r:[[1,0,0],[0,1,0],[0,0,1]],t:[0,100,0]},material:{token:'primary'}})
|
|
7
|
+
test('renderer replaces rather than accumulates; empty scene disposes old resources',()=>{
|
|
8
|
+
const group=new THREE.Group();replaceFigureMeshes(group,[box('a'),box('b')],{primary:'#123456'})
|
|
9
|
+
assert.equal(group.children.length,2);let disposed=0
|
|
10
|
+
const first=group.children[0] as THREE.Mesh;first.geometry.addEventListener('dispose',()=>disposed++)
|
|
11
|
+
replaceFigureMeshes(group,[box('c')],{primary:'#123456'});assert.equal(group.children.length,1);assert.equal(group.children[0].name,'c');assert.equal(disposed,1)
|
|
12
|
+
replaceFigureMeshes(group,[],{});assert.equal(group.children.length,0)
|
|
13
|
+
})
|
|
14
|
+
test('renderer preserves scene on unsupported geometry and resolves pose/material',()=>{
|
|
15
|
+
const group=new THREE.Group();replaceFigureMeshes(group,[box('a')],{primary:'#123456'})
|
|
16
|
+
const old=group.children[0],bounds=new THREE.Box3().setFromObject(group)
|
|
17
|
+
assert.equal(bounds.min.y,0);assert.equal(bounds.max.y,200)
|
|
18
|
+
assert.throws(()=>replaceFigureMeshes(group,[box('b'),{...box('c'),shape:{provider:'unknown',dimensions:{}}}],{primary:'#123456'}),/Unsupported/)
|
|
19
|
+
assert.equal(group.children[0],old)
|
|
20
|
+
const p=box('glow');p.material={token:'primary',preset:'metal',transparent:true,emissive:{token:'primary',intensity:2,on:true}}
|
|
21
|
+
replaceFigureMeshes(group,[p],{primary:'#123456'})
|
|
22
|
+
const material=(group.children[0] as THREE.Mesh).material as THREE.MeshStandardMaterial
|
|
23
|
+
assert.equal(material.metalness,0.8);assert.equal(material.transparent,true);assert.equal(material.emissiveIntensity,2)
|
|
24
|
+
})
|