@things-factory/figure-ui 10.1.147 → 10.1.150
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/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.check.mjs +77 -0
- package/test/browser/build.mjs +31 -6
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -93,12 +93,6 @@ test('fresh, width only, and all set back to fixed are three different answers',
|
|
|
93
93
|
const following = applyFigureAuthoring(a, { kind: 'follow-size', part: 'box', axes: ['y'] })
|
|
94
94
|
assert.deepEqual(resizeAxesOf(following, 'box'), { axes: ['y'], basis: 'chosen' })
|
|
95
95
|
assert.deepEqual(resizeAxesOf(following, 'box', ['x']), { axes: ['x', 'y'], basis: 'chosen' })
|
|
96
|
-
/* The screen: size response is no longer a disguised placement command. */
|
|
97
|
-
const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
|
|
98
|
-
const dimensions = links.slice(links.indexOf(' private renderDimensions('), links.indexOf(' /** The same source'))
|
|
99
|
-
assert.ok(dimensions.includes("this.command({ kind: 'follow-size', part: this.partId })"))
|
|
100
|
-
assert.ok(!dimensions.includes('resize-with-placement'), 'size response does not attach the part')
|
|
101
|
-
assert.ok(!links.includes('figure.label.stands-on-plane'), 'the attachment form does not present a model-placement toggle')
|
|
102
96
|
const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
|
|
103
97
|
assert.equal(ko['figure.button.follow-size'], '모델 크기에 맞추기')
|
|
104
98
|
})
|
|
@@ -108,8 +102,6 @@ test('a part off the floor is not moved by choosing its size response', () => {
|
|
|
108
102
|
a = applyFigureAuthoring(a, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(a), placement: 'floor' })
|
|
109
103
|
const asked = planFigureResize(a, { part: 'shelf', axes: resizeAxesOf(a, 'shelf').axes })
|
|
110
104
|
assert.deepEqual([asked.refusal?.code, asked.refusal?.mm], ['GAP_CHOICE', 500])
|
|
111
|
-
const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
|
|
112
|
-
assert.ok(!links.includes("@click=${() => ask('keep')}"), 'the UI does not offer a size command that also moves the part')
|
|
113
105
|
})
|
|
114
106
|
|
|
115
107
|
test('a field only the room reads is taken out of the part\'s position row and shown apart', () => {
|
|
@@ -122,9 +114,6 @@ test('a field only the room reads is taken out of the part\'s position row and s
|
|
|
122
114
|
const { vectors } = groupFields(fields.filter(f => !roomOnly.includes(f.id)), 'box')
|
|
123
115
|
const position = vectors.find(v => v.kind === 'vector' && v.name === 'position')!
|
|
124
116
|
assert.ok(position.kind === 'vector' && position.cells.x && !position.cells.y && position.cells.z, 'X and Z still move it; Y is held by the floor')
|
|
125
|
-
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
126
|
-
assert.ok(editor.includes('${roomFields.length ? this.renderRoomOnly(roomFields, unavailable) : nothing}'))
|
|
127
|
-
assert.ok(editor.includes('if (agreed) this.linkCommand(refit.step)'), 'the room is declared again only when the author confirms')
|
|
128
117
|
})
|
|
129
118
|
|
|
130
119
|
/*
|
|
@@ -145,8 +134,6 @@ test('the part component carries the face a size drag keeps, for each placement'
|
|
|
145
134
|
assert.deepEqual(state.scaleAnchor, expected[mode])
|
|
146
135
|
assert.equal(componentOf(body, {}, { width: 1000, height: 1000 })!.scaleAnchor, undefined, 'none given, none set')
|
|
147
136
|
}
|
|
148
|
-
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
149
|
-
assert.ok(editor.includes('.anchors=${this.anchorsOf(asset)}'))
|
|
150
137
|
})
|
|
151
138
|
|
|
152
139
|
/* Designer 2026-09-27 (acceptance round 2): what the screen shows for a part a rule places, and what it withholds. */
|
|
@@ -161,16 +148,9 @@ test('a fastened part keeps its position and turn rows, with the numbers the rul
|
|
|
161
148
|
assert.ok(rows.rotation && rows.position, 'the turn row stands though every turn axis is the joint\'s')
|
|
162
149
|
const body = compileFigureAsset(resized).evaluate().geometry.find(g => g.id === 'box') as never
|
|
163
150
|
assert.deepEqual(placementOf(body), { position: [20, 50, 30], rotation: [0, 0, 0] }, 'the numbers a held cell shows')
|
|
164
|
-
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
165
|
-
assert.ok(editor.includes('<input type="text" readonly .value=${given(axis)}'), 'a held cell shows its number, read-only')
|
|
166
|
-
assert.ok(!editor.includes('value="—" disabled'), 'not a blank')
|
|
167
151
|
})
|
|
168
152
|
|
|
169
153
|
test('size response and attachment are separate screen paths', () => {
|
|
170
|
-
const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
|
|
171
|
-
assert.ok(links.includes('figure.button.fasten-start'))
|
|
172
|
-
assert.ok(links.includes('figure.label.target-part-placeholder'))
|
|
173
|
-
assert.ok(!links.includes('? this.renderHeld(asset)'))
|
|
174
154
|
const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
|
|
175
155
|
assert.equal(ko['figure.text.held-on-plane'].replace('{face}', '아랫면').replace('{place}', '바닥').replace('{gap}', '0'), '아랫면을 바닥에 유지 중 (간격 0 mm)')
|
|
176
156
|
})
|
|
@@ -181,10 +161,6 @@ test('a part fastened twisted has no face figure-model can keep for a drag, so t
|
|
|
181
161
|
a = addFigureBox(a, { id: 'blade', size: { x: 300, y: 20, z: 60 }, position: { x: 0, y: 110, z: 0 } })
|
|
182
162
|
a = applyFigureAuthoring(a, { kind: 'attach', part: 'blade', face: 'bottom', to: { part: 'hub', face: 'top' }, twist: 30 })
|
|
183
163
|
assert.equal(figureScaleAnchorOf(a, 'blade'), undefined)
|
|
184
|
-
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
185
|
-
assert.ok(editor.includes('if (reach.scale && this.sizeDragUnsure(asset, id)) return { ...reach, scale: false }'), 'the tool is off')
|
|
186
|
-
assert.ok(editor.includes("if (asset && this.sizeDragUnsure(asset, this.selected)) {"), 'the R key says why')
|
|
187
|
-
assert.ok(editor.includes("result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragUnsure(asset, id)"), 'a drag that still arrives is not written')
|
|
188
164
|
})
|
|
189
165
|
|
|
190
166
|
/*
|
|
@@ -200,14 +176,6 @@ test('"open attachment settings" opens this part\'s fastening, as it is now, and
|
|
|
200
176
|
assert.equal(form.toPlane, true, 'on the plane')
|
|
201
177
|
assert.equal(form.face, 'bottom')
|
|
202
178
|
assert.equal(form.gap, 100, 'the gap it keeps')
|
|
203
|
-
const links = readFileSync(new URL('../client/modeller/figure-links.ts', import.meta.url), 'utf-8')
|
|
204
|
-
const editor = readFileSync(new URL('../client/modeller/editor.ts', import.meta.url), 'utf-8')
|
|
205
|
-
assert.ok(links.includes('@click=${() => this.openFastening(asset)}'))
|
|
206
|
-
const open = links.slice(links.indexOf(' private async openFastening('), links.indexOf(' /**\n * What holds the part, said as its state'))
|
|
207
|
-
assert.ok(open.includes('const own = fasteningOf(this.revision!, this.partId)'), 'the form is loaded from the fastening it has now')
|
|
208
|
-
assert.ok(open.includes("querySelector<HTMLElement>('section[fastening]')"), 'the fastening section is what comes into view')
|
|
209
|
-
assert.ok(!editor.includes("@open-fastening=${() => (this.tab = 'capability')}"), 'it does not switch to the capability tab')
|
|
210
|
-
assert.ok(links.includes('<section fastening ?spotlit=${this.spotlit}'))
|
|
211
179
|
})
|
|
212
180
|
|
|
213
181
|
/*
|
|
@@ -9,29 +9,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
|
|
10
10
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const panel = read('../client/modeller/figure-history-panel.ts')
|
|
14
|
-
const revert = panel.slice(panel.indexOf(' private async revert('))
|
|
15
|
-
assert.ok(revert.indexOf('if (this.dirty) {') < revert.indexOf('revertFigureVersion('), 'asked before anything is reverted')
|
|
16
|
-
assert.ok(revert.includes("if (!(await this.settleUnsaved('revert', version))) return"), 'cancelled or not saved: nothing')
|
|
17
|
-
const settle = panel.slice(panel.indexOf(' private async settleUnsaved('), panel.indexOf(' private async revert('))
|
|
18
|
-
assert.ok(settle.includes('html`<figure-revert-choice .version=${version} .purpose=${purpose}></figure-revert-choice>`'))
|
|
19
|
-
assert.ok(settle.includes('if (!choice) return false'), 'cancelled: nothing')
|
|
20
|
-
const choice = read('../client/modeller/figure-revert-choice.ts')
|
|
21
|
-
assert.ok(choice.includes("closePopup(this, 'discard' satisfies RevertChoice)") && choice.includes("closePopup(this, 'save' satisfies RevertChoice)"))
|
|
22
|
-
assert.ok(choice.includes('slot="start" destructive'), 'letting edits go is marked as what it is')
|
|
23
|
-
})
|
|
24
|
-
|
|
12
|
+
/* The three asked, and a save first that holds the revert, are checked on the page: test/browser/product-page.check.mjs. */
|
|
25
13
|
test('saved first, and known to be -- or nothing is reverted', () => {
|
|
26
|
-
const panel = read('../client/modeller/figure-history-panel.ts')
|
|
27
|
-
const settle = panel.slice(panel.indexOf(' private async settleUnsaved('), panel.indexOf(' private async revert('))
|
|
28
|
-
assert.ok(settle.includes("if (!saved?.ok) {\n this.failure = i18next.t('figure.text.revert-held-unsaved'"), 'a save not done holds the revert')
|
|
29
|
-
assert.ok(settle.includes('return false\n }\n }\n return true'))
|
|
30
|
-
const page = read('../client/pages/figure-modeller-page.ts')
|
|
31
|
-
assert.ok(page.includes('.saveFirst=${this.saveBeforeRevert}'))
|
|
32
|
-
const save = page.slice(page.indexOf(' private saveBeforeRevert'), page.indexOf(' private reverted'))
|
|
33
|
-
assert.ok(save.includes("return this.session.read().dirty ? { ok: false } : { ok: true }"), 'ok only when the session says saved')
|
|
34
|
-
assert.ok(save.includes("code === 'SAVE_UNREACHABLE' ? i18next.t('figure.text.save-unreachable')"), 'not known is said as not known')
|
|
35
14
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
|
|
36
15
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
37
16
|
for (const key of ['figure.text.revert-with-unsaved', 'figure.button.revert-saving', 'figure.button.revert-discarding', 'figure.text.revert-held-unsaved']) assert.ok(t[key], `${lang} ${key}`)
|
|
@@ -69,18 +69,7 @@ test('the source a revert expects is what the server is confirmed to hold -- not
|
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
test('the screen: reverting is on, sends the confirmed source and a request id, and lists the kept drafts apart', () => {
|
|
72
|
-
|
|
73
|
-
assert.ok(!panel.includes('REVERT_KEEPS_REPLACED_DRAFT'), 'the gate is gone')
|
|
74
|
-
const revert = panel.slice(panel.indexOf(' private async revert('), panel.indexOf(' private async undo('))
|
|
75
|
-
/* Read after the unsaved edits are settled -- a save first moves it. */
|
|
76
|
-
assert.ok(revert.indexOf('settleUnsaved(') < revert.indexOf('const expected = this.serverSource?.()'))
|
|
77
|
-
assert.ok(revert.includes('if (expected === undefined) {'), 'not known: not reverted')
|
|
78
|
-
assert.ok(revert.includes('revertFigureVersion(this.figureId!, version, requestId, expected)'))
|
|
79
|
-
const undo = panel.slice(panel.indexOf(' private async undo('), panel.indexOf(' private async send('))
|
|
80
|
-
assert.ok(undo.includes("this.dirty && !(await this.settleUnsaved('undo'))"), 'an undo asks about unsaved edits too')
|
|
81
|
-
assert.ok(panel.includes('<section kept>'), 'kept drafts in their own section, not among the releases')
|
|
82
|
-
assert.ok(panel.includes('${newest && !kept.restoredAt'), 'only the newest, not yet brought back, is offered')
|
|
83
|
-
assert.ok(read('../client/pages/figure-modeller-page.ts').includes('.serverSource=${() => this.session.confirmedSource()}'))
|
|
72
|
+
/* The panel on the page -- the confirmed source sent, the same request asked again, the kept drafts -- is checked in product-page.check.mjs. */
|
|
84
73
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
|
|
85
74
|
const t = JSON.parse(read(`../translations/${lang}.json`))
|
|
86
75
|
for (const key of [
|
package/test/save-landed.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab
|
|
|
11
11
|
|
|
12
12
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
13
13
|
import { thumbnailHashOf } from '../client/modeller/thumbnail-hash.js'
|
|
14
|
+
import { reached } from '../client/graphql/reached.js'
|
|
14
15
|
|
|
15
16
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
16
17
|
/** A picture as the modeller sends one: a data URL. */
|
|
@@ -95,27 +96,28 @@ test('a real conflict, or no server at all: the failure stands and the edits sta
|
|
|
95
96
|
assert.equal(t.state.writes, 0)
|
|
96
97
|
})
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert.
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
/* The page's side -- a save lost and read back, a save not known said as such -- is checked in the browser (test/browser/product-page.check.mjs). */
|
|
100
|
+
test('not reached is said as not known, not as not saved', async () => {
|
|
101
|
+
type Told = Error & { code?: string; cause?: unknown }
|
|
102
|
+
assert.equal(await reached(Promise.resolve('answered')), 'answered')
|
|
103
|
+
/* Lost on the way or on the way back: not known -- SAVE_UNREACHABLE unless the caller names its own. */
|
|
104
|
+
const lost = Object.assign(Error('Failed to fetch'), { networkError: { statusCode: 0 } })
|
|
105
|
+
await assert.rejects(reached(Promise.reject(lost)), (e: Told) => e.code === 'SAVE_UNREACHABLE' && e.cause === lost && e.message === '서버에 연결하지 못했습니다.')
|
|
106
|
+
const offline = new TypeError('Failed to fetch')
|
|
107
|
+
await assert.rejects(reached(Promise.reject(offline), 'REVERT_UNREACHABLE'), (e: Told) => e.code === 'REVERT_UNREACHABLE' && e.cause === offline)
|
|
108
|
+
/* A refusal keeps the server's code, so a conflict is told from a refusal on grounds. */
|
|
109
|
+
const conflict = Object.assign(Error('Response not successful'), { graphQLErrors: [{ message: '다른 편집에서 모델이 변경되었습니다.', extensions: { code: 'FIGURE_SOURCE_CONFLICT' } }] })
|
|
110
|
+
await assert.rejects(reached(Promise.reject(conflict)), (e: Told) => e.code === 'FIGURE_SOURCE_CONFLICT' && e.message === '다른 편집에서 모델이 변경되었습니다.')
|
|
111
|
+
const other = Error('something else')
|
|
112
|
+
await assert.rejects(reached(Promise.reject(other)), (e: Told) => e === other)
|
|
104
113
|
const write = read('../../figure-service/server/service/figure/figure-write.ts')
|
|
105
114
|
assert.ok(write.includes("return new GraphQLError(message, { extensions: { code: 'FIGURE_SOURCE_CONFLICT' } })"), 'the server says a conflict is one')
|
|
106
|
-
const editor = read('../client/modeller/editor.ts')
|
|
107
|
-
assert.ok(editor.includes("=== 'SAVE_UNREACHABLE' ? i18next.t('figure.text.save-unreachable')"))
|
|
108
115
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.save-unreachable'], lang)
|
|
109
116
|
assert.match(JSON.parse(read('../translations/ko.json'))['figure.text.save-unreachable'], /저장됐는지 확인하지 못했습니다/)
|
|
110
117
|
})
|
|
111
118
|
|
|
112
|
-
/* A released figure made a draft by a change is said when it happens -- saved source, or category and tags. */
|
|
119
|
+
/* A released figure made a draft by a change is said when it happens -- saved source, or category and tags (product-page.check.mjs). */
|
|
113
120
|
test('a released figure changed back to a draft is told, whichever change did it', () => {
|
|
114
|
-
const page = read('../client/pages/figure-modeller-page.ts')
|
|
115
|
-
assert.ok(page.includes("if (before === 'released' && after === 'draft') notify({ level: 'info', message: i18next.t('figure.text.released-now-draft') })"))
|
|
116
|
-
assert.ok(page.includes('if (this.session.read().id === id) this.toldDemoted(this.row.state, figure.state)'), 'after a save, read back')
|
|
117
|
-
assert.ok(page.includes('this.toldDemoted(this.row.state, result.figure.state)'), 'after category or tags')
|
|
118
|
-
assert.ok(page.includes("...(result.figure.state ? { state: result.figure.state as 'draft' | 'released' } : {})"), 'and the badge follows')
|
|
119
121
|
for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.released-now-draft'], lang)
|
|
120
122
|
})
|
|
121
123
|
|
|
@@ -33,12 +33,8 @@ const box = (t: number[], r = turned(0)): FigureRenderPart => ({
|
|
|
33
33
|
material: { token: 'palette.primary' }
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
test('things-scene 의 판을
|
|
37
|
-
|
|
38
|
-
assert.match(canvas, /type: 'model-layer'/)
|
|
39
|
-
assert.match(canvas, /mode: 1\n/, '편집 모드 — 시점 단추 · 기즈모 · 도움말 · 단축키')
|
|
40
|
-
assert.ok(canvas.includes('...sceneLook(this.view ?? DEFAULT_VIEW)'))
|
|
41
|
-
assert.ok(canvas.includes('pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2)'), '두 배 표본')
|
|
36
|
+
test('things-scene 의 판을 세운다 — 제 장면을 짜지 않는다', () => {
|
|
37
|
+
/* The board in editing mode, the view settings and the supersampling it is given are the browser check's (test/browser/screen.check.mjs). */
|
|
42
38
|
assert.doesNotMatch(canvas, /new THREE\.|WebGLRenderer|OrbitControls|TransformControls/, '제 장면을 짜지 않는다')
|
|
43
39
|
})
|
|
44
40
|
|
|
@@ -77,10 +73,3 @@ test('돌린 몸체도 왕복한다 — 판에 적은 상태를 그대로 되읽
|
|
|
77
73
|
assert.equal(changedKeys(at, { ...at }), null, '같은 값은 다시 넣지 않는다 — 씬이 메아리를 내지 않게')
|
|
78
74
|
assert.deepEqual(changedKeys(at, { ...at, left: 1 }), { left: 1 })
|
|
79
75
|
})
|
|
80
|
-
|
|
81
|
-
test('고르기와 끌기는 이름으로 알리고, 스스로 적은 것의 메아리는 끌기로 읽지 않는다', () => {
|
|
82
|
-
assert.ok(canvas.includes("root.on('selected'"))
|
|
83
|
-
assert.ok(canvas.includes("if (this.writing) return"))
|
|
84
|
-
assert.ok(canvas.includes("new CustomEvent('part-transformed', { detail: { id: part.id, result }"))
|
|
85
|
-
assert.ok(canvas.includes('setTimeout(() => this.sync())'), '받지 못한 변경은 입력이 둔 자리로 돌아간다')
|
|
86
|
-
})
|
|
@@ -12,17 +12,12 @@ import { fileURLToPath } from 'node:url'
|
|
|
12
12
|
import { addFigurePrimitive, applyFigureAuthoring, compileFigureAsset, createFigureAsset } from '@hatiolab/figure-model'
|
|
13
13
|
import type { FigureAsset, FigureEvaluatedShape, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
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'
|
|
15
|
+
import { apexOver, drawnOf, extentOf, measureNoteOf, sectionActionOf, sectionOf as sectionOfWorking, sectionPointsWordsOf, 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
17
|
import { revisionOf } from './revision-of.ts'
|
|
18
18
|
|
|
19
19
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
20
|
-
/* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
|
|
21
|
-
const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
|
|
22
|
-
const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
|
|
23
20
|
const editorPanel = read('../client/modeller/figure-section-editor.ts')
|
|
24
|
-
const sectionPanel = read('../client/modeller/figure-section-panel.ts')
|
|
25
|
-
const editor = read('../client/modeller/editor.ts')
|
|
26
21
|
const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
|
|
27
22
|
const say = (key: string, params: Record<string, string>) => Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), ko[key] ?? key)
|
|
28
23
|
|
|
@@ -112,31 +107,6 @@ test('치는 동안 입체는 그 점으로 그려지고, 잘못된 동안은
|
|
|
112
107
|
const body = drawn.find(p => p.id === 'wedge') as unknown as { shape: FigureEvaluatedShape }
|
|
113
108
|
assert.equal((body.shape.extrusion!.section as { points: unknown[] }).points.length, 4)
|
|
114
109
|
assert.equal(pointsOf(t).length, 3, 'the figure itself is not changed')
|
|
115
|
-
has(editorPanel, /private preview\(w = this\.lastValid\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: this\.action\(w\) \}/, 'the preview is always of a valid shape')
|
|
116
|
-
has(editorPanel, /this\.preview\(c && this\.valid\(\{ \.\.\.this\.working, cavity: c \}\) \? \{ \.\.\.this\.working, cavity: c \} : this\.lastValid\)/, 'the one other: the inside being dragged, when valid')
|
|
117
|
-
has(editorPanel, /figure\.text\.section-editor-last-valid/)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('적용은 한 명령, 취소·Esc 는 아무것도 보내지 않는다 — 창을 연 뒤 도형이 바뀌면 적용을 막는다', () => {
|
|
121
|
-
has(editorPanel, /private apply\(\) \{\s*if \(this\.stale \|\| !this\.valid\(this\.working\)\) return/)
|
|
122
|
-
has(editorPanel, /private cancel\(\) \{\s*this\.dispatchEvent\(new CustomEvent\('command-live', \{ detail: \{ action: null \}/)
|
|
123
|
-
has(editorPanel, /return this\.revision !== this\.openedAt/)
|
|
124
|
-
has(editor, /<figure-section-editor[\s\S]*?\.revision=\$\{this\.session\?\.read\(\)\.revision \?\? 0\}[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
|
|
125
|
-
has(editor,
|
|
126
|
-
/protected willUpdate[\s\S]*?if \(this\.sectionEditing && this\.sectionEditing !== this\.selected\) this\.leaveSectionEditor\(this\.selected\)\s*\}/,
|
|
127
|
-
'the selection moved off the part by any way closes it and lets go of the preview'
|
|
128
|
-
)
|
|
129
|
-
has(editor, /if \(!editor\?\.dirty \|\| !stillThere\) \{\s*this\.closeSectionEditor\(\)/, 'nothing changed, or nothing to go back to: it just closes')
|
|
130
|
-
has(editor, /private closeSectionEditor\(\) \{\s*this\.sectionEditing = null\s*this\.holdAction\(null\)/)
|
|
131
|
-
has(sectionPanel, /section-editor-open/)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
test('Delete 키는 칸 안에서는 글자를, 밖에서는 고른 점을 지운다 — 창이 먼저 듣고, 받은 키는 뒤의 장면에 넘기지 않는다', () => {
|
|
135
|
-
has(editorPanel, /\(e\.key === 'Delete' \|\| e\.key === 'Backspace'\) && !inField && this\.selected/)
|
|
136
|
-
/* The scene listens on its own element and stops the key there: heard later, Esc selected nothing and Delete took the part. */
|
|
137
|
-
has(editorPanel, /window\.addEventListener\('keydown', this\.keys, true\)/)
|
|
138
|
-
has(editorPanel, /const take = \(\) => \(e\.preventDefault\(\), e\.stopImmediatePropagation\(\)\)/)
|
|
139
|
-
has(editorPanel, /if \(e\.key === 'Escape'\) \{\s*take\(\)\s*this\.requestClose\(\)/)
|
|
140
110
|
})
|
|
141
111
|
|
|
142
112
|
test('그림은 점을 다 담고 여유를 둔다 — 50 mm 단위로', () => {
|
|
@@ -154,11 +124,6 @@ test('편집 창의 말은 다섯 말 모두에 있다', () => {
|
|
|
154
124
|
}
|
|
155
125
|
})
|
|
156
126
|
|
|
157
|
-
test('창의 네 줄은 줄 번호로 선다 — 넓은 화면에서 탭 줄이 숨어도 적용·취소가 밀려 나가지 않는다', () => {
|
|
158
|
-
for (const [part, row] of [['header', 1], ['nav\\[tabs\\]', 2], ['div\\[body\\]', 3], ['footer', 4]] as const)
|
|
159
|
-
has(editorPanel, new RegExp(`${part} \\{\\s*grid-row: ${row};`))
|
|
160
|
-
})
|
|
161
|
-
|
|
162
127
|
test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 — 지금 값이 옮긴 점·다시 친 반지름을 되돌리지 않는다', async () => {
|
|
163
128
|
const { liveOverSession } = await import('../client/modeller/live.ts')
|
|
164
129
|
const t = triangle()
|
|
@@ -174,12 +139,6 @@ test('치는 동안의 명령이 쓴 입력은 명령의 값으로 그린다 —
|
|
|
174
139
|
assert.deepEqual((b.shape.extrusion!.section as { corner: unknown }).corner, { x: 80, z: 80 })
|
|
175
140
|
})
|
|
176
141
|
|
|
177
|
-
test('편집 창이 열린 동안 모델러의 키는 비켜선다 — Esc 가 선택을, Delete 가 부품을, Ctrl+Z 가 도형의 기록을 건드리지 않는다', () => {
|
|
178
|
-
const onKey = editor.slice(editor.indexOf(' private onKey = '), editor.indexOf(' connectedCallback() {', editor.indexOf(' private onKey = ')))
|
|
179
|
-
has(onKey, /if \(this\.sectionEditing\) return\s*const canvas/)
|
|
180
|
-
assert.ok(onKey.indexOf('if (this.sectionEditing) return') < onKey.indexOf('keyActionOf('), 'before any key is taken')
|
|
181
|
-
})
|
|
182
|
-
|
|
183
142
|
test('속성의 형상 치수에는 점·꼭짓점 칸이 없다 (사용자 10-01) — 높이만 남고, 단면 칸이 점 개수를 말한다', async () => {
|
|
184
143
|
const { groupFields } = await import('../client/modeller/fields.ts')
|
|
185
144
|
const f = (id: string) => ({ id, value: 0, unit: 'mm' })
|
|
@@ -188,16 +147,14 @@ test('속성의 형상 치수에는 점·꼭짓점 칸이 없다 (사용자 10-0
|
|
|
188
147
|
'tip.dimension.round', 'tip.dimension.wall', 'tip.dimension.closed.start', 'tip.dimension.wallX', 'tip.corner.x', 'tip.wall.x-']
|
|
189
148
|
const { singles } = groupFields(ids.map(f) as never, 'tip')
|
|
190
149
|
assert.deepEqual(singles.map(g => (g as { field: { id: string } }).field.id), ['tip.dimension.height'])
|
|
191
|
-
|
|
192
|
-
assert.
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
has(editorPanel, /:host\(\[narrow\]\[show='solid'\]\) div\[body\] \{\s*display: none;/)
|
|
200
|
-
has(editorPanel, /this\.room\.observe\(this\.parentElement\)/)
|
|
150
|
+
/* The 단면 group says how many points instead: a triangle three, a pyramid three and its apex; a box nothing. */
|
|
151
|
+
assert.deepEqual(sectionPointsWordsOf(drawnOf(shapeOf(triangle()))), { key: 'figure.text.section-points', params: { points: '3' } })
|
|
152
|
+
const pyramid = applyFigureAuthoring(triangle(), { kind: 'set-section', part: 'wedge', apex: { x: 0, z: 0 } })
|
|
153
|
+
const words = sectionPointsWordsOf(drawnOf(shapeOf(pyramid)))!
|
|
154
|
+
assert.equal(say(words.key, words.params), '점 3개 · 꼭짓점 하나')
|
|
155
|
+
const { addFigureBox } = await import('@hatiolab/figure-model')
|
|
156
|
+
const box = addFigureBox(createFigureAsset('B'), { id: 'wedge', size: { x: 400, y: 100, z: 300 }, position: { x: 0, y: 50, z: 0 } })
|
|
157
|
+
assert.equal(sectionPointsWordsOf(drawnOf(shapeOf(box))), null)
|
|
201
158
|
})
|
|
202
159
|
|
|
203
160
|
test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령으로, 바꾸지 않은 치수는 보내지 않는다', async () => {
|
|
@@ -210,7 +167,9 @@ test('사각형·원도 같은 창에서 — 폭·지름·길이를 한 명령
|
|
|
210
167
|
assert.deepEqual(sectionOf(shape, { ...w, across: { x: 500, z: 300 } }), { kind: 'rectangle', size: { x: 500, z: 300 } })
|
|
211
168
|
assert.deepEqual(figureSectionMeasuresOf(box, 'b')!.measures, { y: 'editable', x: 'editable', z: 'editable' })
|
|
212
169
|
/* The editor sends only what changed: a measure left alone may follow another value and must not be written. */
|
|
213
|
-
|
|
170
|
+
assert.deepEqual(sectionActionOf('b', 'rectangle', w, { ...w, across: { x: 500, z: 300 } }), { kind: 'set-section', part: 'b', across: { x: 500 } })
|
|
171
|
+
assert.deepEqual(sectionActionOf('b', 'rectangle', w, { ...w, length: 260 }), { kind: 'set-section', part: 'b', length: 260 })
|
|
172
|
+
assert.deepEqual(sectionActionOf('b', 'rectangle', w, w), { kind: 'set-section', part: 'b' }, 'nothing changed, nothing written')
|
|
214
173
|
const next = applyFigureAuthoring(box, { kind: 'set-section', part: 'b', across: { x: 500 }, length: 260 })
|
|
215
174
|
const b = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 'b') as unknown as { shape: FigureEvaluatedShape; pose: { t: number[] } })
|
|
216
175
|
assert.deepEqual(b.shape.extrusion!.section, { kind: 'rectangle', size: { x: 500, z: 300 } })
|
|
@@ -252,17 +211,20 @@ test('창 안에서 폭과 속 공간을 함께 — 넓힌 폭에서만 들어
|
|
|
252
211
|
const next = applyFigureAuthoring(tray, { kind: 'set-section', part: 't', across: { x: 600, z: 400 }, cavity: thick })
|
|
253
212
|
const t = (compileFigureAsset(next).evaluate().geometry.find(g => (g as { id: string }).id === 't') as unknown as { shape: FigureEvaluatedShape }).shape
|
|
254
213
|
assert.deepEqual(t.cavity!.wall, thick.wall)
|
|
255
|
-
/* The
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
has(editorPanel, /private get hollowable\(\) \{\s*return !!this\.body\?\.shape\.extrusion && \(this\.kind === 'rectangle' \|\| this\.kind === 'ellipse'\)/, 'no inside for a drawn section')
|
|
214
|
+
/* The editor's one command carries the inside only when it changed: the widths alone leave it out; taken away, null. */
|
|
215
|
+
assert.deepEqual(sectionActionOf('t', 'rectangle', w, { ...w, across: { x: 600, z: 400 } }), { kind: 'set-section', part: 't', across: { x: 600, z: 400 } })
|
|
216
|
+
assert.deepEqual(sectionActionOf('t', 'rectangle', w, wide), { kind: 'set-section', part: 't', across: { x: 600, z: 400 }, cavity: thick })
|
|
217
|
+
assert.deepEqual(sectionActionOf('t', 'rectangle', w, { ...w, cavity: null }), { kind: 'set-section', part: 't', cavity: null })
|
|
260
218
|
})
|
|
261
219
|
|
|
262
|
-
test('
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
220
|
+
test('치수 칸 밑의 말: 다른 값을 따르면 그렇다고, 길이는 관계가 남기는 면을 — 말할 것이 없으면 아무것도', () => {
|
|
221
|
+
assert.deepEqual(measureNoteOf('linked', 1, 'y'), { key: 'figure.text.section-editor-linked' }, 'following another value is said first')
|
|
222
|
+
assert.deepEqual(measureNoteOf('editable', 1, 'y'), { key: 'figure.text.section-editor-keeps', face: 'bottom' }, 'a floor part keeps its start: the bottom')
|
|
223
|
+
assert.deepEqual(measureNoteOf('editable', -1, 'y'), { key: 'figure.text.section-editor-keeps', face: 'top' })
|
|
224
|
+
assert.deepEqual(measureNoteOf('editable', -1, 'x'), { key: 'figure.text.section-editor-keeps', face: 'right' })
|
|
225
|
+
assert.deepEqual(measureNoteOf('editable', 0, 'y'), { key: 'figure.text.section-editor-keeps-centre' })
|
|
226
|
+
assert.equal(measureNoteOf('editable', undefined, 'y'), null, 'the relations cannot say: nothing')
|
|
227
|
+
assert.equal(measureNoteOf(undefined, undefined, 'x'), null, 'a width across says only that it follows')
|
|
266
228
|
assert.equal(say('figure.text.section-editor-keeps', { face: ko['figure.label.face-bottom']! }), '아랫면 유지')
|
|
267
229
|
})
|
|
268
230
|
|
|
@@ -312,43 +274,28 @@ test('편집 창에서 끝을 한 점으로: 켜면 점들의 가운데 위에
|
|
|
312
274
|
const on = { ...w, apex: apexOver(w.points, 'y') }
|
|
313
275
|
assert.deepEqual(on.apex, { z: 0, x: 0 })
|
|
314
276
|
assert.deepEqual(issuesOfWorking(shape, on).words, [])
|
|
315
|
-
/* The editor sends apex only when it changed, null to go back to a push. */
|
|
316
|
-
|
|
277
|
+
/* The editor sends apex only when it changed, null to go back to a push; a drawn section's points always go. */
|
|
278
|
+
assert.deepEqual(sectionActionOf('wedge', 'polygon', w, on), { kind: 'set-section', part: 'wedge', points: w.points, apex: { z: 0, x: 0 } })
|
|
279
|
+
assert.deepEqual(sectionActionOf('wedge', 'polygon', w, w), { kind: 'set-section', part: 'wedge', points: w.points })
|
|
317
280
|
const pyramid = applyFigureAuthoring(wedge, { kind: 'set-section', part: 'wedge', apex: on.apex })
|
|
318
281
|
const p = shapeOf(pyramid)
|
|
319
282
|
assert.equal(p.provider, 'loft')
|
|
320
283
|
const again = workingOfShape(p)
|
|
321
284
|
assert.deepEqual(again.apex, { x: 0, z: 0 })
|
|
285
|
+
assert.deepEqual(sectionActionOf('wedge', 'polygon', again, { ...again, apex: null }).apex, null, 'turned off: back to a push')
|
|
286
|
+
assert.equal('apex' in sectionActionOf('wedge', 'polygon', again, { ...again, apex: { x: 0, z: 0 } }), false, 'the same apex is not sent')
|
|
322
287
|
assert.deepEqual(sectionOfWorking(p, again), { kind: 'polygon', points: again.points })
|
|
323
288
|
assert.equal(issuesOfWorking(p, { ...again, apex: { x: Number.NaN, z: 0 } }).words[0]!.key, 'figure.text.section-issue-number')
|
|
324
|
-
/* Its length is its height: the same field, called so. */
|
|
325
|
-
has(editorPanel, /this\.working\.apex \? 'figure\.label\.section-editor-height' : 'figure\.label\.section-editor-length'/)
|
|
326
289
|
const back = applyFigureAuthoring(pyramid, { kind: 'set-section', part: 'wedge', apex: null })
|
|
327
290
|
assert.equal(shapeOf(back).provider, 'extrude')
|
|
328
291
|
})
|
|
329
292
|
|
|
293
|
+
|
|
330
294
|
test('끝면에 붙은 부품이 있으면 한 점으로 모으기를 막고 그 면을 말한다 — 커널의 거절로 판정한다', () => {
|
|
331
|
-
has(editor, /const apex = apexOver\(drawn\.section\.points, drawn\.axis\)[\s\S]*?session\.previewCommand\(\{ kind: 'set-section', part, apex \}\)/)
|
|
332
|
-
has(editor, /\.code === 'FEATURE_MAPPING_REQUIRED'\)/)
|
|
333
|
-
has(editorPanel, /const blocked = !apex && !this\.opened\?\.apex \? this\.pointBlocked : ''/)
|
|
334
|
-
has(editorPanel, /\?disabled=\$\{!!blocked\}/)
|
|
335
295
|
assert.equal(say('figure.text.section-editor-apex-blocked', { face: ko['figure.label.face-top']! }), '윗면에 붙은 부품이 있어 한 점으로 모을 수 없습니다.')
|
|
336
|
-
/* A pyramid in the section panel: its editor opens, and it has no corners and no inside. */
|
|
337
|
-
has(sectionPanel, /const e = body\?\.shape \? drawnOf\(body\.shape\) : undefined/)
|
|
338
296
|
})
|
|
339
297
|
|
|
340
298
|
test('닫기 전에 묻는다 (사용자 10-01): 바꾼 것이 있으면 적용·버리기·계속 편집 — 바꾼 것이 없으면 그냥 닫는다', () => {
|
|
341
|
-
const choice = read('../client/modeller/figure-section-close-choice.ts')
|
|
342
|
-
/* Esc and 취소 both ask; while it asks, the question has the keys. */
|
|
343
|
-
has(editorPanel, /if \(e\.key === 'Escape'\) \{\s*take\(\)\s*this\.requestClose\(\)/)
|
|
344
|
-
has(editorPanel, /<button @click=\$\{\(\) => this\.requestClose\(\)\}>\$\{i18next\.t\('figure\.button\.section-editor-cancel'\)\}/)
|
|
345
|
-
has(editorPanel, /if \(this\.asking\) return\n/)
|
|
346
|
-
has(editorPanel, /if \(!this\.dirty\) \{\s*this\.cancel\(\)\s*return true/)
|
|
347
|
-
/* Apply is only offered when it would be taken: not for a refused shape, not after the figure changed. */
|
|
348
|
-
has(editorPanel, /if \(choice === 'apply' && !held\)/)
|
|
349
|
-
has(choice, /\$\{this\.held\s*\? nothing/)
|
|
350
|
-
/* Picking another part while changes wait keeps the part selected until the editor closed. */
|
|
351
|
-
has(editor, /this\.selected = editing\s*editor\.requestClose\(\)\.then\(closed => \{\s*if \(closed && this\.selected === editing\) this\.selected = next/)
|
|
352
299
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
353
300
|
const t = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
|
|
354
301
|
for (const key of ['figure.text.section-editor-close-changed', 'figure.text.section-editor-close-invalid', 'figure.button.section-editor-apply-and-close', 'figure.button.section-editor-discard', 'figure.button.keep-editing'])
|
|
@@ -16,10 +16,13 @@ import {
|
|
|
16
16
|
cavityDepthOf,
|
|
17
17
|
cavityRefusalOf,
|
|
18
18
|
cavityRequestOf,
|
|
19
|
+
cavityRefusalWordsOf,
|
|
19
20
|
cavityResultOf,
|
|
21
|
+
cornerActionOf,
|
|
20
22
|
cornerLimitOf,
|
|
21
23
|
cornerRefusalOf,
|
|
22
24
|
defaultWallOf,
|
|
25
|
+
hollowableOf,
|
|
23
26
|
wallLimitOf
|
|
24
27
|
} from '../client/modeller/section-controls.ts'
|
|
25
28
|
import { livePartsOf } from '../client/modeller/live.ts'
|
|
@@ -27,13 +30,9 @@ import { editRefusalOf } from '../client/modeller/refusals.ts'
|
|
|
27
30
|
import { revisionOf } from './revision-of.ts'
|
|
28
31
|
|
|
29
32
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
30
|
-
/* A source file is a big string: a failing assert.match on it spends minutes building the diff and looks hung. */
|
|
31
|
-
const has = (source: string, re: RegExp, message?: string) => assert.ok(re.test(source), message ?? `missing: ${re}`)
|
|
32
|
-
const hasNot = (source: string, re: RegExp, message?: string) => assert.ok(!re.test(source), message ?? `present: ${re}`)
|
|
33
33
|
const panel = read('../client/modeller/figure-section-panel.ts')
|
|
34
34
|
/* The inside is drawn and typed in its own element (user 2026-10-01): its words and limits are read there. */
|
|
35
35
|
const drawing = read('../client/modeller/figure-cavity-drawing.ts')
|
|
36
|
-
const editor = read('../client/modeller/editor.ts')
|
|
37
36
|
const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
|
|
38
37
|
|
|
39
38
|
const probe = () => addFigureBox(createFigureAsset('Probe'), { id: 'body', size: { x: 400, y: 200, z: 300 }, position: { x: 0, y: 100, z: 0 } })
|
|
@@ -56,7 +55,10 @@ test('모서리 반지름은 네 모서리를 한 값으로 둥글게 하고, 0
|
|
|
56
55
|
const sharp = applyFigureAuthoring(round, { kind: 'set-section-corner', part: 'body', corner: null })
|
|
57
56
|
assert.equal(shapeOf(sharp).extrusion!.section.kind, 'rectangle')
|
|
58
57
|
assert.equal('corner' in shapeOf(sharp).extrusion!.section, false)
|
|
59
|
-
|
|
58
|
+
/* The field's one command: a radius rounds, 0 takes the rounding away -- and the kernel takes both. */
|
|
59
|
+
assert.deepEqual(cornerActionOf('body', 40), { kind: 'set-section-corner', part: 'body', corner: 40 })
|
|
60
|
+
assert.deepEqual(cornerActionOf('body', 0), { kind: 'set-section-corner', part: 'body', corner: null })
|
|
61
|
+
assert.equal('corner' in shapeOf(applyFigureAuthoring(round, cornerActionOf('body', 0))).extrusion!.section, false)
|
|
60
62
|
})
|
|
61
63
|
|
|
62
64
|
test('속을 비우면 기본 벽·막힌 바닥·열린 윗면의 쟁반이 되고, 끝과 벽을 하나씩 바꾼다', () => {
|
|
@@ -90,9 +92,6 @@ test('벽이 단면을 다 채우거나 모서리가 변의 절반을 넘으면,
|
|
|
90
92
|
})
|
|
91
93
|
|
|
92
94
|
test('편집기는 단면 칸의 명령을 세션의 이력 하나로 돌리고, 치는 동안의 값은 그리기만 한다', () => {
|
|
93
|
-
has(editor, /<figure-section-panel[\s\S]*?@command=\$\{\(e: CustomEvent\) => this\.linkCommand\(e\.detail\.action\)\}/)
|
|
94
|
-
has(editor, /@command-live=\$\{\(e: CustomEvent\) => this\.holdAction\(e\.detail\.action\)\}/)
|
|
95
|
-
has(editor, /private linkCommand\(action: FigureAuthoringAction, from: 'section-editor' \| 'panel' = 'panel'\): boolean \{\s*\/\*[^*]*\*\/\s*this\.holdAction\(null\)/, 'a command sent lets go of the one in hand')
|
|
96
95
|
for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es']) {
|
|
97
96
|
const words = JSON.parse(read(`../translations/${lang}.json`)) as Record<string, string>
|
|
98
97
|
const used = [...new Set([...(panel + drawing).matchAll(/'(figure\.(?:label|text)\.section-[\w-]+|figure\.label\.group-section)'/g)].map(m => m[1]!))]
|
|
@@ -139,7 +138,6 @@ test('속 공간은 무엇이 되었는지와 깊이로 보인다 — 한쪽이
|
|
|
139
138
|
assert.equal(cavityDepthOf(tray), 170, 'the 200 mm push less its 30 mm floor')
|
|
140
139
|
assert.equal(cavityResultOf({ ...tray.cavity!, start: 'open' }), 'pipe')
|
|
141
140
|
assert.equal(cavityResultOf({ ...tray.cavity!, end: { closed: 5 } }), 'sealed')
|
|
142
|
-
has(panel, /figure\.text\.section-depth/)
|
|
143
141
|
})
|
|
144
142
|
|
|
145
143
|
test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 — 받을 수 없는 값은 그리지 않는다', () => {
|
|
@@ -151,9 +149,16 @@ test('치는 동안의 값은 사본에 그려지고 원본은 그대로다 —
|
|
|
151
149
|
assert.equal(livePartsOf(asset, { values: {}, action: { kind: 'set-section-corner', part: 'body', corner: 999 } }), null)
|
|
152
150
|
})
|
|
153
151
|
|
|
154
|
-
test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', () => {
|
|
152
|
+
test('다각 단면에는 속 공간을 만들 수 없다고 그 자리에서 말한다', async () => {
|
|
155
153
|
/* A drawn section has no inside control at all, rather than a line saying it cannot have one (user 2026-10-01). */
|
|
156
|
-
|
|
154
|
+
const { addFigurePrimitive } = await import('@hatiolab/figure-model')
|
|
155
|
+
const drawn = addFigurePrimitive(createFigureAsset('D'), { id: 'body', kind: 'polygon', dimensions: { height: 100 }, points: [{ x: -50, z: -40 }, { x: 50, z: -40 }, { x: 0, z: 40 }], position: { x: 0, y: 50, z: 0 } })
|
|
156
|
+
const pyramid = applyFigureAuthoring(drawn, { kind: 'set-section', part: 'body', apex: { x: 0, z: 0 } })
|
|
157
|
+
const tube = addFigurePrimitive(createFigureAsset('C'), { id: 'body', kind: 'cylinder', dimensions: { radius: 50, length: 200 }, position: { x: 0, y: 0, z: 0 } })
|
|
158
|
+
assert.equal(hollowableOf(shapeOf(probe())), true, 'a pushed rectangle')
|
|
159
|
+
assert.equal(hollowableOf(shapeOf(tube)), true, 'a pushed circle')
|
|
160
|
+
assert.equal(hollowableOf(shapeOf(drawn)), false, 'a drawn section')
|
|
161
|
+
assert.equal(hollowableOf(shapeOf(pyramid)), false, 'a pyramid')
|
|
157
162
|
})
|
|
158
163
|
|
|
159
164
|
test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고, 그 면을 사람의 말로 부른다', () => {
|
|
@@ -164,11 +169,6 @@ test('붙어 있는 것이 기대는 면을 없애는 편집은 거절되고,
|
|
|
164
169
|
assert.equal(said, say('figure.text.section-face-in-use', { faces: ko['figure.label.face-inside-bottom']! }))
|
|
165
170
|
})
|
|
166
171
|
|
|
167
|
-
test('치는 동안 그려진 도형이 칸의 글자를 덮어쓰지 않는다 — 친 것은 보낼 때까지 칸이 든다', () => {
|
|
168
|
-
has(panel, /\.value=\$\{live\(this\.typed\[key\] \?\? shown\)\}/)
|
|
169
|
-
has(panel, /this\.typed = \{ \.\.\.this\.typed, \[key\]: \(ev\.target as HTMLInputElement\)\.value \}/)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
172
|
test('면마다 벽 두께를 치는 동안에도 그려진다 — 명령이 없앤 입력(한 벽 두께)의 지금 값은 미리보기를 막지 않는다', async () => {
|
|
173
173
|
const { liveOverSession } = await import('../client/modeller/live.ts')
|
|
174
174
|
const tray = applyFigureAuthoring(probe(), { kind: 'set-cavity', part: 'body', cavity: { wall: 30, start: { closed: 30 }, end: 'open' } })
|
|
@@ -194,5 +194,10 @@ test('한 면의 벽 한도는 그 방향의 너비에서 맞은편 벽을 뺀
|
|
|
194
194
|
/* The kernel agrees: just under takes, the limit refuses. */
|
|
195
195
|
assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 359 } })), null)
|
|
196
196
|
assert.equal(cavityRefusalOf(shape, cavityRequestOf(shape, { wall: { ...walls, 'x-': 360 } }))!.code, 'wall-too-thick')
|
|
197
|
-
|
|
197
|
+
/* The drawing says that side's most with the walls one per side, and the kernel's all-round most otherwise. */
|
|
198
|
+
const over = { wall: { ...walls, 'x-': 380 }, start: { closed: 30 }, end: 'open' as const }
|
|
199
|
+
assert.deepEqual(cavityRefusalWordsOf(shape, over, true, 'x-'), { key: 'figure.text.section-wall-over', params: { max: '360' } })
|
|
200
|
+
assert.deepEqual(cavityRefusalWordsOf(shape, over, false, 'x-'), { key: 'figure.text.section-wall-over', params: { max: String(wallLimitOf(shape)) } })
|
|
201
|
+
assert.deepEqual(cavityRefusalWordsOf(shape, { ...over, wall: walls, start: { closed: 120 }, end: { closed: 90 } }, false), { key: 'figure.text.section-ends-over', params: { max: '200' } })
|
|
202
|
+
assert.equal(cavityRefusalWordsOf(shape, { ...over, wall: walls }, true, 'x-'), null)
|
|
198
203
|
})
|