@things-factory/figure-ui 10.1.15 → 10.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/figure-source.ts +32 -6
- package/client/pages/figure-modeller-page.ts +81 -43
- package/dist-client/modeller/figure-source.d.ts +21 -8
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +38 -2
- package/dist-client/pages/figure-modeller-page.js +77 -41
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -62,16 +62,32 @@ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePl
|
|
|
62
62
|
* is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the
|
|
63
63
|
* floor no matter what the sample said; the fourth as a hollowed tray that
|
|
64
64
|
* came out a solid block.
|
|
65
|
+
*
|
|
66
|
+
* All four passed the round-trip test, because the fixture it uses did not
|
|
67
|
+
* carry the field. So the test now reads the **field list the format declares**
|
|
68
|
+
* rather than trusting the fixture.
|
|
69
|
+
*
|
|
70
|
+
* `shape` is unpacked into `round`, `path` and `hollow` rather than carried
|
|
71
|
+
* whole, because the editor holds those as separate state. Every field added
|
|
72
|
+
* under `shape` costs a line here, and forgetting that line was the fourth
|
|
73
|
+
* of the four.
|
|
65
74
|
*/
|
|
66
75
|
/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */
|
|
67
76
|
export interface BoardModel {
|
|
68
77
|
/** 형식 버전. */
|
|
69
78
|
version?: number
|
|
70
|
-
|
|
79
|
+
/*
|
|
80
|
+
The three sizes keep the scene component's names, and the scene assigns its axes differently
|
|
81
|
+
from the format: here `depth` is the vertical one. Naming the axis alone ("the y axis") does
|
|
82
|
+
not say which of the two conventions is meant, and this interface sits on the boundary between
|
|
83
|
+
them — so each field names the `FigureSource` field it becomes. `toFigureSource` and
|
|
84
|
+
`fromFigureSource` below are the only places that may disagree with these three lines.
|
|
85
|
+
*/
|
|
86
|
+
/** 가로 너비 (mm) → `base.x`. */
|
|
71
87
|
width: number
|
|
72
|
-
/**
|
|
88
|
+
/** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */
|
|
73
89
|
height: number
|
|
74
|
-
/** 높이 (mm).
|
|
90
|
+
/** 높이 (mm) → `base.y`. 서 있는 방향이다. */
|
|
75
91
|
depth: number
|
|
76
92
|
/** 컴포넌트 타입 이름. **저장되는 식별자다.** */
|
|
77
93
|
figureType?: string
|
|
@@ -136,11 +152,21 @@ export interface PartModel {
|
|
|
136
152
|
|
|
137
153
|
/** 색 토큰 이름 (`palette.primary` 등). */
|
|
138
154
|
token?: string
|
|
139
|
-
|
|
155
|
+
/*
|
|
156
|
+
`FigurePart['material']` is required in the format, so these index into it directly. The
|
|
157
|
+
`NonNullable` on `shape` above is not the same case — `shape` is optional there.
|
|
158
|
+
*/
|
|
159
|
+
preset?: FigurePart['material']['preset']
|
|
140
160
|
flatShading?: boolean
|
|
141
161
|
transparent?: boolean
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
/**
|
|
163
|
+
* 자체발광. 없으면 안 빛난다.
|
|
164
|
+
*
|
|
165
|
+
* 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진
|
|
166
|
+
* 전구는 빨갛지 않고 어두운 플라스틱이다.
|
|
167
|
+
*/
|
|
168
|
+
emissive?: FigurePart['material']['emissive']
|
|
169
|
+
surface?: FigurePart['material']['surface']
|
|
144
170
|
|
|
145
171
|
/* 아직 아무 일도 안 하지만 잃어버리지 않는다. */
|
|
146
172
|
materialSlot?: FigurePart['materialSlot']
|
|
@@ -719,23 +719,26 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
719
719
|
}))
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
-
|
|
722
|
+
/*
|
|
723
|
+
`detail.staged` is the dock's answer to "did anything actually happen to my candidate". The
|
|
724
|
+
listener is synchronous, so the dock reads it right after dispatching and only then tells the
|
|
725
|
+
chat card to say so. It used to be called `accepted`, which claimed more than it knew — the
|
|
726
|
+
page had put the candidate somewhere, not the person had agreed to it.
|
|
727
|
+
*/
|
|
728
|
+
private receiveDockProposal = (event: Event) => {
|
|
723
729
|
const detail = (event as CustomEvent).detail
|
|
724
730
|
if (!detail) return
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
|
|
731
|
+
|
|
732
|
+
const source =
|
|
733
|
+
detail?.source ||
|
|
734
|
+
(detail?.proposal?.source
|
|
735
|
+
? typeof detail.proposal.source === 'string'
|
|
736
|
+
? JSON.parse(detail.proposal.source)
|
|
737
|
+
: detail.proposal.source
|
|
738
|
+
: undefined)
|
|
739
|
+
|
|
736
740
|
if (source) {
|
|
737
|
-
this.
|
|
738
|
-
detail.accepted = true
|
|
741
|
+
detail.staged = this.openProposalForReview(source, detail.proposal)
|
|
739
742
|
}
|
|
740
743
|
}
|
|
741
744
|
|
|
@@ -743,36 +746,71 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
743
746
|
const detail = (event as CustomEvent).detail
|
|
744
747
|
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
|
|
745
748
|
if (source) {
|
|
746
|
-
this.
|
|
749
|
+
this.openProposalForReview(source, detail.proposal)
|
|
747
750
|
}
|
|
748
751
|
}
|
|
749
752
|
|
|
750
|
-
|
|
753
|
+
/**
|
|
754
|
+
* A candidate from the dock opens the review lane. It does not become the draft.
|
|
755
|
+
*
|
|
756
|
+
* This used to write `board` and `parts` straight onto the working draft and then set
|
|
757
|
+
* `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
|
|
758
|
+
* erased the lane built to review it. There was no moment where the person said yes, and a
|
|
759
|
+
* draft has no version history to go back to, so the previous work was simply gone.
|
|
760
|
+
*
|
|
761
|
+
* `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
|
|
762
|
+
* and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
|
|
763
|
+
* `figure-ask` entrance already honoured it through `openAiProposal`; the dock entrance is now
|
|
764
|
+
* the same door. Applying is what the take button does, from the lane, per change.
|
|
765
|
+
*
|
|
766
|
+
* ## Why the overwrite was there — so it does not come back
|
|
767
|
+
*
|
|
768
|
+
* The dock opened the review lane originally. Before `c42a0b63df` this handler read:
|
|
769
|
+
*
|
|
770
|
+
* if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
|
|
771
|
+
* if (detail?.draftType && detail.draftType !== this.folded?.type) return
|
|
772
|
+
* if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
|
|
773
|
+
* this.receive(detail.proposal)
|
|
774
|
+
*
|
|
775
|
+
* Three guards, each a silent `return`, and the last compared a serialized source byte for byte.
|
|
776
|
+
* Any difference that changed nothing at all dropped the candidate with no message, so the dock
|
|
777
|
+
* would report a candidate and the screen would show none.
|
|
778
|
+
*
|
|
779
|
+
* The `ox-assistant-chat` migration fixed that by replacing the path rather than the guards, and
|
|
780
|
+
* the review lane went out with them — `applySourceDirectly` wrote straight to the draft, and
|
|
781
|
+
* cleared `proposalSession` on the way past. **Nobody decided that reviews were unwanted here.**
|
|
782
|
+
* It was the cost of loosening a guard by deleting what stood around it.
|
|
783
|
+
*
|
|
784
|
+
* So the guards are gone and stay gone, and the lane is back. If a candidate ever seems not to
|
|
785
|
+
* arrive, the thing to look for is a silent `return` on this path — not the lane.
|
|
786
|
+
*
|
|
787
|
+
* Returns whether the lane opened, so the dock can tell the chat whether to say it staged.
|
|
788
|
+
*/
|
|
789
|
+
private openProposalForReview(rawSource: any, proposal?: any): boolean {
|
|
751
790
|
const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource
|
|
752
|
-
if (!source || !Array.isArray(source.parts)) return
|
|
791
|
+
if (!source || !Array.isArray(source.parts)) return false
|
|
753
792
|
|
|
754
|
-
this.recordHistory()
|
|
755
793
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.
|
|
771
|
-
|
|
794
|
+
/*
|
|
795
|
+
The dock sends a FigureProposal whose metrics sit at the top level; the staged-on-navigate
|
|
796
|
+
path carries the raw tool result, which nests them under score/cost. Read both rather than
|
|
797
|
+
making one of the two entrances reshape its payload.
|
|
798
|
+
*/
|
|
799
|
+
this.proposalSession = proposalSessions.openAiProposal(this.folded, {
|
|
800
|
+
source: JSON.stringify(source),
|
|
801
|
+
grade: proposal?.grade ?? proposal?.score?.grade,
|
|
802
|
+
triangles: proposal?.triangles ?? proposal?.cost?.triangles,
|
|
803
|
+
groups: proposal?.groups ?? proposal?.cost?.groups,
|
|
804
|
+
attempts: proposal?.attempts,
|
|
805
|
+
quality: proposal?.quality
|
|
806
|
+
} as FigureProposal)
|
|
807
|
+
|
|
808
|
+
this.mode = 'edit'
|
|
772
809
|
this.requestUpdate()
|
|
773
|
-
|
|
810
|
+
return true
|
|
774
811
|
} catch (e) {
|
|
775
|
-
console.error('[FigureModeller] Failed to
|
|
812
|
+
console.error('[FigureModeller] Failed to open the AI candidate for review:', e)
|
|
813
|
+
return false
|
|
776
814
|
}
|
|
777
815
|
}
|
|
778
816
|
|
|
@@ -781,7 +819,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
781
819
|
connectedCallback() {
|
|
782
820
|
super.connectedCallback()
|
|
783
821
|
window.addEventListener('figure-ai-context-request', this.announceAiContext)
|
|
784
|
-
window.addEventListener('figure-ai-review', this.
|
|
822
|
+
window.addEventListener('figure-ai-review', this.receiveDockProposal)
|
|
785
823
|
window.addEventListener('figure-ai-stage', this.onFigureAiStage)
|
|
786
824
|
window.addEventListener('figure-save-request', this.onSaveRequest)
|
|
787
825
|
window.addEventListener('keydown', this.handleKeyDown)
|
|
@@ -792,7 +830,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
792
830
|
window.removeEventListener('figure-save-request', this.onSaveRequest)
|
|
793
831
|
window.removeEventListener('figure-ai-stage', this.onFigureAiStage)
|
|
794
832
|
window.removeEventListener('figure-ai-context-request', this.announceAiContext)
|
|
795
|
-
window.removeEventListener('figure-ai-review', this.
|
|
833
|
+
window.removeEventListener('figure-ai-review', this.receiveDockProposal)
|
|
796
834
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
|
|
797
835
|
super.disconnectedCallback()
|
|
798
836
|
}
|
|
@@ -1286,7 +1324,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1286
1324
|
}
|
|
1287
1325
|
const pending = consumePendingFigureProposal()
|
|
1288
1326
|
if (pending?.source) {
|
|
1289
|
-
this.
|
|
1327
|
+
this.openProposalForReview(pending.source, pending.proposal)
|
|
1290
1328
|
}
|
|
1291
1329
|
return
|
|
1292
1330
|
}
|
|
@@ -1307,7 +1345,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1307
1345
|
this.startNew()
|
|
1308
1346
|
const pending = consumePendingFigureProposal()
|
|
1309
1347
|
if (pending?.source) {
|
|
1310
|
-
this.
|
|
1348
|
+
this.openProposalForReview(pending.source, pending.proposal)
|
|
1311
1349
|
}
|
|
1312
1350
|
return
|
|
1313
1351
|
}
|
|
@@ -1316,7 +1354,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1316
1354
|
this.startNew()
|
|
1317
1355
|
const pending = consumePendingFigureProposal()
|
|
1318
1356
|
if (pending?.source) {
|
|
1319
|
-
this.
|
|
1357
|
+
this.openProposalForReview(pending.source, pending.proposal)
|
|
1320
1358
|
}
|
|
1321
1359
|
return
|
|
1322
1360
|
}
|
|
@@ -1325,14 +1363,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1325
1363
|
this.startNew()
|
|
1326
1364
|
const pending = consumePendingFigureProposal()
|
|
1327
1365
|
if (pending?.source) {
|
|
1328
|
-
this.
|
|
1366
|
+
this.openProposalForReview(pending.source, pending.proposal)
|
|
1329
1367
|
}
|
|
1330
1368
|
return
|
|
1331
1369
|
}
|
|
1332
1370
|
|
|
1333
1371
|
const pending = consumePendingFigureProposal()
|
|
1334
1372
|
if (pending?.source) {
|
|
1335
|
-
this.
|
|
1373
|
+
this.openProposalForReview(pending.source, pending.proposal)
|
|
1336
1374
|
}
|
|
1337
1375
|
}
|
|
1338
1376
|
|
|
@@ -61,16 +61,25 @@ import { type DetailLevel, type FigurePart, type FigurePlacement, type FigureSou
|
|
|
61
61
|
* is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the
|
|
62
62
|
* floor no matter what the sample said; the fourth as a hollowed tray that
|
|
63
63
|
* came out a solid block.
|
|
64
|
+
*
|
|
65
|
+
* All four passed the round-trip test, because the fixture it uses did not
|
|
66
|
+
* carry the field. So the test now reads the **field list the format declares**
|
|
67
|
+
* rather than trusting the fixture.
|
|
68
|
+
*
|
|
69
|
+
* `shape` is unpacked into `round`, `path` and `hollow` rather than carried
|
|
70
|
+
* whole, because the editor holds those as separate state. Every field added
|
|
71
|
+
* under `shape` costs a line here, and forgetting that line was the fourth
|
|
72
|
+
* of the four.
|
|
64
73
|
*/
|
|
65
74
|
/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */
|
|
66
75
|
export interface BoardModel {
|
|
67
76
|
/** 형식 버전. */
|
|
68
77
|
version?: number;
|
|
69
|
-
/** 가로 너비 (mm)
|
|
78
|
+
/** 가로 너비 (mm) → `base.x`. */
|
|
70
79
|
width: number;
|
|
71
|
-
/**
|
|
80
|
+
/** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */
|
|
72
81
|
height: number;
|
|
73
|
-
/** 높이 (mm).
|
|
82
|
+
/** 높이 (mm) → `base.y`. 서 있는 방향이다. */
|
|
74
83
|
depth: number;
|
|
75
84
|
/** 컴포넌트 타입 이름. **저장되는 식별자다.** */
|
|
76
85
|
figureType?: string;
|
|
@@ -132,13 +141,17 @@ export interface PartModel {
|
|
|
132
141
|
hidden?: boolean;
|
|
133
142
|
/** 색 토큰 이름 (`palette.primary` 등). */
|
|
134
143
|
token?: string;
|
|
135
|
-
preset?: FigurePart['material']
|
|
136
|
-
preset?: infer P;
|
|
137
|
-
} ? P : never) : never;
|
|
144
|
+
preset?: FigurePart['material']['preset'];
|
|
138
145
|
flatShading?: boolean;
|
|
139
146
|
transparent?: boolean;
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
/**
|
|
148
|
+
* 자체발광. 없으면 안 빛난다.
|
|
149
|
+
*
|
|
150
|
+
* 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진
|
|
151
|
+
* 전구는 빨갛지 않고 어두운 플라스틱이다.
|
|
152
|
+
*/
|
|
153
|
+
emissive?: FigurePart['material']['emissive'];
|
|
154
|
+
surface?: FigurePart['material']['surface'];
|
|
142
155
|
materialSlot?: FigurePart['materialSlot'];
|
|
143
156
|
/** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */
|
|
144
157
|
keepRound?: FigurePart['keepRound'];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a board. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The board is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x board width\n * base.y board depth <- height\n * base.z board height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - board width/2\n * position.y zPos + depth/2 - board depth/2 <- zPos is the underside\n * position.z top + height/2 - board height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface BoardModel {\n /** 형식 버전. */\n version?: number\n /** 가로 너비 (mm). 씬의 x 축. */\n width: number\n /** 세로 깊이 (mm). 씬의 y 축 (평면에서는 세로, 저작자가 위에서 내려다보는 세로). */\n height: number\n /** 높이 (mm). 씬의 z 축. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n preset?: FigurePart['material'] extends infer M ? (M extends { preset?: infer P } ? P : never) : never\n flatShading?: boolean\n transparent?: boolean\n emissive?: NonNullable<FigurePart['material']>['emissive']\n surface?: NonNullable<FigurePart['material']>['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nfunction round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(board: BoardModel): Vec3 {\n return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 }\n}\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function boardExtent(board: BoardModel, axis: PlaneAxis): number {\n return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, board: BoardModel): FigurePart {\n return partTo(part, halfOf(board))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, board: BoardModel): PartModel {\n return partFrom(part, halfOf(board))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource {\n const half = halfOf(board)\n\n return withoutEmpty({\n version: board.version ?? FIGURE_SOURCE_VERSION,\n type: board.figureType ?? '',\n base: { x: board.width, y: board.depth, z: board.height },\n placement: board.placement,\n detailLevel: board.detailLevel,\n styleKit: board.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: board.animations,\n capabilities: board.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { board: BoardModel; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const board = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as BoardModel\n\n return { board, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a board. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The board is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x board width\n * base.y board depth <- height\n * base.z board height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - board width/2\n * position.y zPos + depth/2 - board depth/2 <- zPos is the underside\n * position.z top + height/2 - board height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface BoardModel {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nfunction round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(board: BoardModel): Vec3 {\n return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 }\n}\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function boardExtent(board: BoardModel, axis: PlaneAxis): number {\n return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, board: BoardModel): FigurePart {\n return partTo(part, halfOf(board))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, board: BoardModel): PartModel {\n return partFrom(part, halfOf(board))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource {\n const half = halfOf(board)\n\n return withoutEmpty({\n version: board.version ?? FIGURE_SOURCE_VERSION,\n type: board.figureType ?? '',\n base: { x: board.width, y: board.depth, z: board.height },\n placement: board.placement,\n detailLevel: board.detailLevel,\n styleKit: board.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: board.animations,\n capabilities: board.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { board: BoardModel; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const board = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as BoardModel\n\n return { board, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
|
|
@@ -98,9 +98,45 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
98
98
|
*/
|
|
99
99
|
updated(changed: Map<string, unknown>): void;
|
|
100
100
|
private announceAiContext;
|
|
101
|
-
private
|
|
101
|
+
private receiveDockProposal;
|
|
102
102
|
private onFigureAiStage;
|
|
103
|
-
|
|
103
|
+
/**
|
|
104
|
+
* A candidate from the dock opens the review lane. It does not become the draft.
|
|
105
|
+
*
|
|
106
|
+
* This used to write `board` and `parts` straight onto the working draft and then set
|
|
107
|
+
* `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
|
|
108
|
+
* erased the lane built to review it. There was no moment where the person said yes, and a
|
|
109
|
+
* draft has no version history to go back to, so the previous work was simply gone.
|
|
110
|
+
*
|
|
111
|
+
* `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
|
|
112
|
+
* and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
|
|
113
|
+
* `figure-ask` entrance already honoured it through `openAiProposal`; the dock entrance is now
|
|
114
|
+
* the same door. Applying is what the take button does, from the lane, per change.
|
|
115
|
+
*
|
|
116
|
+
* ## Why the overwrite was there — so it does not come back
|
|
117
|
+
*
|
|
118
|
+
* The dock opened the review lane originally. Before `c42a0b63df` this handler read:
|
|
119
|
+
*
|
|
120
|
+
* if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
|
|
121
|
+
* if (detail?.draftType && detail.draftType !== this.folded?.type) return
|
|
122
|
+
* if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
|
|
123
|
+
* this.receive(detail.proposal)
|
|
124
|
+
*
|
|
125
|
+
* Three guards, each a silent `return`, and the last compared a serialized source byte for byte.
|
|
126
|
+
* Any difference that changed nothing at all dropped the candidate with no message, so the dock
|
|
127
|
+
* would report a candidate and the screen would show none.
|
|
128
|
+
*
|
|
129
|
+
* The `ox-assistant-chat` migration fixed that by replacing the path rather than the guards, and
|
|
130
|
+
* the review lane went out with them — `applySourceDirectly` wrote straight to the draft, and
|
|
131
|
+
* cleared `proposalSession` on the way past. **Nobody decided that reviews were unwanted here.**
|
|
132
|
+
* It was the cost of loosening a guard by deleting what stood around it.
|
|
133
|
+
*
|
|
134
|
+
* So the guards are gone and stay gone, and the lane is back. If a candidate ever seems not to
|
|
135
|
+
* arrive, the thing to look for is a silent `return` on this path — not the lane.
|
|
136
|
+
*
|
|
137
|
+
* Returns whether the lane opened, so the dock can tell the chat whether to say it staged.
|
|
138
|
+
*/
|
|
139
|
+
private openProposalForReview;
|
|
104
140
|
private proposalBaseSource?;
|
|
105
141
|
connectedCallback(): void;
|
|
106
142
|
disconnectedCallback(): void;
|