@things-factory/figure-ui 10.1.74 → 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} +137 -126
- 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-refusals.ts → refusals.ts} +62 -13
- 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/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +5 -0
- package/dist-client/modeller/v3-editor.js +21 -10
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +31 -0
- package/dist-client/modeller/v3-refusals.d.ts +37 -5
- package/dist-client/modeller/v3-refusals.js +59 -16
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- 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 +17 -16
- 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} +11 -7
- 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} +10 -10
- package/test/refusals.test.ts +166 -0
- 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 +15 -7
- package/translations/ja.json +15 -7
- package/translations/ko.json +15 -7
- package/translations/ms.json +15 -7
- package/translations/zh.json +15 -7
- package/docs/v3-product-cutover.md +0 -25
- package/test/v3-refusals.test.ts +0 -136
- 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
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
compileFigureAsset,
|
|
3
|
+
compileFigureDocument,
|
|
4
4
|
LIMITS,
|
|
5
5
|
SEGMENT_PRESETS,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
FIGURE_BUDGET_KEYS,
|
|
7
|
+
FIGURE_GRADE_SCALE,
|
|
8
|
+
figureCostOf,
|
|
9
|
+
figureGradeOf,
|
|
10
|
+
figureMaterialKeyOf,
|
|
11
|
+
figureScoreOf,
|
|
12
|
+
figureWorldBoxesOf,
|
|
13
|
+
figureWritersOf
|
|
14
14
|
} from '@hatiolab/figure-model'
|
|
15
|
-
import type {
|
|
15
|
+
import type { FigureAsset, FigureAssetGeometry, FigureBudgetKey, FigureBudgetUse, FigureCost, FigureGeometry, FigureScore } from '@hatiolab/figure-model'
|
|
16
16
|
|
|
17
17
|
import { capacityFrom } from './scene-budget.js'
|
|
18
18
|
import type { FigureCapacity } from './scene-budget.js'
|
|
@@ -21,40 +21,40 @@ import type { FigureCapacity } from './scene-budget.js'
|
|
|
21
21
|
* The modeller's maturity report, read off the evaluated asset (user, 2026-09-24: the tab showed a count and nothing
|
|
22
22
|
* else — 「왜 이렇게 너프되었는지」):
|
|
23
23
|
*
|
|
24
|
-
* grade A–D and its scale figure-model's `
|
|
24
|
+
* grade A–D and its scale figure-model's `figureScoreOf`: one screen's target shared per instance (groups,
|
|
25
25
|
* triangles) and the declared rule limits (independent, transparent)
|
|
26
|
-
* radar · axes six axes; reactive is not measured, and detail has no
|
|
26
|
+
* radar · axes six axes; reactive is not measured, and detail has no field
|
|
27
27
|
* drawing cost triangles, distinct shapes, distinct materials
|
|
28
28
|
* trend what grows with the instance count and what the instances share
|
|
29
29
|
*
|
|
30
30
|
* Kept apart from the element so it can be tested without a browser.
|
|
31
31
|
*
|
|
32
|
-
* ## What the
|
|
32
|
+
* ## What the renderer shares, which is what the numbers count
|
|
33
33
|
*
|
|
34
|
-
* things-scene draws one mesh per drawn body (`figure-
|
|
35
|
-
* a draw call. It shares geometry by provider, dimensions, segments and profile (`figure-
|
|
34
|
+
* things-scene draws one mesh per drawn body (`figure-rig.ts`) — no merging by material — so a body is
|
|
35
|
+
* a draw call. It shares geometry by provider, dimensions, segments and profile (`figure-geometry.ts`, `keyOf`)
|
|
36
36
|
* and materials by token, preset, transparency and light (`materialFor`). The keys below are those keys; a
|
|
37
37
|
* distinct-shape count that used another key would describe a renderer that does not exist.
|
|
38
38
|
*
|
|
39
|
-
* ## Budgets
|
|
39
|
+
* ## Budgets does not have
|
|
40
40
|
*
|
|
41
41
|
* Two budgets are not measured here, and are not shown as zero:
|
|
42
|
-
* - off-palette surfaces: the
|
|
43
|
-
* - part count by detail level:
|
|
42
|
+
* - off-palette surfaces: the material has no surface field, so there is nothing to count;
|
|
43
|
+
* - part count by detail level: does not carry the detail level (chief architect, 2026-09-24, ADR-0087).
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/*
|
|
47
|
-
The grade, its budgets and its bounds are figure-model's (`
|
|
47
|
+
The grade, its budgets and its bounds are figure-model's (`figureScoreOf`), so the catalogue grades a figure as this tab
|
|
48
48
|
does. The grade is the headroom against one screen's target (`LIMITS.screenInstances`), not a draw-call limit.
|
|
49
49
|
*/
|
|
50
|
-
export {
|
|
51
|
-
export type {
|
|
50
|
+
export { FIGURE_BUDGET_KEYS, FIGURE_GRADE_SCALE as GRADE_SCALE, figureGradeOf as gradeOf }
|
|
51
|
+
export type { FigureBudgetKey, FigureBudgetUse, FigureScore }
|
|
52
52
|
|
|
53
|
-
export type
|
|
53
|
+
export type FigureAxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'
|
|
54
54
|
|
|
55
55
|
/** One maturity axis. */
|
|
56
|
-
export interface
|
|
57
|
-
key:
|
|
56
|
+
export interface FigureMaturityAxis {
|
|
57
|
+
key: FigureAxisKey
|
|
58
58
|
/** 0–1. Absent when the axis cannot be measured. */
|
|
59
59
|
ratio?: number
|
|
60
60
|
reading?: string
|
|
@@ -66,7 +66,7 @@ export interface V3MaturityAxis {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Instances on one screen, and what each series costs at that count. */
|
|
69
|
-
export interface
|
|
69
|
+
export interface FigureCurvePoint {
|
|
70
70
|
instances: number
|
|
71
71
|
triangles: number
|
|
72
72
|
drawCalls: number
|
|
@@ -75,16 +75,16 @@ export interface V3CurvePoint {
|
|
|
75
75
|
materials: number
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export interface
|
|
79
|
-
cost:
|
|
78
|
+
export interface FigureReport {
|
|
79
|
+
cost: FigureCost
|
|
80
80
|
capacity: FigureCapacity
|
|
81
|
-
score:
|
|
81
|
+
score: FigureScore
|
|
82
82
|
/** Distinct shapes and materials among the drawn bodies — the 「그리는 비용」 rows. */
|
|
83
83
|
distinctShapes: number
|
|
84
84
|
distinctMaterials: number
|
|
85
85
|
reuse: { distinct: number; parts: number; saved: number }
|
|
86
|
-
axes:
|
|
87
|
-
curve:
|
|
86
|
+
axes: FigureMaturityAxis[]
|
|
87
|
+
curve: FigureCurvePoint[]
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** The board target for the scale axis: 500 assets on one drawing (design document §4.15). */
|
|
@@ -94,27 +94,27 @@ export const BOARD_ASSETS = 500
|
|
|
94
94
|
* The report for one asset, or nothing when no body is drawn — then there is nothing to measure, and the screen
|
|
95
95
|
* says so rather than drawing zeros.
|
|
96
96
|
*
|
|
97
|
-
* Evaluated at the asset's own design and state defaults, as `
|
|
97
|
+
* Evaluated at the asset's own design and state defaults, as `figureCostOf` is.
|
|
98
98
|
*/
|
|
99
|
-
export function
|
|
100
|
-
const cost =
|
|
99
|
+
export function figureReportOf(asset: FigureAsset): FigureReport | undefined {
|
|
100
|
+
const cost = figureCostOf(asset)
|
|
101
101
|
const capacity = capacityFrom(cost.groups, cost.triangles)
|
|
102
102
|
if (!capacity) return undefined
|
|
103
103
|
|
|
104
|
-
const drawn =
|
|
104
|
+
const drawn = compileFigureAsset(asset)
|
|
105
105
|
.evaluate()
|
|
106
106
|
.geometry.filter(body => body.visible !== false)
|
|
107
|
-
const plan =
|
|
108
|
-
const writers =
|
|
107
|
+
const plan = compileFigureDocument(asset.document)
|
|
108
|
+
const writers = figureWritersOf(asset)
|
|
109
109
|
|
|
110
110
|
const shapes = new Set(drawn.map(shapeKeyOf))
|
|
111
111
|
const materials = new Set(drawn.map(materialKeyOf))
|
|
112
112
|
|
|
113
|
-
const score =
|
|
113
|
+
const score = figureScoreOf(asset)!
|
|
114
114
|
|
|
115
115
|
const reuse = { distinct: shapes.size, parts: drawn.length, saved: drawn.length - shapes.size }
|
|
116
116
|
|
|
117
|
-
const axes:
|
|
117
|
+
const axes: FigureMaturityAxis[] = [
|
|
118
118
|
{
|
|
119
119
|
key: 'scale',
|
|
120
120
|
// The board target filled is 1; room beyond it is better spent on the other axes, so it does not count more.
|
|
@@ -154,10 +154,10 @@ export function v3ReportOf(asset: V3Asset): V3Report | undefined {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* The key things-scene shares a body's geometry by (`figure-
|
|
157
|
+
* The key things-scene shares a body's geometry by (`figure-geometry.ts` `keyOf`): provider, dimensions to a
|
|
158
158
|
* micrometre, segments (12 when unset), and a polygon's corners.
|
|
159
159
|
*/
|
|
160
|
-
export function shapeKeyOf(body:
|
|
160
|
+
export function shapeKeyOf(body: FigureAssetGeometry | FigureGeometry): string {
|
|
161
161
|
if (!('shape' in body)) {
|
|
162
162
|
// A scalar body has no mesh of its own; it is still one drawn body, told apart by what it declares.
|
|
163
163
|
const dims = Object.keys(body.dimensions).sort().map(k => `${k}=${body.dimensions[k]}`).join(',')
|
|
@@ -175,21 +175,21 @@ export function shapeKeyOf(body: V3AssetGeometry | V3Geometry): string {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* The key things-scene shares a material by (`figure-
|
|
178
|
+
* The key things-scene shares a material by (`figure-rig.ts` `materialFor`), within one asset: token, preset,
|
|
179
179
|
* transparency, and the light when it is on. The colour follows from the token, so it is not a second key.
|
|
180
180
|
*/
|
|
181
|
-
export function materialKeyOf(body:
|
|
182
|
-
return
|
|
181
|
+
export function materialKeyOf(body: FigureAssetGeometry): string {
|
|
182
|
+
return figureMaterialKeyOf(body)
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Does it look like one set — baselines 3 and 4, three checks.
|
|
187
187
|
*
|
|
188
|
-
* Two of them hold by construction
|
|
189
|
-
* format refuses a segment count outside the presets (`
|
|
188
|
+
* Two of them hold by construction: the material has no surface field, so there is no texture, and the
|
|
189
|
+
* format refuses a segment count outside the presets (`compileFigureAsset`, `QUALITY`). They are still counted, so
|
|
190
190
|
* the ratio stays one fault in three.
|
|
191
191
|
*/
|
|
192
|
-
function consistency(asset:
|
|
192
|
+
function consistency(asset: FigureAsset, distinctMaterials: number): FigureMaturityAxis {
|
|
193
193
|
const textures = 0
|
|
194
194
|
const offPreset = asset.appearance.filter(look => look.segments !== undefined && !SEGMENT_PRESETS.includes(look.segments)).length
|
|
195
195
|
const faults = (textures > 0 ? 1 : 0) + (offPreset > 0 ? 1 : 0) + (distinctMaterials > LIMITS.distinctMaterials ? 1 : 0)
|
|
@@ -201,7 +201,7 @@ function consistency(asset: V3Asset, distinctMaterials: number): V3MaturityAxis
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
/** Seen from above, these do not fill their footprint box, by
|
|
204
|
+
/** Seen from above, these do not fill their footprint box, by provider name. */
|
|
205
205
|
const ROUND_FOOTPRINT: readonly string[] = ['cylinder', 'ellipticCylinder', 'frustum', 'sphere']
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -211,20 +211,20 @@ const ROUND_FOOTPRINT: readonly string[] = ['cylinder', 'ellipticCylinder', 'fru
|
|
|
211
211
|
* finer segment count does not raise it: H = −Σ p·log p over each material's share of the footprint plus the
|
|
212
212
|
* background, divided by log(`LIMITS.distinctMaterials` + 1), the most this measure can reach.
|
|
213
213
|
*
|
|
214
|
-
* The footprint is the base box: the space the asset declares when it declares one (ADR-0044's box is
|
|
214
|
+
* The footprint is the base box: the space the asset declares when it declares one (ADR-0044's box is the model's
|
|
215
215
|
* occupancy), otherwise the drawn bodies' own extent. Bodies are their axis-aligned world boxes
|
|
216
|
-
* (`
|
|
216
|
+
* (`figureWorldBoxesOf`, the gate's reading); the higher one covers the lower.
|
|
217
217
|
*/
|
|
218
218
|
function readable(
|
|
219
|
-
asset:
|
|
220
|
-
plan: ReturnType<typeof
|
|
221
|
-
drawn:
|
|
219
|
+
asset: FigureAsset,
|
|
220
|
+
plan: ReturnType<typeof compileFigureDocument>,
|
|
221
|
+
drawn: FigureAssetGeometry[],
|
|
222
222
|
writers: Map<string, string>
|
|
223
|
-
):
|
|
223
|
+
): FigureMaturityAxis {
|
|
224
224
|
const placed = drawn
|
|
225
225
|
.filter(body => 'pose' in body)
|
|
226
226
|
// One placed, drawn body gives exactly one box; boxes are taken one by one to keep each with its material.
|
|
227
|
-
.map(body => ({ body, box:
|
|
227
|
+
.map(body => ({ body, box: figureWorldBoxesOf([body], writers)[0]! }))
|
|
228
228
|
if (placed.length === 0) return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
229
229
|
|
|
230
230
|
let frame: { x0: number; x1: number; z0: number; z1: number }
|
|
@@ -260,7 +260,7 @@ function readable(
|
|
|
260
260
|
|
|
261
261
|
/** Which material shows from above, on a 64 × 64 grid over the footprint, over world boxes. */
|
|
262
262
|
function topViewRegions(
|
|
263
|
-
placed: { body:
|
|
263
|
+
placed: { body: FigureAssetGeometry; box: ReturnType<typeof figureWorldBoxesOf>[number] }[],
|
|
264
264
|
frame: { x0: number; x1: number; z0: number; z1: number }
|
|
265
265
|
) {
|
|
266
266
|
const GRID = 64
|
|
@@ -320,13 +320,13 @@ export function decadeSteps(from: number, to: number): number[] {
|
|
|
320
320
|
|
|
321
321
|
// ─── The trend chart ─────────────────────────────────────────────────────────
|
|
322
322
|
|
|
323
|
-
export type
|
|
323
|
+
export type FigureCurveSeries = 'triangles' | 'drawCalls' | 'geometries' | 'materials'
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
326
|
* The four lines: two that grow with the instance count and two that stay flat — the flat ones are what the
|
|
327
327
|
* instances share. Materials are dashed and drawn last, so a line under them still shows where the two meet.
|
|
328
328
|
*/
|
|
329
|
-
export const CURVE_SERIES: readonly { key:
|
|
329
|
+
export const CURVE_SERIES: readonly { key: FigureCurveSeries; flat: boolean; dashed?: boolean }[] = [
|
|
330
330
|
{ key: 'triangles', flat: false },
|
|
331
331
|
{ key: 'drawCalls', flat: false },
|
|
332
332
|
{ key: 'geometries', flat: true },
|
|
@@ -336,10 +336,10 @@ export const CURVE_SERIES: readonly { key: V3CurveSeries; flat: boolean; dashed?
|
|
|
336
336
|
/** The chart's size. The chart is shown at this size in a sheet; shrunk into the side, its words are unreadable. */
|
|
337
337
|
export const CURVE_SIZE = { width: 680, height: 380, pad: { top: 52, right: 208, bottom: 44, left: 62 } } as const
|
|
338
338
|
|
|
339
|
-
export interface
|
|
339
|
+
export interface FigureCurveLayout {
|
|
340
340
|
xTicks: { x: number; label: string }[]
|
|
341
341
|
yTicks: { y: number; label: string }[]
|
|
342
|
-
lines: { key:
|
|
342
|
+
lines: { key: FigureCurveSeries; d: string; flat: boolean; dashed: boolean; last: number }[]
|
|
343
343
|
/** The value the flat lines share when they lie on one another, so the screen can say why one line is missing. */
|
|
344
344
|
overlap?: number
|
|
345
345
|
}
|
|
@@ -354,7 +354,7 @@ export function shortNumber(v: number): string {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
/** Where the chart puts each line and tick. Both axes are logarithmic. */
|
|
357
|
-
export function curveLayout(points:
|
|
357
|
+
export function curveLayout(points: FigureCurvePoint[]): FigureCurveLayout | undefined {
|
|
358
358
|
if (points.length === 0) return undefined
|
|
359
359
|
const { width, height, pad } = CURVE_SIZE
|
|
360
360
|
const minX = points[0]!.instances
|
|
@@ -369,9 +369,9 @@ export function curveLayout(points: V3CurvePoint[]): V3CurveLayout | undefined {
|
|
|
369
369
|
const px = (v: number) => pad.left + ((log10(v) - log10(minX)) / spanX) * plotW
|
|
370
370
|
const py = (v: number) => pad.top + plotH - (log10(v) / spanY) * plotH
|
|
371
371
|
|
|
372
|
-
const yTicks:
|
|
372
|
+
const yTicks: FigureCurveLayout['yTicks'] = []
|
|
373
373
|
for (let e = 0; Math.pow(10, e) <= maxY * 1.001; e++) yTicks.push({ y: py(Math.pow(10, e)), label: shortNumber(Math.pow(10, e)) })
|
|
374
|
-
const xTicks:
|
|
374
|
+
const xTicks: FigureCurveLayout['xTicks'] = []
|
|
375
375
|
for (let e = 0; Math.pow(10, e) <= maxX * 1.001; e++) {
|
|
376
376
|
const v = Math.pow(10, e)
|
|
377
377
|
if (v >= minX) xTicks.push({ x: px(v), label: shortNumber(v) })
|
|
@@ -390,7 +390,7 @@ export function curveLayout(points: V3CurvePoint[]): V3CurveLayout | undefined {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
/** What the parts list shows under its title: the cost, how many fit, the grade and what decides it. */
|
|
393
|
-
export interface
|
|
393
|
+
export interface FigureBudgetLine {
|
|
394
394
|
triangles: number
|
|
395
395
|
groups: number
|
|
396
396
|
instances: number
|
|
@@ -398,7 +398,7 @@ export interface V3BudgetLine {
|
|
|
398
398
|
tightest: { key: string; used: number; limit: number }
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
export function budgetOf(report:
|
|
401
|
+
export function budgetOf(report: FigureReport | undefined): FigureBudgetLine | undefined {
|
|
402
402
|
if (!report) return undefined
|
|
403
403
|
const { tightest } = report.score
|
|
404
404
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one modeller. The catalogue, a new figure and an old address all open the same page. `figure-
|
|
2
|
+
* The one modeller. The catalogue, a new figure and an old address all open the same page. `figure-modeller`
|
|
3
3
|
* stays as an alias so a link written under that name still lands.
|
|
4
4
|
*/
|
|
5
5
|
export const MODELLER = 'figure-modeller'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseFigureAsset, proposeFigureOccupancy, type FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* 「부품에 맞추기」의
|
|
4
|
+
* 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.
|
|
5
5
|
*
|
|
6
|
-
* figure-model 은 이것을 둘로 나눠 두었다: 제안(`
|
|
6
|
+
* figure-model 은 이것을 둘로 나눠 두었다: 제안(`proposeFigureOccupancy`)은 무엇이 선언될지를 보여 주고, 확인
|
|
7
7
|
* (`declare-occupancy`)은 그 제안이 읽은 모양 그대로일 때만 받는다. 저작자가 누른 단추가 그 확인이다. 제안과 확인
|
|
8
8
|
* 사이에 모델이 바뀌면 figure-model 이 `OCCUPANCY_STALE` 로 거절하므로, 여기서는 누른 순간의 원본으로 둘을 한
|
|
9
9
|
* 번에 만든다.
|
|
@@ -16,11 +16,11 @@ export function occupancyFromParts(
|
|
|
16
16
|
source: string,
|
|
17
17
|
/** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */
|
|
18
18
|
placement?: 'floor' | 'ceiling' | 'center' | 'wall'
|
|
19
|
-
): Extract<
|
|
20
|
-
const asset =
|
|
19
|
+
): Extract<FigureAuthoringAction, { kind: 'declare-occupancy' }> {
|
|
20
|
+
const asset = parseFigureAsset(source)
|
|
21
21
|
return {
|
|
22
22
|
kind: 'declare-occupancy',
|
|
23
|
-
proposal:
|
|
23
|
+
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
24
24
|
placement: placement ?? asset.occupancy?.placement ?? 'floor'
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FigurePrimitiveCreation } from '@hatiolab/figure-model'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { FigurePaletteKind } from './figure-parts.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* What a palette button makes: the part at a starting size a person then changes. Standing on the floor (its lowest
|
|
7
7
|
* point at y 0) where the provider's frame is centred; a portal's frame already rises from its bottom.
|
|
8
8
|
*/
|
|
9
|
-
export function paletteRecipe(kind:
|
|
9
|
+
export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimitiveCreation {
|
|
10
10
|
const standing = (height: number) => ({ id, position: { x: 0, y: height / 2, z: 0 } })
|
|
11
11
|
switch (kind) {
|
|
12
12
|
case 'box':
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { compileFigureAsset, figureOccupiedBoxesOf, figureWritersOf, FIGURE_INSTANCE_SIZE_INPUTS } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
|
-
/* The preview's arithmetic, apart from the element so it is tested without a browser (see figure-
|
|
4
|
+
/* The preview's arithmetic, apart from the element so it is tested without a browser (see figure-preview.ts). */
|
|
5
5
|
|
|
6
6
|
export const PREVIEW_FACTOR = 2
|
|
7
7
|
|
|
@@ -17,12 +17,12 @@ export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' |
|
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
/** The size the figure was made at: its instance size when it declares one, else the room its bodies take. */
|
|
20
|
-
export function authoredSizeOf(asset:
|
|
20
|
+
export function authoredSizeOf(asset: FigureAsset): { x: number; y: number; z: number } {
|
|
21
21
|
const d = asset.designInputs
|
|
22
|
-
const declared = (['x', 'y', 'z'] as const).map(axis => d[
|
|
22
|
+
const declared = (['x', 'y', 'z'] as const).map(axis => d[FIGURE_INSTANCE_SIZE_INPUTS[axis]])
|
|
23
23
|
if (declared.every(v => typeof v === 'number' && v > 0)) return { x: declared[0]!, y: declared[1]!, z: declared[2]! }
|
|
24
24
|
/* No declared size: the room its bodies take up, so the instance is placed at the size it draws. */
|
|
25
|
-
const boxes =
|
|
25
|
+
const boxes = figureOccupiedBoxesOf(compileFigureAsset(asset).evaluate().geometry, figureWritersOf(asset))
|
|
26
26
|
const span = (axis: 'x' | 'y' | 'z') =>
|
|
27
27
|
boxes.length ? Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2)) - Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2)) : 0
|
|
28
28
|
return { x: Math.max(1, Math.round(span('x'))), y: Math.max(1, Math.round(span('y'))), z: Math.max(1, Math.round(span('z'))) }
|
|
@@ -59,13 +59,13 @@ export function previewInstances(type: string, size: { x: number; y: number; z:
|
|
|
59
59
|
* it is drawn as asked. A figure that follows its height only is drawn at its one width in the wider variants, and
|
|
60
60
|
* the preview says so rather than drawing them as though they grew.
|
|
61
61
|
*/
|
|
62
|
-
export function previewClampsOf(asset:
|
|
62
|
+
export function previewClampsOf(asset: FigureAsset, size: { x: number; y: number; z: number }): Record<string, Array<{ axis: 'x' | 'y' | 'z'; asked: number; used: number }>> {
|
|
63
63
|
const inputs = asset.document.model.inputs
|
|
64
64
|
return Object.fromEntries(
|
|
65
65
|
PREVIEW_WAYS.map(way => [
|
|
66
66
|
way.name,
|
|
67
67
|
(['x', 'y', 'z'] as const).flatMap(axis => {
|
|
68
|
-
const input = inputs.find(i => i.id ===
|
|
68
|
+
const input = inputs.find(i => i.id === FIGURE_INSTANCE_SIZE_INPUTS[axis])
|
|
69
69
|
if (!input) return []
|
|
70
70
|
const asked = Math.round(size[axis] * (way.axes.includes(axis) ? PREVIEW_FACTOR : 1))
|
|
71
71
|
const used = Math.round(Math.min(Math.max(asked, input.min ?? -Infinity), input.max ?? Infinity))
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
type
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
type
|
|
2
|
+
createFigureAsset,
|
|
3
|
+
createFigureEditorWorkspace,
|
|
4
|
+
serializeFigureAsset,
|
|
5
|
+
parseFigureAsset,
|
|
6
|
+
type FigureInputChange,
|
|
7
|
+
type FigurePrimitiveCreation,
|
|
8
|
+
type FigurePartEdit,
|
|
9
|
+
type FigureAuthoringAction,
|
|
10
|
+
type FigureProposalStep
|
|
11
11
|
} from '@hatiolab/figure-model'
|
|
12
12
|
|
|
13
|
-
export interface
|
|
13
|
+
export interface FigureProductRecord {
|
|
14
14
|
id: string
|
|
15
15
|
source: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */
|
|
19
|
-
export interface
|
|
20
|
-
load(id: string): Promise<
|
|
19
|
+
export interface FigureProductStore {
|
|
20
|
+
load(id: string): Promise<FigureProductRecord>
|
|
21
21
|
save(request: {
|
|
22
22
|
id?: string
|
|
23
23
|
source: string
|
|
@@ -27,12 +27,12 @@ export interface V3ProductStore {
|
|
|
27
27
|
thumbnail?: string
|
|
28
28
|
/** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */
|
|
29
29
|
type?: string
|
|
30
|
-
}): Promise<
|
|
30
|
+
}): Promise<FigureProductRecord>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
|
|
34
|
-
export function
|
|
35
|
-
const workspace =
|
|
34
|
+
export function createFigureProductSession(store: FigureProductStore) {
|
|
35
|
+
const workspace = createFigureEditorWorkspace()
|
|
36
36
|
let epoch = 0,
|
|
37
37
|
id: string | undefined,
|
|
38
38
|
baseline: string | undefined
|
|
@@ -69,7 +69,7 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
69
69
|
++epoch
|
|
70
70
|
clear()
|
|
71
71
|
loading = false
|
|
72
|
-
workspace.open(
|
|
72
|
+
workspace.open(serializeFigureAsset(createFigureAsset('새 Figure')))
|
|
73
73
|
creationId = crypto.randomUUID()
|
|
74
74
|
return read()
|
|
75
75
|
},
|
|
@@ -99,7 +99,7 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
99
99
|
loading = false
|
|
100
100
|
return read()
|
|
101
101
|
},
|
|
102
|
-
apply(changes:
|
|
102
|
+
apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {
|
|
103
103
|
const control = writable()
|
|
104
104
|
// AI must supply the context AND revision captured when it started. Never substitute current values.
|
|
105
105
|
if (origin === 'ai' && (context === undefined || expectedRevision === undefined))
|
|
@@ -116,7 +116,7 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
116
116
|
* An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.
|
|
117
117
|
* Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.
|
|
118
118
|
*/
|
|
119
|
-
applyProposal(steps: readonly
|
|
119
|
+
applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string) {
|
|
120
120
|
writable()
|
|
121
121
|
return workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps })
|
|
122
122
|
},
|
|
@@ -124,18 +124,18 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
124
124
|
* A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open
|
|
125
125
|
* now. The same path as a proposal; the person is its origin.
|
|
126
126
|
*/
|
|
127
|
-
applyRecipe(steps: readonly
|
|
127
|
+
applyRecipe(steps: readonly FigureProposalStep[]) {
|
|
128
128
|
const at = writable()
|
|
129
129
|
return workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps })
|
|
130
130
|
},
|
|
131
|
-
add(part:
|
|
131
|
+
add(part: FigurePrimitiveCreation) {
|
|
132
132
|
return workspace.addPrimitive({ ...writable(), part })
|
|
133
133
|
},
|
|
134
|
-
edit(edit:
|
|
134
|
+
edit(edit: FigurePartEdit) {
|
|
135
135
|
return workspace.editPart({ ...writable(), edit })
|
|
136
136
|
},
|
|
137
137
|
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
138
|
-
command(action:
|
|
138
|
+
command(action: FigureAuthoringAction) {
|
|
139
139
|
return workspace.command({ ...writable(), action })
|
|
140
140
|
},
|
|
141
141
|
undo() {
|
|
@@ -153,7 +153,7 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
153
153
|
*
|
|
154
154
|
* The picture is the caller's because taking it needs a canvas, and this session is the place that knows
|
|
155
155
|
* nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the
|
|
156
|
-
* source matters more than the card (see `
|
|
156
|
+
* source matters more than the card (see `thumbnail.ts`).
|
|
157
157
|
*/
|
|
158
158
|
async save({ thumbnail, type }: { thumbnail?: string; type?: string } = {}) {
|
|
159
159
|
const control = writable(),
|
|
@@ -164,7 +164,7 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
164
164
|
const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, ...(id || !type ? {} : { type }) })
|
|
165
165
|
if (requestEpoch !== epoch) return read()
|
|
166
166
|
if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')
|
|
167
|
-
if (
|
|
167
|
+
if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)
|
|
168
168
|
throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')
|
|
169
169
|
workspace.acknowledgeSaved(exported)
|
|
170
170
|
id = record.id
|
|
@@ -176,4 +176,4 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
export type
|
|
179
|
+
export type FigureProductSession = ReturnType<typeof createFigureProductSession>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { fetchRecipeGuide, fetchRecipeTemplates } from '../graphql/index.js'
|
|
2
|
-
import { templatesOf, type RecipeTemplate } from './
|
|
2
|
+
import { templatesOf, type RecipeTemplate } from './recipes.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Where the modeller gets its recipe templates: the server's static files (ADR-0095, amended 2026-09-26). The one place
|
|
6
|
-
* they are read -- the rules in `
|
|
6
|
+
* they are read -- the rules in `recipes.ts` stay free of the network so they are tested without it.
|
|
7
7
|
*/
|
|
8
8
|
export async function availableRecipes(lang: string): Promise<RecipeTemplate[]> {
|
|
9
9
|
return templatesOf(await fetchRecipeTemplates(lang))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { describeFigureRecipe } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureRecipe, FigureRecipeParam, FigureRecipeValue } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
import type { RecipeTemplateRow } from '../graphql/index.js'
|
|
5
5
|
|
|
6
6
|
/** A template in the catalog: the server's row, its recipe parsed. */
|
|
7
7
|
export interface RecipeTemplate extends Omit<RecipeTemplateRow, 'source'> {
|
|
8
|
-
recipe:
|
|
8
|
+
recipe: FigureRecipe
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/*
|
|
@@ -30,11 +30,11 @@ export function templatesOf(rows: ReadonlyArray<RecipeTemplateRow>): RecipeTempl
|
|
|
30
30
|
export const recipePartName = (part: string) => part.replace(/^\{id\}-/, '')
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* How a template works, in lines the catalog shows -- worked out from its steps (figure-model `
|
|
33
|
+
* How a template works, in lines the catalog shows -- worked out from its steps (figure-model `describeFigureRecipe`), each
|
|
34
34
|
* a translation key and its words.
|
|
35
35
|
*/
|
|
36
|
-
export function recipeWorkings(recipe:
|
|
37
|
-
const d =
|
|
36
|
+
export function recipeWorkings(recipe: FigureRecipe): Array<{ key: string; words: Record<string, string | number> }> {
|
|
37
|
+
const d = describeFigureRecipe(recipe)
|
|
38
38
|
const lines: Array<{ key: string; words: Record<string, string | number> }> = []
|
|
39
39
|
for (const m of d.moves)
|
|
40
40
|
lines.push(
|
|
@@ -55,7 +55,7 @@ export function recipeWord(words: Record<string, string> | undefined, language:
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Whether a recipe can be put in now: one for a part needs a part chosen. */
|
|
58
|
-
export function recipeReady(recipe: Pick<
|
|
58
|
+
export function recipeReady(recipe: Pick<FigureRecipe, 'target'>, selected: string): boolean {
|
|
59
59
|
return recipe.target === 'figure' || !!selected
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ export function recipeReady(recipe: Pick<V3Recipe, 'target'>, selected: string):
|
|
|
63
63
|
* A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a
|
|
64
64
|
* person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.
|
|
65
65
|
*/
|
|
66
|
-
export function recipeValueOf(param:
|
|
66
|
+
export function recipeValueOf(param: FigureRecipeParam, typed: string | boolean): FigureRecipeValue {
|
|
67
67
|
if (param.kind === 'flag') return typed === true || typed === 'true'
|
|
68
68
|
if (param.kind === 'ratio' || param.kind === 'mm') {
|
|
69
69
|
const n = typeof typed === 'string' && typed.trim() !== '' ? Number(typed) : NaN
|
|
@@ -75,7 +75,7 @@ export function recipeValueOf(param: V3RecipeParam, typed: string | boolean): V3
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** What a field shows for a value: a ratio as a percentage. */
|
|
78
|
-
export function recipeFieldOf(param:
|
|
78
|
+
export function recipeFieldOf(param: FigureRecipeParam, value: FigureRecipeValue): string {
|
|
79
79
|
if (param.kind === 'ratio' && typeof value === 'number') return String(Math.round(value * 1e6) / 1e4)
|
|
80
80
|
return String(value)
|
|
81
81
|
}
|