@things-factory/figure-ui 10.1.37 → 10.1.39
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/graphql/index.ts +53 -0
- package/client/graphql/v3-store.ts +8 -2
- package/client/modeller/figure-history-panel.ts +33 -87
- package/client/modeller/figure-v3-parts.ts +365 -0
- package/client/modeller/figure-v3-verdict.ts +247 -0
- package/client/modeller/gate-findings.ts +105 -0
- package/client/modeller/v3-editor.ts +11 -2
- package/client/modeller/v3-occupancy.ts +22 -0
- package/client/modeller/v3-product-session.ts +23 -4
- package/client/modeller/v3-renderer.ts +27 -18
- package/client/modeller/v3-thumbnail.ts +97 -0
- package/client/pages/figure-v3-modeller-page.ts +272 -12
- package/client/route.ts +8 -0
- package/dist-client/graphql/index.d.ts +8 -0
- package/dist-client/graphql/index.js +51 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/v3-store.js +8 -2
- package/dist-client/graphql/v3-store.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +21 -0
- package/dist-client/modeller/figure-history-panel.js +34 -87
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +41 -0
- package/dist-client/modeller/figure-v3-parts.js +352 -0
- package/dist-client/modeller/figure-v3-parts.js.map +1 -0
- package/dist-client/modeller/figure-v3-verdict.d.ts +50 -0
- package/dist-client/modeller/figure-v3-verdict.js +268 -0
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +12 -0
- package/dist-client/modeller/gate-findings.js +102 -0
- package/dist-client/modeller/gate-findings.js.map +1 -0
- package/dist-client/modeller/v3-editor.js +11 -2
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-occupancy.d.ts +14 -0
- package/dist-client/modeller/v3-occupancy.js +22 -0
- package/dist-client/modeller/v3-occupancy.js.map +1 -0
- package/dist-client/modeller/v3-product-session.d.ts +44 -2
- package/dist-client/modeller/v3-product-session.js +13 -2
- package/dist-client/modeller/v3-product-session.js.map +1 -1
- package/dist-client/modeller/v3-renderer.d.ts +9 -0
- package/dist-client/modeller/v3-renderer.js +22 -30
- package/dist-client/modeller/v3-renderer.js.map +1 -1
- package/dist-client/modeller/v3-thumbnail.d.ts +3 -0
- package/dist-client/modeller/v3-thumbnail.js +74 -0
- package/dist-client/modeller/v3-thumbnail.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +52 -1
- package/dist-client/pages/figure-v3-modeller-page.js +294 -12
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +7 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/resolve-ts-siblings.mjs +18 -0
- package/test/v3-product-write.test.ts +14 -2
- package/test/v3-release-panel.test.ts +73 -0
- package/test/v3-verdict.test.ts +82 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +79 -1
- package/translations/ja.json +79 -1
- package/translations/ko.json +79 -1
- package/translations/ms.json +79 -1
- package/translations/zh.json +79 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type V3InputChange, type V3PrimitiveCreation, type V3PartEdit } from '@hatiolab/figure-model';
|
|
1
|
+
import { type V3InputChange, type V3PrimitiveCreation, type V3PartEdit, type V3AuthoringAction } from '@hatiolab/figure-model';
|
|
2
2
|
export interface V3ProductRecord {
|
|
3
3
|
id: string;
|
|
4
4
|
source: string;
|
|
@@ -11,6 +11,8 @@ export interface V3ProductStore {
|
|
|
11
11
|
source: string;
|
|
12
12
|
expectedSource?: string;
|
|
13
13
|
creationId?: string;
|
|
14
|
+
/** The card's picture. Absent when the browser could not draw one; the source is saved either way. */
|
|
15
|
+
thumbnail?: string;
|
|
14
16
|
}): Promise<V3ProductRecord>;
|
|
15
17
|
}
|
|
16
18
|
/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
|
|
@@ -263,6 +265,37 @@ export declare function createV3ProductSession(store: V3ProductStore): {
|
|
|
263
265
|
bindings: import("@hatiolab/figure-model").V3EvaluatedBinding[];
|
|
264
266
|
palette: Record<string, string>;
|
|
265
267
|
};
|
|
268
|
+
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
269
|
+
command(action: V3AuthoringAction): {
|
|
270
|
+
context: null;
|
|
271
|
+
name: null;
|
|
272
|
+
fields: never[];
|
|
273
|
+
geometry: never[];
|
|
274
|
+
bindings: never[];
|
|
275
|
+
dirty: boolean;
|
|
276
|
+
revision: null;
|
|
277
|
+
canUndo?: undefined;
|
|
278
|
+
canRedo?: undefined;
|
|
279
|
+
palette?: undefined;
|
|
280
|
+
} | {
|
|
281
|
+
context: string;
|
|
282
|
+
name: string;
|
|
283
|
+
revision: number;
|
|
284
|
+
dirty: boolean;
|
|
285
|
+
canUndo: boolean;
|
|
286
|
+
canRedo: boolean;
|
|
287
|
+
fields: {
|
|
288
|
+
value: number;
|
|
289
|
+
id: string;
|
|
290
|
+
unit: import("@hatiolab/figure-model").V3Unit;
|
|
291
|
+
min: number;
|
|
292
|
+
max: number;
|
|
293
|
+
role?: "design" | "state";
|
|
294
|
+
}[];
|
|
295
|
+
geometry: import("@hatiolab/figure-model").V3AssetGeometry[];
|
|
296
|
+
bindings: import("@hatiolab/figure-model").V3EvaluatedBinding[];
|
|
297
|
+
palette: Record<string, string>;
|
|
298
|
+
};
|
|
266
299
|
undo(): {
|
|
267
300
|
context: null;
|
|
268
301
|
name: null;
|
|
@@ -324,7 +357,16 @@ export declare function createV3ProductSession(store: V3ProductStore): {
|
|
|
324
357
|
palette: Record<string, string>;
|
|
325
358
|
};
|
|
326
359
|
source(): string;
|
|
327
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Saves the model, and the card's picture when the caller managed to take one.
|
|
362
|
+
*
|
|
363
|
+
* The picture is the caller's because taking it needs a canvas, and this session is the place that knows
|
|
364
|
+
* nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the
|
|
365
|
+
* source matters more than the card (see `v3-thumbnail.ts`).
|
|
366
|
+
*/
|
|
367
|
+
save({ thumbnail }?: {
|
|
368
|
+
thumbnail?: string;
|
|
369
|
+
}): Promise<{
|
|
328
370
|
id: string | undefined;
|
|
329
371
|
loading: boolean;
|
|
330
372
|
saving: boolean;
|
|
@@ -93,6 +93,10 @@ export function createV3ProductSession(store) {
|
|
|
93
93
|
edit(edit) {
|
|
94
94
|
return workspace.editPart({ ...writable(), edit });
|
|
95
95
|
},
|
|
96
|
+
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
97
|
+
command(action) {
|
|
98
|
+
return workspace.command({ ...writable(), action });
|
|
99
|
+
},
|
|
96
100
|
undo() {
|
|
97
101
|
return workspace.undo(writable());
|
|
98
102
|
},
|
|
@@ -103,12 +107,19 @@ export function createV3ProductSession(store) {
|
|
|
103
107
|
const { context } = writable();
|
|
104
108
|
return workspace.export({ context }).text;
|
|
105
109
|
},
|
|
106
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Saves the model, and the card's picture when the caller managed to take one.
|
|
112
|
+
*
|
|
113
|
+
* The picture is the caller's because taking it needs a canvas, and this session is the place that knows
|
|
114
|
+
* nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the
|
|
115
|
+
* source matters more than the card (see `v3-thumbnail.ts`).
|
|
116
|
+
*/
|
|
117
|
+
async save({ thumbnail } = {}) {
|
|
107
118
|
const control = writable(), requestEpoch = epoch;
|
|
108
119
|
const exported = workspace.export(control);
|
|
109
120
|
saving = true;
|
|
110
121
|
try {
|
|
111
|
-
const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId });
|
|
122
|
+
const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail });
|
|
112
123
|
if (requestEpoch !== epoch)
|
|
113
124
|
return read();
|
|
114
125
|
if (!record.id || (id !== undefined && record.id !== id))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3-product-session.js","sourceRoot":"","sources":["../../client/modeller/v3-product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createV3Asset,\n createV3EditorWorkspace,\n serializeV3Asset,\n parseV3Asset,\n type V3InputChange,\n type V3PrimitiveCreation,\n type V3PartEdit\n} from '@hatiolab/figure-model'\n\nexport interface V3ProductRecord {\n id: string\n source: string\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface V3ProductStore {\n load(id: string): Promise<V3ProductRecord>\n save(request: { id?: string; source: string; expectedSource?: string; creationId?: string }): Promise<V3ProductRecord>\n}\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createV3ProductSession(store: V3ProductStore) {\n const workspace = createV3EditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n function read() {\n return { ...workspace.read(), id, loading, saving, error }\n }\n function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeV3Asset(createV3Asset('새 Figure')))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: V3InputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n },\n add(part: V3PrimitiveCreation) {\n return workspace.addPrimitive({ ...writable(), part })\n },\n edit(edit: V3PartEdit) {\n return workspace.editPart({ ...writable(), edit })\n },\n undo() {\n return workspace.undo(writable())\n },\n redo() {\n return workspace.redo(writable())\n },\n source() {\n const { context } = writable()\n return workspace.export({ context }).text\n },\n async save() {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n saving = true\n try {\n const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId })\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeV3Asset(parseV3Asset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n workspace.acknowledgeSaved(exported)\n id = record.id\n baseline = record.source\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type V3ProductSession = ReturnType<typeof createV3ProductSession>\n"]}
|
|
1
|
+
{"version":3,"file":"v3-product-session.js","sourceRoot":"","sources":["../../client/modeller/v3-product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createV3Asset,\n createV3EditorWorkspace,\n serializeV3Asset,\n parseV3Asset,\n type V3InputChange,\n type V3PrimitiveCreation,\n type V3PartEdit,\n type V3AuthoringAction\n} from '@hatiolab/figure-model'\n\nexport interface V3ProductRecord {\n id: string\n source: string\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface V3ProductStore {\n load(id: string): Promise<V3ProductRecord>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n }): Promise<V3ProductRecord>\n}\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createV3ProductSession(store: V3ProductStore) {\n const workspace = createV3EditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n function read() {\n return { ...workspace.read(), id, loading, saving, error }\n }\n function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeV3Asset(createV3Asset('새 Figure')))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: V3InputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n },\n add(part: V3PrimitiveCreation) {\n return workspace.addPrimitive({ ...writable(), part })\n },\n edit(edit: V3PartEdit) {\n return workspace.editPart({ ...writable(), edit })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: V3AuthoringAction) {\n return workspace.command({ ...writable(), action })\n },\n undo() {\n return workspace.undo(writable())\n },\n redo() {\n return workspace.redo(writable())\n },\n source() {\n const { context } = writable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `v3-thumbnail.ts`).\n */\n async save({ thumbnail }: { thumbnail?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n saving = true\n try {\n const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail })\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeV3Asset(parseV3Asset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n workspace.acknowledgeSaved(exported)\n id = record.id\n baseline = record.source\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type V3ProductSession = ReturnType<typeof createV3ProductSession>\n"]}
|
|
@@ -26,6 +26,15 @@ export interface V3RenderPart {
|
|
|
26
26
|
};
|
|
27
27
|
segments?: number;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The bodies a canvas draws, out of everything the asset evaluated.
|
|
31
|
+
*
|
|
32
|
+
* Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something
|
|
33
|
+
* on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from
|
|
34
|
+
* rendering, from shadows and from pointer picking (V3 designer's ruling 2026-09-22); drawing it would make
|
|
35
|
+
* "hidden" mean nothing in the modeller while it means something on a board.
|
|
36
|
+
*/
|
|
37
|
+
export declare function drawableV3Parts(geometry: readonly unknown[]): V3RenderPart[];
|
|
29
38
|
export declare function disposeV3Meshes(group: THREE.Group): void;
|
|
30
39
|
/** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
|
|
31
40
|
export declare function replaceV3Meshes(group: THREE.Group, parts: readonly V3RenderPart[], palette: Record<string, string>): void;
|
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import { buildV3Geometry } from '@hatiolab/things-scene';
|
|
3
|
+
/**
|
|
4
|
+
* The bodies a canvas draws, out of everything the asset evaluated.
|
|
5
|
+
*
|
|
6
|
+
* Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something
|
|
7
|
+
* on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from
|
|
8
|
+
* rendering, from shadows and from pointer picking (V3 designer's ruling 2026-09-22); drawing it would make
|
|
9
|
+
* "hidden" mean nothing in the modeller while it means something on a board.
|
|
10
|
+
*/
|
|
11
|
+
export function drawableV3Parts(geometry) {
|
|
12
|
+
return geometry.filter((body) => !!body && typeof body === 'object' && 'shape' in body && 'pose' in body && body.visible !== false);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The mesh for one body — things-scene's builder, not a copy of it.
|
|
16
|
+
*
|
|
17
|
+
* The modeller kept its own provider switch and it knew four providers; the converter writes eleven. A body the
|
|
18
|
+
* board drew was refused here with "Unsupported V3 provider: roundedBoxXZ" (2026-09-24, the first canvas check of
|
|
19
|
+
* ADR-0087 step 4). One builder, and the canvas an author looks at shows what a board will show.
|
|
20
|
+
*/
|
|
2
21
|
function geometry(part) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
if (part.shape.provider === 'box')
|
|
6
|
-
g = new THREE.BoxGeometry(d.width, d.height, d.depth);
|
|
7
|
-
else if (part.shape.provider === 'cylinder')
|
|
8
|
-
g = new THREE.CylinderGeometry(d.radius, d.radius, d.length, part.segments ?? 12);
|
|
9
|
-
else if (part.shape.provider === 'roundedBox') {
|
|
10
|
-
const w = d.width / 2, z = d.depth / 2, r = d.round, s = new THREE.Shape();
|
|
11
|
-
s.moveTo(-w + r, -z);
|
|
12
|
-
s.lineTo(w - r, -z);
|
|
13
|
-
s.quadraticCurveTo(w, -z, w, -z + r);
|
|
14
|
-
s.lineTo(w, z - r);
|
|
15
|
-
s.quadraticCurveTo(w, z, w - r, z);
|
|
16
|
-
s.lineTo(-w + r, z);
|
|
17
|
-
s.quadraticCurveTo(-w, z, -w, z - r);
|
|
18
|
-
s.lineTo(-w, -z + r);
|
|
19
|
-
s.quadraticCurveTo(-w, -z, -w + r, -z);
|
|
20
|
-
g = new THREE.ExtrudeGeometry(s, { depth: d.height, bevelEnabled: false, steps: 1, curveSegments: 6 });
|
|
21
|
-
g.rotateX(Math.PI / 2);
|
|
22
|
-
g.translate(0, d.height / 2, 0);
|
|
23
|
-
}
|
|
24
|
-
else if (part.shape.provider === 'portal') {
|
|
25
|
-
if (!part.shape.profile?.length)
|
|
26
|
-
throw Error('Portal profile is missing');
|
|
27
|
-
const s = new THREE.Shape(part.shape.profile.map(p => new THREE.Vector2(p.x, p.y)));
|
|
28
|
-
g = new THREE.ExtrudeGeometry(s, { depth: d.depth, bevelEnabled: false, steps: 1 });
|
|
29
|
-
g.translate(0, 0, -d.depth / 2);
|
|
30
|
-
}
|
|
31
|
-
else
|
|
22
|
+
const built = buildV3Geometry(part.shape, part.segments ?? 12);
|
|
23
|
+
if (!built)
|
|
32
24
|
throw Error('Unsupported V3 provider: ' + part.shape.provider);
|
|
33
|
-
return
|
|
25
|
+
return built;
|
|
34
26
|
}
|
|
35
27
|
export function disposeV3Meshes(group) {
|
|
36
28
|
for (const child of [...group.children]) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3-renderer.js","sourceRoot":"","sources":["../../client/modeller/v3-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"v3-renderer.js","sourceRoot":"","sources":["../../client/modeller/v3-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import * as THREE from 'three'\nimport { buildV3Geometry } from '@hatiolab/things-scene'\n\n/** Evaluated V3 boundary: graph values are resolved before reaching the renderer. */\nexport interface V3RenderPart {\n id: string\n shape: { provider: string; dimensions: Record<string, number>; profile?: Array<{x:number;y:number}> }\n pose: {r:number[][];t:number[]}\n material: {token:string;preset?:string;transparent?:boolean;emissive?:{token:string;intensity:number;on:boolean}}\n segments?:number\n}\n\n/**\n * The bodies a canvas draws, out of everything the asset evaluated.\n *\n * Two are left out. A scalar body carries no shape or pose — it is a number the graph produced, not something\n * on screen. A body the asset hides is evaluated, placed and gated exactly as a shown one, and excluded from\n * rendering, from shadows and from pointer picking (V3 designer's ruling 2026-09-22); drawing it would make\n * \"hidden\" mean nothing in the modeller while it means something on a board.\n */\nexport function drawableV3Parts(geometry: readonly unknown[]): V3RenderPart[] {\n return geometry.filter(\n (body): body is V3RenderPart =>\n !!body && typeof body === 'object' && 'shape' in body && 'pose' in body && (body as { visible?: boolean }).visible !== false\n )\n}\n\n/**\n * The mesh for one body — things-scene's builder, not a copy of it.\n *\n * The modeller kept its own provider switch and it knew four providers; the converter writes eleven. A body the\n * board drew was refused here with \"Unsupported V3 provider: roundedBoxXZ\" (2026-09-24, the first canvas check of\n * ADR-0087 step 4). One builder, and the canvas an author looks at shows what a board will show.\n */\nfunction geometry(part: V3RenderPart): THREE.BufferGeometry {\n const built = buildV3Geometry(part.shape as never, part.segments ?? 12) as THREE.BufferGeometry | undefined\n if (!built) throw Error('Unsupported V3 provider: ' + part.shape.provider)\n return built\n}\n\nexport function disposeV3Meshes(group:THREE.Group):void {\n for(const child of [...group.children]){\n if(child instanceof THREE.Mesh){child.geometry.dispose();const materials=Array.isArray(child.material)?child.material:[child.material];materials.forEach(m=>m.dispose())}\n group.remove(child)\n }\n}\n\n/** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */\nexport function replaceV3Meshes(group:THREE.Group,parts:readonly V3RenderPart[],palette:Record<string,string>):void {\n const staged=new THREE.Group(),ids=new Set<string>()\n try{\n for(const part of parts){\n if(ids.has(part.id))throw Error('Duplicate V3 geometry ID: '+part.id);ids.add(part.id)\n const color=palette[part.material.token]\n if(!color||!/^#[0-9a-f]{6}$/i.test(color))throw Error('Unresolved material token: '+part.material.token)\n const {r,t}=part.pose\n if(r.length!==3||r.some(row=>row.length!==3||row.some(x=>!Number.isFinite(x)))||t.length!==3||t.some(x=>!Number.isFinite(x)))throw Error('Invalid V3 pose: '+part.id)\n const emissive=part.material.emissive\n if(emissive?.on&&!/^#[0-9a-f]{6}$/i.test(palette[emissive.token]??''))throw Error('Unresolved emissive token')\n const g=geometry(part)\n const material=new THREE.MeshStandardMaterial({color,metalness:part.material.preset==='metal'?0.8:0,roughness:part.material.preset==='metal'?0.3:0.65,transparent:!!part.material.transparent,opacity:part.material.transparent?0.35:1,emissive:emissive?.on?palette[emissive.token]:0,emissiveIntensity:emissive?.on?emissive.intensity:0})\n const mesh=new THREE.Mesh(g,material);mesh.name=part.id;mesh.userData.figurePartId=part.id\n mesh.matrixAutoUpdate=false;mesh.matrix.set(r[0][0],r[0][1],r[0][2],t[0],r[1][0],r[1][1],r[1][2],t[1],r[2][0],r[2][1],r[2][2],t[2],0,0,0,1)\n staged.add(mesh)\n }\n }catch(error){disposeV3Meshes(staged);throw error}\n disposeV3Meshes(group)\n for(const mesh of [...staged.children])group.add(mesh)\n group.updateMatrixWorld(true)\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import './v3-canvas.js';
|
|
2
|
+
/**
|
|
3
|
+
* 카드에 쓸 그림을 찍는다 — **숨은 자리에 V3 캔버스를 하나 더 세워서.**
|
|
4
|
+
*
|
|
5
|
+
* V2 의 `figure-thumbnail.ts` 와 같은 규율이고, 까닭도 같다.
|
|
6
|
+
*
|
|
7
|
+
* ## 왜 보이는 캔버스를 찍지 않나
|
|
8
|
+
*
|
|
9
|
+
* 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있다. 그것을 찍으면 카드마다 프레임이
|
|
10
|
+
* 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이 된다. 목록은 견주는 자리이므로 그림은
|
|
11
|
+
* **정해진 크기와 정해진 카메라**로 찍는다. `figure-v3-canvas` 의 `thumbnail()` 은 보이는 캔버스를
|
|
12
|
+
* 그대로 뽑으므로 카드용이 아니다 — 여기서는 새로 세운 캔버스가 스스로 `fit()` 한 자리에서 찍는다.
|
|
13
|
+
*
|
|
14
|
+
* ## 왜 서버가 아니라 브라우저인가
|
|
15
|
+
*
|
|
16
|
+
* 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주 실패한다. 도형을 그릴 GPU 는
|
|
17
|
+
* 이미 보는 사람 손에 있다.
|
|
18
|
+
*
|
|
19
|
+
* ## 못 찍어도 저장은 간다
|
|
20
|
+
*
|
|
21
|
+
* 탭이 뒤로 가면 브라우저가 `requestAnimationFrame` 을 멈춘다. 그 위에 저장이 얹혀 있으면 저장이
|
|
22
|
+
* 통째로 멈춘다 — 단추는 잠긴 채로, 아무 말도 없이. 그래서 시계와 경주시키고, 늦으면 그림 없이
|
|
23
|
+
* 간다. 정본이 그림보다 중하고, 그림은 다음에 저작면을 열 때 다시 채운다.
|
|
24
|
+
*/
|
|
25
|
+
/** 카드는 4:3 이다. V2 와 같은 크기 — 목록에 두 판이 섞여 있어도 카드가 고르다. */
|
|
26
|
+
const WIDTH = 480;
|
|
27
|
+
const HEIGHT = 360;
|
|
28
|
+
/** 넘으면 그림 없이 저장한다. 빠르기가 아니라 저장이 멈추지 않는 것이 목적이다. */
|
|
29
|
+
const PATIENCE = 5000;
|
|
30
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
/** 씬이 서고 카메라가 자리를 잡을 틈. rAF 가 멈춘 탭에서도 풀리도록 시계와 경주시킨다. */
|
|
32
|
+
function settle() {
|
|
33
|
+
const frames = new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150))));
|
|
34
|
+
return Promise.race([frames, delay(600)]);
|
|
35
|
+
}
|
|
36
|
+
function within(work, ms) {
|
|
37
|
+
return Promise.race([work, delay(ms).then(() => undefined)]);
|
|
38
|
+
}
|
|
39
|
+
export async function captureV3Thumbnail(parts, palette) {
|
|
40
|
+
if (!parts.length)
|
|
41
|
+
return undefined;
|
|
42
|
+
/*
|
|
43
|
+
화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.
|
|
44
|
+
WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).
|
|
45
|
+
*/
|
|
46
|
+
const host = document.createElement('div');
|
|
47
|
+
host.setAttribute('figure-v3-thumbnail-stage', '');
|
|
48
|
+
host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`;
|
|
49
|
+
document.body.appendChild(host);
|
|
50
|
+
const shooter = document.createElement('figure-v3-canvas');
|
|
51
|
+
shooter.parts = parts;
|
|
52
|
+
shooter.palette = palette;
|
|
53
|
+
shooter.context = 'thumbnail';
|
|
54
|
+
shooter.style.cssText = 'display: block; width: 100%; height: 100%;';
|
|
55
|
+
try {
|
|
56
|
+
host.appendChild(shooter);
|
|
57
|
+
const shot = await within((async () => {
|
|
58
|
+
await settle();
|
|
59
|
+
shooter.fit?.();
|
|
60
|
+
return shooter.thumbnail?.();
|
|
61
|
+
})(), PATIENCE);
|
|
62
|
+
if (!shot)
|
|
63
|
+
console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`);
|
|
64
|
+
return shot;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.warn(`[thumbnail] 세우지 못했습니다 — ${e.message}`);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
host.remove();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=v3-thumbnail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/v3-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,CAAA;AAIvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wDAAwD;AACxD,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AAElB,mDAAmD;AACnD,MAAM,QAAQ,GAAG,IAAI,CAAA;AAUrB,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAEnF,yDAAyD;AACzD,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAqB,EACrB,OAA+B;IAE/B,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAEnC;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAA;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,kBAAkB,CAAY,CAAA;IACrE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,OAAO,GAAG,OAAO,CAAA;IACzB,OAAO,CAAC,OAAO,GAAG,WAAW,CAAA;IAC7B,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QACzB,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,CAAC,GAAG,EAAE,EAAE,CAAA;YACf,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,CAAA;QAC9B,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QACD,IAAI,CAAC,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QAC7E,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './v3-canvas.js'\n\nimport type { V3RenderPart } from './v3-renderer.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 V3 캔버스를 하나 더 세워서.**\n *\n * V2 의 `figure-thumbnail.ts` 와 같은 규율이고, 까닭도 같다.\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있다. 그것을 찍으면 카드마다 프레임이\n * 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이 된다. 목록은 견주는 자리이므로 그림은\n * **정해진 크기와 정해진 카메라**로 찍는다. `figure-v3-canvas` 의 `thumbnail()` 은 보이는 캔버스를\n * 그대로 뽑으므로 카드용이 아니다 — 여기서는 새로 세운 캔버스가 스스로 `fit()` 한 자리에서 찍는다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주 실패한다. 도형을 그릴 GPU 는\n * 이미 보는 사람 손에 있다.\n *\n * ## 못 찍어도 저장은 간다\n *\n * 탭이 뒤로 가면 브라우저가 `requestAnimationFrame` 을 멈춘다. 그 위에 저장이 얹혀 있으면 저장이\n * 통째로 멈춘다 — 단추는 잠긴 채로, 아무 말도 없이. 그래서 시계와 경주시키고, 늦으면 그림 없이\n * 간다. 정본이 그림보다 중하고, 그림은 다음에 저작면을 열 때 다시 채운다.\n */\n\n/** 카드는 4:3 이다. V2 와 같은 크기 — 목록에 두 판이 섞여 있어도 카드가 고르다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\n/** 넘으면 그림 없이 저장한다. 빠르기가 아니라 저장이 멈추지 않는 것이 목적이다. */\nconst PATIENCE = 5000\n\ninterface Shooter extends HTMLElement {\n parts?: V3RenderPart[]\n palette?: Record<string, string>\n context?: string\n thumbnail?: () => string | undefined\n fit?: () => void\n}\n\nconst delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))\n\n/** 씬이 서고 카메라가 자리를 잡을 틈. rAF 가 멈춘 탭에서도 풀리도록 시계와 경주시킨다. */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n return Promise.race([frames, delay(600)])\n}\n\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureV3Thumbnail(\n parts: V3RenderPart[],\n palette: Record<string, string>\n): Promise<string | undefined> {\n if (!parts.length) return undefined\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-v3-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-v3-canvas') as Shooter\n shooter.parts = parts\n shooter.palette = palette\n shooter.context = 'thumbnail'\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n const shot = await within(\n (async () => {\n await settle()\n shooter.fit?.()\n return shooter.thumbnail?.()\n })(),\n PATIENCE\n )\n if (!shot) console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { PageView } from '@operato/shell';
|
|
2
2
|
import '../modeller/v3-editor.js';
|
|
3
|
-
|
|
3
|
+
import '@material/web/icon/icon.js';
|
|
4
|
+
import '../modeller/figure-history-panel.js';
|
|
5
|
+
import '../modeller/figure-v3-verdict.js';
|
|
6
|
+
/**
|
|
7
|
+
* The V3 product page has exactly one authoritative workspace for UI and AI input transactions.
|
|
8
|
+
*
|
|
9
|
+
* It answers to `figure-v3-modeller` while the catalogue's assets are still V2. The designer's order switches
|
|
10
|
+
* the production data and the consuming paths together (ADR-0087, step 5): until the assets are converted, a
|
|
11
|
+
* `figure-modeller` that opened this page would fail on every figure in the catalogue. Having its own route
|
|
12
|
+
* is what makes steps 3 and 4 — the conversion on a recoverable copy, and the canvas check — possible at all.
|
|
13
|
+
* At step 5 the V2 page goes, and this one takes the `figure-modeller` name and route.
|
|
14
|
+
*/
|
|
4
15
|
export declare class FigureV3ModellerPage extends PageView {
|
|
5
16
|
static styles: import("lit").CSSResult;
|
|
6
17
|
private readonly session;
|
|
@@ -8,8 +19,47 @@ export declare class FigureV3ModellerPage extends PageView {
|
|
|
8
19
|
private discardApproved;
|
|
9
20
|
private revision;
|
|
10
21
|
private error;
|
|
22
|
+
/**
|
|
23
|
+
* 발행 패널이 읽는 행의 사실 — 상태와 발행 판 번호. 원본은 세션이 들고, 이 둘은 행에 있다.
|
|
24
|
+
*
|
|
25
|
+
* 이 화면에는 발행 패널이 없었다. 저장은 되고 발행 · 판본 · 복원 · 판정을 볼 자리가 없어서, ADR-0087 5단계에서
|
|
26
|
+
* `figure-modeller` 가 이 화면으로 바뀌면 저작자는 발행을 할 수 없었다. 새 V3 도형은 공간을 선언하기 전에는 전부
|
|
27
|
+
* 발행 판정에 막히는데 그 선언을 할 단추도 없었다(2026-09-24 확인). 서버의 문 하나가 두 형식을 다 읽으므로
|
|
28
|
+
* V2 화면의 패널을 그대로 건다.
|
|
29
|
+
*/
|
|
30
|
+
private row;
|
|
31
|
+
private typeError;
|
|
32
|
+
/** Why 「부품에 맞추기」 did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
|
|
33
|
+
private fitError;
|
|
34
|
+
/**
|
|
35
|
+
* Which tab the side shows. The verdict comes first: it is what the author needs while editing, and the count on
|
|
36
|
+
* its tab stays in sight while the history is open — as on the V2 page.
|
|
37
|
+
*/
|
|
38
|
+
private tab;
|
|
39
|
+
private instances?;
|
|
40
|
+
/**
|
|
41
|
+
* The source the verdict judges. The session refuses to export while it loads or saves, so the last one read is
|
|
42
|
+
* kept rather than blanked — the verdict would otherwise flicker to 「waiting」 on every save.
|
|
43
|
+
*/
|
|
44
|
+
private draft?;
|
|
45
|
+
private draftSource;
|
|
46
|
+
private wasDirty;
|
|
11
47
|
private announce;
|
|
12
48
|
private changed;
|
|
49
|
+
private readRow;
|
|
50
|
+
/**
|
|
51
|
+
* 타입을 정한다 — 보드가 도형을 부르는 이름이고, 발행하면 굳는다(ADR-0063 결정 4).
|
|
52
|
+
*
|
|
53
|
+
* 새 V3 도형은 저장할 때 임시 타입을 받는데, 이 화면에는 그것을 바꿀 칸이 없어서 임시 타입 그대로 발행됐다.
|
|
54
|
+
* 행만 바뀐다 — V3 자산은 타입을 원본에 적지 않으므로(적으면 형식이 거절한다) 편집 중인 원본과 부딪히지 않는다.
|
|
55
|
+
* 서버의 거절(이미 쓰는 타입 · 발행 뒤)은 그 문장 그대로 칸 아래에 둔다.
|
|
56
|
+
*/
|
|
57
|
+
private renameType;
|
|
58
|
+
/** 「부품에 맞추기」 — 공간을 지금 부품에서 다시 선언한다. 저장은 저작자가 따로 한다. */
|
|
59
|
+
private fitToParts;
|
|
60
|
+
private released;
|
|
61
|
+
/** 과거 판으로 되돌렸으면 서버의 원본이 바뀐 것이다 — 다시 연다. 패널은 저장 안 된 것이 있으면 되돌리기를 막는다. */
|
|
62
|
+
private reverted;
|
|
13
63
|
/** The dock must explicitly use this checked transaction contract, never an unversioned overwrite. */
|
|
14
64
|
private applyAI;
|
|
15
65
|
connectedCallback(): void;
|
|
@@ -23,4 +73,5 @@ export declare class FigureV3ModellerPage extends PageView {
|
|
|
23
73
|
actions: never[];
|
|
24
74
|
};
|
|
25
75
|
render(): import("lit-html").TemplateResult<1>;
|
|
76
|
+
private renderTab;
|
|
26
77
|
}
|