@hatiolab/figure-model 0.3.3 → 0.4.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +3 -0
  3. package/dist/asset-types.d.ts +34 -3
  4. package/dist/asset-types.d.ts.map +1 -1
  5. package/dist/capabilities.js +28 -4
  6. package/dist/capabilities.js.map +1 -1
  7. package/dist/graph-types.d.ts +60 -1
  8. package/dist/graph-types.d.ts.map +1 -1
  9. package/dist/graph.d.ts.map +1 -1
  10. package/dist/graph.js +212 -21
  11. package/dist/graph.js.map +1 -1
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/kernel-version.d.ts +1 -1
  17. package/dist/kernel-version.js +1 -1
  18. package/dist/names.d.ts +19 -0
  19. package/dist/names.d.ts.map +1 -0
  20. package/dist/names.js +47 -0
  21. package/dist/names.js.map +1 -0
  22. package/dist/package.d.ts +86 -0
  23. package/dist/package.d.ts.map +1 -0
  24. package/dist/package.js +202 -0
  25. package/dist/package.js.map +1 -0
  26. package/dist/render-lod.d.ts +12 -0
  27. package/dist/render-lod.d.ts.map +1 -0
  28. package/dist/render-lod.js +32 -0
  29. package/dist/render-lod.js.map +1 -0
  30. package/dist/shape-ops.d.ts +1 -1
  31. package/dist/shape-ops.d.ts.map +1 -1
  32. package/dist/shape-ops.js.map +1 -1
  33. package/dist/words.d.ts +6 -6
  34. package/dist/words.js +8 -8
  35. package/dist/words.js.map +1 -1
  36. package/docs/assembly-constraints.md +3 -3
  37. package/docs/asset-persistence.md +8 -8
  38. package/docs/authoring-transactions.md +2 -2
  39. package/docs/capability-bindings.md +1 -1
  40. package/docs/common-kernel-experiment.md +2 -2
  41. package/docs/completeness-review-2026-10-04.md +150 -1
  42. package/docs/core-semantics.md +2 -2
  43. package/docs/cutover.md +4 -4
  44. package/docs/document-contract.md +3 -3
  45. package/docs/dryroom-profile-validation.md +1 -1
  46. package/docs/editor-integration.md +4 -4
  47. package/docs/evaluation-changes-design.md +304 -0
  48. package/docs/evaluation-performance-summary-2026-10-05.md +236 -0
  49. package/docs/full-conveyor-validation.md +8 -8
  50. package/docs/full-dryroom-validation.md +5 -5
  51. package/docs/full-forklift-validation.md +5 -5
  52. package/docs/full-robot-validation.md +4 -4
  53. package/docs/layout-system.md +2 -2
  54. package/docs/legacy-risk-audit-2026-09-23.md +14 -14
  55. package/docs/minimal-model.md +3 -3
  56. package/docs/openusd-review.md +14 -14
  57. package/docs/operator-contracts.md +4 -4
  58. package/docs/performance-followup-2026-10-05.md +314 -0
  59. package/docs/prototypes/README.md +2 -2
  60. package/docs/prototypes/asset.schema.json +1 -1
  61. package/docs/prototypes/document.schema.json +1 -1
  62. package/docs/prototypes/editor-browser.mjs +2 -2
  63. package/docs/prototypes/linear-comparison.py +1 -1
  64. package/docs/prototypes/render-dryroom.py +1 -1
  65. package/docs/prototypes/render-full-dryroom.mjs +12 -12
  66. package/docs/prototypes/runtime-browser.mjs +3 -3
  67. package/docs/research-review.md +1 -1
  68. package/docs/review-2026-09-22.md +19 -19
  69. package/docs/runtime-extraction.md +10 -10
  70. package/docs/sample-validation-expanded.md +5 -5
  71. package/docs/sample-validation.md +5 -5
  72. package/docs/shape-dimension-contract.md +1 -1
  73. package/docs/spatial-contracts.md +2 -2
  74. package/docs/status.md +1 -1
  75. package/docs/storage-integration.md +5 -5
  76. package/package.json +1 -1
@@ -0,0 +1,314 @@
1
+ # Figure 후속 성능 실행 계획 (2026-10-05)
2
+
3
+ 결정 정본: [ADR-0108 현행 보완](../../operato-application/adr/ADR-0108.md).
4
+ 출발점: 0.3.4·10.1.73 일반 변경분 채택 및 설치본 검증 완료. 과거 튜닝 성과는 유지한다.
5
+ 제품 목표와 원자료는 [성능 요약](evaluation-performance-summary-2026-10-05.md)을 따른다.
6
+ 이 문서는 실행 상태를 관리하며 예정 효과를 성과로 기록하지 않는다.
7
+
8
+ ## 순서와 현재 상태
9
+
10
+ | 순서 | 후보 | 먼저 확인할 근거 | 채택 조건 | 현재 상태 |
11
+ | ---- | ------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------- |
12
+ | P1 | 내부 임시 객체·배열의 할당 감소 | kernel/changes buffer/rig/geometry allocation stack, bytes/frame와 GC | 큰 할당 출처 한 곳의 개선, 비계측 전체 프레임·p95와 소유권 유지 | 호출 트리 집계 완료; 첫 대상 = 병합 은행 형상 키 |
13
+ | P2 | 반복 부품 공통 계산 | 반복마다 같은 변환·id/path·layout을 재구성하는 양 | 동일 연산 순서·bit 동일과 구조 변경 지원 | 첫 후보 측정; 기본 채택 보류 |
14
+ | P3 | 동일 입력 평가 공유 | 동일/서로 다른 size·pose에서 중복량 | 모든 의존성·정의/버전 포함, 불변 공유와 인스턴스 격리, bounded cache | 검토 예정 |
15
+ | P4 | resize 이벤트 프레임 단위 반영 | 이벤트 수 대비 실제 update 수, 제품 drag trace | 부착/시뮬레이션·최종 입력 정확성, 조회 시 최신성 | 중복 존재 확인 후 판단 |
16
+ | P5 | 실행 계획 간소화 | 상수 의존 노드·미사용 중간 출력·실행 횟수 | 공개 값 조회/진단 보존, 전체 평가 동등성, 컴파일·메모리 비용 포함 | 검토 예정 |
17
+
18
+ P2 코드의 단서: `repeat@1`은 매 평가마다 path/id, identity 행렬과 여러 compose 결과를 만든다.
19
+ 변경분은 structure 여부가 결정되기 전에도 ids/keys/paths/indexes를 모으며 path 동결 사본을 구성한다.
20
+ 이는 읽기에서 찾은 후보이지 최대 병목 또는 제거 가능하다는 확정은 아니다. 프로필로 순서를 정한다.
21
+
22
+ ## P1 측정 설계
23
+
24
+ - 기존 `check-figure-gap.mjs`에 `ALLOCATION_PROFILE=1`을 추가해 collected object를 포함한 sampled heap
25
+ allocation stack 원자료와 할당 위치별 bytes/frame을 보존한다. profiling과 frame timing 구간은 분리한다.
26
+ - 첫 실행은 한 판의 동일 빌드로 board 60대·크기만 교대, 1 round의 D1/D2/C다. 이름 D/C는 기존 harness 호환용이며
27
+ **이번에는 모두 동일 baseline**이다. 이것으로 성능 개선을 주장하지 않는다.
28
+ - 원자료에 GPU, build 경로, 입력, 계측 조건을 남긴다. minified 위치는 sourcemap 또는 함수/호출 사슬로 확인한다.
29
+ 익명 노드를 임의로 커널에 귀속하지 않는다. sampled bytes는 추정량이고 총 할당의 정확한 계수가 아니다.
30
+ - 프로필로 가장 큰 할당 출처를 고른 뒤 같은 입력의 500대에서 확인한다. 동일 입력과 인스턴스별 상이 입력을 따로 검증한다.
31
+ - 구현 후보마다 회귀 시험→수정→관련 시험·build→비계측 browser A/A·A/B 순서로 진행한다.
32
+ 반복 수와 순서는 사전에 고정한다. 중앙값·p95, 생성·보유/peak/dispose 메모리, draw·triangles·body·copies를 보고한다.
33
+ - 전체 스냅샷 미조회/가끔 조회/매 프레임 조회, resize 뒤 pose, flag 및 부착점도 확인한다.
34
+
35
+ ## 제약과 중단 기준
36
+
37
+ - 내부 작업 버퍼는 재사용 가능하지만 호출자 소유 typed array와 이전 snapshot을 덮어쓰지 않는다.
38
+ - 읽기 수명 제한만으로 내부 가변 객체 노출을 허용하지 않는다. 전체 `plan.evaluate`의 가변 소유권도 유지한다.
39
+ - 기능 생략, 동일 시간 강제, 출력 clamp로 측정량을 줄이지 않는다. metadata·id·path·selection을 보존한다.
40
+ - 후보 하나당 먼저 고정한 A/B 실행을 마친 뒤 판단한다. 효과가 관찰 변동과 구별되지 않으면 보류하고 다음 큰 비용으로 이동한다.
41
+ - 이미 다른 세션에서 구현한 LOD(ADR-0116)는 별도다. baseline은 OFF이고 이번 작업은 해당 runtime 소스를 변경하지 않는다.
42
+ - 패키지 발행·제품 의존성 갱신은 별도 결정이다. 다른 세션의 worktree/미커밋/index를 변경하지 않는다.
43
+
44
+ ## 기록
45
+
46
+ 2026-10-05: ADR-0108 일반 변경분 현행 계약과 P1~P5를 보완했고 ADR-0109·0111의 초기 현황에
47
+ 후속 발행/설치 검증 링크를 추가했다. 런타임 최적화는 아직 적용하지 않았다.
48
+
49
+ ### P1 첫 관찰 (60대, 동일 baseline 3회)
50
+
51
+ Apple M5 Metal, board, 폭 2400↔2496만 교대, warmup 30/frame 60.
52
+ 기존 gap harness의 D1/D2/C를 모두 같은 things-scene.mjs에 연결했다.
53
+ heap profiler는 4,096 byte 간격으로 collected object를 포함하고 각 60프레임의 할당을 샘플링한다.
54
+ 총 sampled allocation: 10.21 / 10.49 / 10.49 MiB/frame.
55
+
56
+ | 대표 할당 위치 | 위치별 sampled KiB/frame (3회) | 해석 |
57
+ | --------------- | ------------------------------ | -------------------------------------------------------------------------------- |
58
+ | fromChanges | 933 / 948 / 940 | rig의 body·중첩 pose 재구성과 외형 입히기 후보 |
59
+ | planOf → wA | 928 / 895 / 914 | 배치 계획 호출 사슬. minified 함수 wA를 source로 매핑한 후 대상 확정 |
60
+ | advance → l → A | 804 / 801 / 801 | kernel 일반 변경 처리 호출 사슬. 익명/압축 이름을 정확히 귀속하기 전 최적화 금지 |
61
+
62
+ 이는 상위 selfSize 위치 일부이며 각 함수 전체의 inclusive allocation 또는 정확한 byte 계수가 아니다.
63
+ 대부분의 할당을 위 세 곳이 모두 설명한다고 주장하지 않는다.
64
+
65
+ `fromChanges` source를 확인하면 각 body를 새 record로 만들고 pose의 중첩 배열을 구성한 뒤
66
+ `dressOne`에서 다시 spread해 dressed record를 만든다. bindings Map와 palette 사본도 만든다.
67
+ 후보는 가변 renderer 내부 전용 body/pose의 재사용과 중간 body wrapper 제거다.
68
+ 다만 이전 `lastDrawn` 보관·LOD 경계 캐시·스냅샷 조회·실패 중간 상태·plan 동일성에 대한 계약부터
69
+ 확인해야 한다. 현재 LOD 작업과 겹치는 rig 소스는 즉시 수정하지 않는다.
70
+
71
+ 원자료: things-scene/test/browser/figure-vs-js/gap-1791199515874.json 및 같은 디렉터리 allocation-*.json.
72
+ 생성 원자료는 기존 ignore 정책대로 로컬에 남기고 요약을 이 문서에 보존한다.
73
+
74
+ ### P1 규모 확인 (500대, 동일 baseline 3회)
75
+
76
+ 같은 board/입력/계측 조건에서 77.22 / 76.97 / 76.93 MiB/frame이었다.
77
+ 비계측 frame 중앙값은 49.67 / 51.65 / 50.01ms였다. 세 표식은 같은 빌드이므로 성능 개선을 뜻하지 않는다.
78
+ 대표 self allocation 위치는 fromChanges 약 7.7MiB/frame, planOf→wA 약 7.3MiB/frame,
79
+ 커널 advance→l→A 약 6.5MiB/frame이었다(두 번째 실행). 60대의 호출 사슬과 일치한다.
80
+ 원자료: things-scene/test/browser/figure-vs-js/gap-1791199606515.json 및 allocation-board-500-*.json.
81
+
82
+ 첫 구현 후보는 fromChanges의 중간 body wrapper·중첩 pose 객체 할당을 줄이는 것으로 특정한다(잠정 — 아래 「첫 대상 다시 정하기」에서 바꿨다).
83
+ 적용 전에 lastDrawn 보관, LOD revision/bounds cache, shape/body 동일성, 공개 evaluation의 동결 및 실패 시
84
+ 그림 유지 계약을 시험으로 고정한다. 전체 가변 객체 재사용보다 중간 wrapper 제거를 먼저 비교한다.
85
+ 공개 소유권을 바꾸는 재사용은 허용하지 않는다. rig 소스는 다른 세션 LOD 변경이 있으므로 별도 변경 단위로 진행한다.
86
+ 아직 runtime 후보는 적용하지 않았다. 이번 완료 범위는 ADR 현행화·실행 계획·profile 도구·기준 분석이다.
87
+
88
+ ### P1 호출 트리 집계 (기존 원자료 6개, 새 측정 없음)
89
+
90
+ 원자료: things-scene/test/browser/figure-vs-js/allocation-board-{60,500}-{D1,D2,C}-r1-*.json(세 표식 모두 같은 빌드). 각 할당(sampled
91
+ selfSize)을 호출 사슬의 잎에서 뿌리 쪽으로 올라가며 처음 만나는 기준 프레임 하나에만 귀속했다. 기준 프레임은 압축 뒤에도 이름이
92
+ 남는 메서드다. 커널 `advance`·`read`·`changed`·`value`·`place`, rig 외형 `fromChanges`·`dressOne`·`dress`, 그리기 준비 `draw`·`planOf`·
93
+ `drawMerged`·`drawCarried`, 보드·렌더러 `onchange`·`_applyProps`·`set`·`trigger`·`renderThreeD`·`render`. rig `update` 바로 아래에서 어느
94
+ 기준에도 닿지 않은 것과 기준이 없는 것은 미분류다. 값은 MiB/frame(비중), 합계는 전체 sampled bytes와 같다.
95
+
96
+ 중복 없는 분류:
97
+
98
+ | 분류 | 60대 D1 / D2 / C | 500대 D1 / D2 / C |
99
+ | ------------ | ----------------------------------------- | -------------------------------------------- |
100
+ | 커널 | 4.92 (48.2%) / 4.92 (46.9%) / 4.94 (47.1%) | 41.01 (53.1%) / 41.00 (53.3%) / 40.94 (53.2%) |
101
+ | 그리기 준비 | 3.70 (36.3%) / 3.96 (37.8%) / 3.95 (37.7%) | 23.26 (30.1%) / 23.05 (30.0%) / 23.04 (30.0%) |
102
+ | rig 외형 | 0.94 (9.2%) / 0.95 (9.1%) / 0.95 (9.0%) | 7.94 (10.3%) / 7.89 (10.2%) / 7.94 (10.3%) |
103
+ | 보드·렌더러 | 0.61 (6.0%) / 0.61 (5.8%) / 0.62 (5.9%) | 4.85 (6.3%) / 4.88 (6.3%) / 4.86 (6.3%) |
104
+ | 미분류 | 0.04 (0.3%) / 0.04 (0.3%) / 0.04 (0.3%) | 0.16 (0.2%) / 0.15 (0.2%) / 0.15 (0.2%) |
105
+ | 합계 | 10.21 / 10.49 / 10.49 | 77.22 / 76.97 / 76.93 |
106
+
107
+ 분류 안의 inclusive 순위(부모와 자식은 합산하지 않는다. 들여쓴 줄은 바로 위 줄 안에 포함된다). 압축 이름은 번들 원문의 그 위치
108
+ 코드로 정체를 확인한 것만 적었다.
109
+
110
+ | 분류·호출 | 60대 D1 / D2 / C | 500대 D1 / D2 / C |
111
+ | ---------------------------------------------------------- | ------------------ | ------------------- |
112
+ | 커널 `advance` | 분류 합계와 같음 | 41.01 / 41.00 / 40.94 |
113
+ | `step`(압축 `A`) | 4.65 / 4.65 / 4.68 | 38.93 / 38.91 / 38.85 |
114
+ | 행 `repeat@1` 실행 | 1.17 / 1.16 / 1.16 | 9.76 / 9.78 / 9.71 |
115
+ | 밀어내기 `extrude@1` 실행 → `runExtrude`(압축 `Hw`) | 1.11 / 1.12 / 1.13 | 9.32 / 9.31 / 9.33 |
116
+ | 밀어내기 매개변수 해석(압축 `Tw`) | 0.41 / 0.41 / 0.41 | 3.42 / 3.41 / 3.44 |
117
+ | 형상 description 생성(압축 `Iw`) | 0.55 / 0.55 / 0.56 | 4.66 / 4.67 / 4.67 |
118
+ | `rigid@1` 실행 | 0.70 / 0.69 / 0.70 | 5.82 / 5.86 / 5.82 |
119
+ | 행렬곱 `mm`(압축 `fM`, 여러 연산이 부름) | 0.62 / 0.60 / 0.61 | 5.09 / 5.11 / 5.06 |
120
+ | 그리기 준비 `drawMerged` | 2.34 / 2.63 / 2.60 | 12.94 / 12.81 / 12.77 |
121
+ | 병합 은행 조회 `acquireMergedFigureGeometry`(압축 `rk`) | 2.22 / 2.51 / 2.48 | 12.21 / 12.08 / 12.05 |
122
+ | 부품 형상 문자열 키 `figureGeometryKeyOf`의 map·join | 1.03 / 1.05 / 1.04 | 8.61 / 8.67 / 8.60 |
123
+ | 그리기 준비 `planOf` | 1.06 / 1.03 / 1.05 | 8.44 / 8.37 / 8.38 |
124
+ | `figureRenderBatchesOf`(압축 `wA`, body별 signature 포함) | 0.97 / 0.94 / 0.96 | 7.93 / 7.85 / 7.88 |
125
+ | rig 외형 `fromChanges` | 0.94 / 0.95 / 0.95 | 7.94 / 7.89 / 7.94 |
126
+ | 보드 컴포넌트 `set`(내장) | 0.30 / 0.31 / 0.31 | 2.49 / 2.49 / 2.49 |
127
+ | 보드 `WebGLRenderer.render` | 0.13 / 0.12 / 0.13 | 0.99 / 1.01 / 1.00 |
128
+
129
+ 미분류는 프레임 시계 `tick`의 Set 등과 rig `update`의 입력 준비(`stateOverrides`)다(500대 0.15~0.16 MiB/frame).
130
+
131
+ ### 첫 대상 다시 정하기
132
+
133
+ | 후보 | 할당(500대) | 제거 가능한 몫(코드 읽기 추정) | 직접 CPU 근거 | 계약 위험 |
134
+ | ---------------------------------------------------- | ---------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | --------- |
135
+ | 병합 은행 조회의 부품 형상 키 | 8.6 MiB (11%) | 크기 변경에서 형상이 그대로인 부품(16개 중 11개)의 키 | 은행 조회가 rig당 6~8 µs(Node, 형상 키 4.5 µs), 500대 `drawMerged` 5.5 ms | 낮음(동결 형상만 캐시, things-scene 안) |
136
+ | 배치 계획의 body signature(`figureRenderBatchesOf`) | 7.9 MiB (10%) | 그대로인 형상의 signature | 500대 `planOf` 5.5 ms | 낮음(figure-model 안, 같은 규칙) |
137
+ | 밀어내기 매개변수 해석 | 3.4 MiB (4%) | 노드의 고정 매개변수에만 의존해 거의 전부 | 따로 재지 않음 | 매우 낮음 |
138
+ | 행 `repeat@1` 실행 | 9.8 MiB (13%) | 사본 id·path·offset 일부(P2) | 따로 재지 않음 | 중간(id·path·선택 의미) |
139
+ | rig 외형 `fromChanges` | 7.9 MiB (10%) | 중간 body wrapper·중첩 자세의 일부 | 500대 1.75 ms | 중간(lastDrawn·LOD·실패 시 유지) |
140
+ | 밀어내기 형상 생성(`runExtrude` 본체) | 9.3 MiB (12%) | 낮음(새 크기의 형상은 새로 만들어야 한다) | — | — |
141
+
142
+ - 첫 대상을 `fromChanges`에서 병합 은행 조회의 부품 형상 키로 바꾼다. 할당은 `fromChanges`보다 약간 크고, 제거 가능한 몫이 분명하며,
143
+ 직접 CPU 근거가 있고, 계약 위험이 낮다. 두 번째는 같은 규칙의 배치 계획 signature, 세 번째는 밀어내기 매개변수 해석이다.
144
+ - 이 두 키 캐시는 2026-10-05에 한 번 구현해 되돌린 후보다(형상별 키 캐시: 배치 계획 약 0.2 ms 감소, 전체 프레임 효과 미확인,
145
+ things-scene `docs/plans`와 completeness review 기록). 그때 남긴 시험(`things-scene/test/figure-geometry-key.check.js`,
146
+ figure-model `render-batches.test.ts`)을 출발점으로 쓴다. 당시 판단은 60대 위주의 페이지 교차 측정이었다. 이번에는 500대
147
+ 실행 단위 A/B로 판단하고, 효과가 변동과 구별되지 않으면 다시 보류한다.
148
+ - 할당 감소가 GC 시간에 비례한다고 가정하지 않는다. 채택은 비계측 전체 프레임 중앙값·p95와 메모리로 판단한다.
149
+ - 행 실행과 `fromChanges`는 그 뒤에 다시 고른다.
150
+
151
+ ### P1 첫 대상: 병합 은행의 부품 형상 키 캐시 — 채택하지 않음 (2026-10-05)
152
+
153
+ things-scene `candidate/merged-shape-key`(병합하지 않음). 깊게 동결된 형상만 형상 객체마다 segments별 키를 `WeakMap`에 둔다. 가변
154
+ 형상과 겉만 동결된 형상은 매번 계산한다. 시험: 키 시험에 LOD 단계(segments) 전환을 더했고, figure 시험 92개와 단위 시험 2,713개가
155
+ 통과했다. 모든 형상 캐시, 겉만 동결 검사, segments 무시 결함을 심어 실패를 확인했다.
156
+
157
+ 측정(`check-figure-gap.mjs`, 보드, 크기만 교대, 사전 고정 6 round × D1·D2·C, 순서 여섯 가지 한 번씩, 워밍업 30, 비계측 150프레임).
158
+ D는 v10 `f287f5f7` 빌드, C는 그 위에 이 후보만 얹은 빌드다.
159
+
160
+ | 500대 | A/A (D2−D1) round별 | 후보 (C−D1) round별 |
161
+ | ---------------------------- | -------------------------------- | ------------------------------ |
162
+ | 프레임 중앙값 차이 ms | −0.35 −0.09 −0.62 −0.46 +0.42 −0.10 | +0.57 +0.30 +1.33 +1.68 +0.70 +0.53 |
163
+ | p95 차이 ms | −0.36 −2.06 −2.02 −0.55 +0.49 −0.22 | +3.09 +6.74 +7.10 +6.25 +5.83 +6.87 |
164
+
165
+ | 500대 | D1 | D2 | C |
166
+ | ------------------------- | ------------------- | ------------------- | -------------------- |
167
+ | 할당 MB/frame(6회) | 76.75~77.26 | 76.92~77.39 | 72.61~73.05 |
168
+ | 보유 MB(측정 뒤 GC) | 42.7~43.3 | 42.7~43.3 | 46.9 |
169
+ | 단계 계측 merged / GC ms | 5.49 / 4.72 | 5.80 / 4.51 | 5.63 / 5.36 |
170
+
171
+ 60대(회귀 확인, 2 round): 프레임 차이 −0.63, −0.08 ms(A/A −2.97, +0.22), 할당 10.5 → 10.0 MB/frame, 보유 10.1 → 10.7 MB. 작업량(draw call,
172
+ 삼각형, body, 사본)은 두 판이 같았다.
173
+
174
+ 판정: 채택하지 않는다. 할당은 프레임당 약 4.2 MB(5%) 줄었지만, 500대에서 전체 프레임 중앙값이 6 round 모두 0.3~1.7 ms, p95가 3~7 ms
175
+ 나빠졌고 보유 메모리가 3.6 MB 늘었다. 병합 단계 시간은 줄지 않았다. 크기 변경마다 새로 생기는 형상 객체가 짧게 사는 WeakMap 키로
176
+ 쌓여 GC 처리가 늘었다는 해석은 GC 시간·보유량 증가와 맞지만 직접 확인하지 않았다.
177
+
178
+ 다음: 지시대로 상수 node parameter 파싱(밀어내기 매개변수 해석, 500대 3.4 MiB)으로 넘어간다.
179
+
180
+ ### P1 두 번째: 상수 node parameter 파싱 — 효과 미확인, 보류 (2026-10-05)
181
+
182
+ figure-model `candidate/extrude-param-cache`(병합하지 않음). 컴파일된 plan이 자기 사본의 node 매개변수를 동결하고, `extrude@1`이 실행
183
+ 중에 읽는 레이아웃과 description을 동결된 매개변수 객체마다 한 번만 만든다. 호출자 문서의 매개변수는 동결하지 않고, 동결되지 않은
184
+ 매개변수는 매번 읽는다. 결과에는 그 객체가 들어가지 않는다(숫자와 이름만 복사). 시험 `src/shape-params.test.ts` 3개, 결함 3종(동결 여부
185
+ 무시, 결과 faces 공유, 호출자 매개변수 동결)을 심어 실패를 확인했고 기존 759개가 통과했다(figure-model HEAD는 다른 세션의
186
+ `src/render-lod.test.ts` 타입 오류 하나로 `npm test`가 타입 검사에서 멈춰 있어 `node --test`로 직접 돌렸다. 정상 `npm test`·build 검증은
187
+ 그 오류가 고쳐진 뒤에 다시 한다 — 직접 실행한 통과만으로 전체 검증 완료가 아니다).
188
+
189
+ 측정은 첫 후보와 같다(보드, 크기만 교대, 6 round × D1·D2·C, 순서 여섯 가지, 워밍업 30, 비계측 150프레임). things-scene은 같은 `f287f5f7`이고
190
+ D는 figure-model HEAD `83737e3`, C는 그 위에 이 후보만 얹었다.
191
+
192
+ | 500대 | A/A (D2−D1) round별 | 후보 (C−D1) round별 |
193
+ | ---------------------- | -------------------------------------- | --------------------------------------- |
194
+ | 프레임 중앙값 차이 ms | +0.02 −0.20 −1.38 −1.07 +0.93 +0.54 | −0.76 −1.31 −1.07 −1.20 +0.51 −1.32 |
195
+ | p95 차이 ms | −1.10 +0.92 −2.44 −1.10 +1.92 −0.78 | −2.78 −2.49 −1.79 +1.28 +1.61 −1.48 |
196
+
197
+ | 500대 | D1 | D2 | C |
198
+ | ----------------------------- | ------------ | ------------ | ------------ |
199
+ | 할당 MB/frame(6회) | 76.84~77.19 | 76.86~77.31 | 70.51~71.12 |
200
+ | 보유 MB(측정 뒤 GC) | 42.7~43.3 | 42.7~43.3 | 43.3 |
201
+ | 단계 계측 커널 / GC ms | 20.08 / 4.71 | 19.78 / 4.74 | 18.79 / 4.51 |
202
+
203
+ 60대(회귀 확인, 2 round): 프레임 차이 +0.94, +0.25 ms(A/A −0.05, −0.23), 커널 2.51 → 2.44 ms, 할당 10.8 → 10.0 MB/frame, 보유 같음. 작업량은
204
+ 같았다. 기능 회귀는 없다(시험과 작업량). 성능 회귀는 미확정이다 — 60대 두 회차 모두 후보가 느렸고, 2회로는 판단할 수 없다.
205
+
206
+ 판정: 효과 미확인으로 보류하고 후보로 보존한다. 할당이 프레임당 약 6 MB(8%) 줄고 단계 계측의 커널이 약 1.1 ms 줄었지만, 전체 프레임 중앙값 차이는
207
+ 6 round 중 5회 감소했어도 모두 같은 조건 A/A의 범위(−1.38~+0.93) 안에 있고, p95는 섞였다. 메모리 비용은 없다.
208
+
209
+ 다음 차례(사용자 지시의 순서는 여기까지): 남은 후보는 행 `repeat@1` 실행(P2, 9.8 MiB)과 `fromChanges`(7.9 MiB)다. 커널과 할당의
210
+ 감소가 측정되는 후보들이 단독으로는 전체 프레임 검출 폭에 닿지 않으므로, 다음 결정이 필요하다 — 후보를 계속 하나씩 볼지, 측정 반복을
211
+ 늘려 검출 폭을 낮출지.
212
+
213
+ ### P2 첫 후보: 행 `repeat@1` 실행의 오프셋·id 재사용 — 기본 채택 보류 (2026-10-05)
214
+
215
+ figure-model `candidate/repeat-run` `07625f8`(병합하지 않음, a5363bd 위). 일반 평가의 행 실행이 복사본 하나의 오프셋 자세
216
+ `compose(pose, { r: identity(), t })`를 멤버마다가 아니라 복사본마다 한 번 만들고, 행 node의 멤버 key·path가 값으로 같은 동안 복사본 id
217
+ 문자열을 다시 쓴다. compose 호출과 그 순서는 같아서 자세는 이전 실행과 bit 단위로 같다. path는 복사본마다 새 배열이다(전체 평가 결과는
218
+ 호출자 소유). 자세 단계의 끝 행(terminal) 경로는 이미 오프셋을 재사용하고 있어 손대지 않았다.
219
+
220
+ 시험 `src/repeat-run.test.ts` 4개를 구현 전에 썼다: 처음 작성된 행 실행을 시험 안에 두고 fixture 시퀀스 전체에서 −0까지 bit 비교(id·path·
221
+ key·index·shape 포함), 개수 증감, 안쪽 두 행의 개수가 3+2 ↔ 2+3으로 바뀌어 멤버 수는 같고 path만 다른 격자(바깥 행은 z 회전·rise 포함),
222
+ path가 복사본마다·평가마다 다른 배열인지. 결함 다섯(멤버 검사 누락, 멤버 수만 검사, path 공유, 합성 순서 변경, rise 누락)을 심어 모두
223
+ 실패를 확인했다. fixture만으로는 멤버 검사 누락과 합성 순서 변경이 통과해서 격자 시험을 더했다. HEAD 구현도 네 시험을 통과한다.
224
+ 직접 실행한 시험 763개 통과, 정상 시험 명령은 기존 타입 오류로 차단(`src/render-lod.test.ts:42`, a5781eb). 정상 `npm test`·build가
225
+ 통과해야 최종 검증이 끝난다.
226
+
227
+ 측정은 앞 후보들과 같다(보드, 크기만 교대, 6 round × D1·D2·C, 순서 여섯 가지, 워밍업 30, 비계측 150프레임; 60대는 회귀 확인용 2 round).
228
+ things-scene `f287f5f7`, D = figure-model `a5363bd`, C = 이 후보. D 번들은 매개변수 파싱 측정의 기준 번들과 바이트 단위로 같다.
229
+
230
+ | 500대 | A/A (D2−D1) round별 | 후보 (C−D1) round별 |
231
+ | ---------------------- | -------------------------------------- | --------------------------------------- |
232
+ | 프레임 중앙값 차이 ms | −0.32 +0.34 −0.34 +1.38 +0.27 +1.43 | +1.87 −0.13 −0.98 −0.38 −0.70 −1.14 |
233
+ | p95 차이 ms | +0.07 +0.17 −1.85 +3.85 +1.68 +3.59 | +4.57 −0.15 −1.78 −0.46 +1.16 −0.40 |
234
+
235
+ | 500대 | D1 | D2 | C |
236
+ | ----------------------------- | ------------ | ------------ | ------------ |
237
+ | 할당 MB/frame(6회) | 77.19~77.42 | 76.89~77.38 | 76.44~76.82 |
238
+ | 보유 MB(측정 뒤 GC) | 42.7~43.2 | 42.7~43.3 | 42.2~42.7 |
239
+ | 단계 계측 커널 / GC ms | 19.67 / 4.55 | 20.06 / 4.92 | 19.09 / 4.57 |
240
+
241
+ 작업량은 같았다: draw call 2001, 삼각형 944012, body 20000, 행 복사 12000(60대: 241, 113292, 2400, 1440).
242
+
243
+ 60대(2 round): 프레임 중앙값 차이 −0.22, +0.59 ms(A/A −0.38, −0.11), p95 차이 +4.01, +0.13 ms(A/A +0.28, −0.61). 커널 2.47 → 2.35 ms
244
+ (D2도 2.35), 할당 10.49~10.70 → 10.41~10.42 MB/frame, GC 0.44 → 0.42 ms, 보유 10.1 → 10.3 MB. 검증한 시험과 측정 장면에서 기능 차이를 발견하지 못했다(정상 시험 명령은 아직 차단).
245
+ 성능 회귀는 미확정이다 — 첫 회차 p95가 A/A 밖으로 나빠졌고, 2회로는 반복되는지 가를 수 없다.
246
+
247
+ 원인 확인(별도 할당 프로필, 500대 3 round, 이 실행의 프레임 시간은 판단에 쓰지 않는다): 실제로 서빙된 번들
248
+ (`things-scene/test/browser/figure-vs-js/gap-D/bundle.js` sha256 `ac1f744a…bd7a0`, `gap-C/bundle.js` sha256 `021859f9…c338`)에서
249
+ 장면용 `repeat@1` 실행 화살표 함수를 0부터 센 위치로 찾았다 — D 97275행 230217열, C 97275행 230238열. 같은 번들에는 측정 도구가
250
+ `parseFigureAsset`용으로 함께 묶은 node_modules figure-model의 사본이 98711행 12열에 있다. 각 샘플은 자기 node의 selfSize로 한 번만
251
+ 세고, 호출 경로 어디에든 그 함수가 있으면 넣었다(부모·자식 inclusive를 더하지 않는다).
252
+
253
+ | 500대, MiB/frame(3회 평균) | D1 | D2 | C |
254
+ | ------------------------------ | ----- | ----- | ----- |
255
+ | 장면용 `repeat@1`(하위 호출 포함) | 9.738 | 9.737 | 9.211 |
256
+ | `parseFigureAsset`용 사본 | 0 | 0 | 0 |
257
+ | 두 사본이 한 경로에 함께 | 0 | 0 | 0 |
258
+ | 전체 | 77.21 | 77.19 | 76.75 |
259
+
260
+ 원자료: `allocation-board-500-{D1,D2,C}-r{1,2,3}-17912030…json`(같은 폴더). 서빙 번들 사본은 `figure-vs-js/repeat-run-2026-10-05/`
261
+ (다음 측정이 `gap-*/bundle.js`를 덮으므로). 재집계: things-scene `test/browser/figure-allocation-under.mjs`(91a5ac30) — 위치·번들 해시를
262
+ 인자로 받고 해시가 다르면 거절한다.
263
+ 행 실행의 할당은 약 0.53 MiB/frame(5.4%) 줄었고, 전체 할당 감소(약 0.45 MiB, 0.6%)와 크기가 맞는다. 할당 감소를 GC 감소로 옮겨 적지 않는다.
264
+
265
+ 판정: 기본 채택 보류, 후보와 검증 기록을 보존한다. 500대 프레임 중앙값은 6회 중 5회 줄었지만 +1.87 ms(A/A −0.34~+1.43 밖, 위쪽)와
266
+ −0.98·−0.38·−0.70·−1.14 ms(아래쪽 밖)가 섞여 있고, 범위를 벗어났다는 것만으로 통계적으로 확정된 차이는 아니다. p95는 500대와 60대
267
+ 모두 첫 회차에서 나빠졌다. 일관된 개선은 확인하지 못했고, p95 악화의 반복성도 미확정이다. 할당 감소는 이 판정을 대신하지 않는다.
268
+
269
+ ### P1 세 번째: `fromChanges`의 중간 객체 — 기본 채택 보류 (2026-10-05)
270
+
271
+ things-scene `candidate/from-changes-dress` `3e7101e6`(병합하지 않음, v10 `91a5ac30` 위). 변경분이 알린 body 하나마다 기존 코드가
272
+ 만들던 할당 지점 가운데 셋을 없앴다: (1) `fromChanges`가 먼저 만든 body 객체 — `dressOne`이 이것을 펼쳐 최종 body를 다시 만들었다,
273
+ (2) `dressOne`의 반환 포장 `{ dressed, visible, ref }`, (3) 표시 여부를 읽는 함수 객체. 최종 body는 그림이 담는 결과 객체라 여전히
274
+ 만든다(펼치기 대신 처음부터 그 모양으로). 새 자세 객체와 그 배열 넷도 그대로 만든다 — 이전 그림이 그 자세를 계속 갖는다. 모양과 재질
275
+ 조회는 `dressOne`과 같은 도움 함수를 쓴다. `evaluation()`·`evaluateOnce()`는 바꾸지 않았다.
276
+
277
+ 시험(구현 전): 앞 단계에서 남긴 그림이 뒤 단계 뒤에도 그대로인지, 각 body가 그 모양의 재질 객체 하나를 쓰는지, `evaluation()`에 쓴 값이
278
+ rig가 그린 것에 닿지 않는지(`test/figure-merge.check.js`). 결함 다섯(body마다 새 재질, 표시 여부 무시, 필드 순서, 이전 그림의 자세
279
+ 배열에 덮어쓰기, segments 누락)을 심어 모두 실패를 확인했다. `npm run test:figure` 92개(figure-model 소스 대상), `npm test` 2715개 통과.
280
+
281
+ 측정: 앞 후보들과 같은 방식. D = things-scene `91a5ac30`, C = 이 후보, figure-model은 둘 다 `a5363bd` dist. 이번 D는 앞 후보들의 D와
282
+ 같지 않다(그 사이 v10에 1dd84d39가 들어왔다) — 앞 후보들과 절대 시간을 비교하지 않는다.
283
+
284
+ | 500대 | A/A (D2−D1) round별 | 후보 (C−D1) round별 |
285
+ | ---------------------- | -------------------------------------- | --------------------------------------- |
286
+ | 프레임 중앙값 차이 ms | +0.05 −1.39 +0.18 +6.77 +5.78 +3.16 | +0.69 −0.38 +1.92 +4.78 +4.61 +1.24 |
287
+ | p95 차이 ms | +0.05 −8.80 +1.53 +6.43 +5.20 +3.79 | +1.62 −6.71 +2.13 +4.62 +3.06 −1.29 |
288
+
289
+ | 500대 | D1 | D2 | C |
290
+ | ----------------------------- | ------------ | ------------ | ------------ |
291
+ | 할당 MB/frame(6회) | 76.91~77.33 | 77.01~77.44 | 76.49~76.80 |
292
+ | 보유 MB(측정 뒤 GC) | 42.7~43.3 | 42.7~43.2 | 42.7~43.3 |
293
+ | 단계 계측 `fromChanges` ms | 1.79 | 1.82 | 2.43 |
294
+ | 단계 계측 커널 / GC ms | 19.75 / 5.31 | 20.05 / 4.73 | 19.90 / 4.91 |
295
+
296
+ 이번 500대 A/A는 4~6회차에서 +3~+7 ms로 크게 벌어졌다(같은 build끼리) — 실행 간 변동이 컸다는 근거다. 환경 변화가 원인인지는
297
+ 확인하지 않았다. 그 폭 안에서는 후보의 프레임 효과를 가를 수 없다. 작업량은 같았다(draw call 2001, 삼각형 944012, 장면 body 20000, 행 복사 12000; 60대 241, 113292, 2400, 1440).
298
+
299
+ 60대(2 round): 프레임 중앙값 차이 +0.66, +0.42 ms(A/A +0.29, +0.16), p95 +0.24, −0.37 ms(A/A +0.41, −1.01; 둘 다 A/A 범위 −1.01~+0.41 안이지만 이 범위만으로 효과를 판정하지 않는다). 단계 계측 `fromChanges`
300
+ 0.21·0.20 → 0.32 ms, 할당 10.68~10.79 → 10.08~10.39 MB/frame, 보유 같음. 검증한 시험과 측정 장면에서 기능 차이를 발견하지 못했다.
301
+
302
+ 원인 확인(별도 할당 프로필, 500대 3 round): 서빙 번들 사본 `figure-vs-js/from-changes-2026-10-05/gap-D.bundle.js` sha256
303
+ `a82cb950…f944`, `gap-C.bundle.js` sha256 `1e851a55…a489`, `fromChanges`는 두 번들 모두 0부터 센 97275행 332754열.
304
+ `test/browser/figure-allocation-under.mjs`로 집계: `fromChanges`(하위 호출 포함) D1 7.938 · D2 7.908 · C 6.932 MiB/frame(약 1.0 MiB,
305
+ 12.6% 감소), 전체 77.28 · 77.09 · 76.49.
306
+
307
+ 변경 body 수(별도 계측 실행, 비계측 비교와 분리; 같은 Figure, 같은 두 크기, rig 500개, 30프레임, A·B 같음): 프레임마다 changes step 500(rig마다 하나),
308
+ 변경분이 알린 body 20000, 새로 만든 최종 body 20000, structure 0. 크기 교대가 모든 body를 옮기므로 이 장면에서는
309
+ 변경 body 수가 장면 body 수와 같다 — 다른 입력에서는 다를 수 있다. 할당 감소를 body 수로 나누면 약 52 B/body다(표본 추정).
310
+
311
+ 판정: 기본 채택 보류, 후보와 기록을 보존한다. `fromChanges` 할당은 약 1.0 MiB/frame 줄었지만, 단계 계측의 `fromChanges` 시간이 500대에서
312
+ 약 0.6 ms, 60대에서 약 0.1 ms 늘었다. 60대 프레임 중앙값은 두 회차 모두 A/A보다 느렸다(성능 회귀 미확정). 500대 프레임
313
+ 효과는 이 실행의 A/A 폭 안이라 확인하지 못했다. 시간이 늘어난 이유는 추론이다: 최종 body를 `{ id }`에서 시작해 속성 8~9개를 차례로
314
+ 더하면 객체 안의 자리를 넘어 별도 속성 저장소가 생길 수 있고, 펼치기는 처음부터 맞는 크기로 만든다 — 확인하지 않았다.
@@ -1,4 +1,4 @@
1
- # v3 레이아웃 설계 검증 예제
1
+ # Figure 레이아웃 설계 검증 예제
2
2
 
3
3
  `node --test docs/prototypes/typed-repeat.test.mjs`: 단위/좌표계 그래프와 반복 조립체 통합.
4
4
  [배치 의도 변경과 검증 한계](../typed-repeat-contracts.md)를 함께 확인한다.
@@ -17,7 +17,7 @@
17
17
  모델별 분기 없는 실행기와 실제 샘플에서 유도한 선언을 분리했다.
18
18
  범위·213개 크기/상태 조합·반례·미지원 항목은
19
19
  [공통 커널 실험 기록](../common-kernel-experiment.md)을 따른다.
20
- 제품 v3 구현이나 전체 샘플 변환 완료가 아니다.
20
+ 제품 Figure 구현이나 전체 샘플 변환 완료가 아니다.
21
21
 
22
22
  ## 외부 연구 구현 probe
23
23
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Experimental V3 asset envelope; not a complete V2 migration contract",
3
+ "title": "Experimental Figure asset envelope; not a complete V2 migration contract",
4
4
  "type": "object", "additionalProperties": false,
5
5
  "required": ["version", "name", "document", "designInputs", "stateDefaults", "palette", "appearance"],
6
6
  "properties": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Experimental V3 graph and capability document (not the complete Figure asset format)",
3
+ "title": "Experimental Figure graph and capability document (not the complete Figure asset format)",
4
4
  "type": "object",
5
5
  "additionalProperties": false,
6
6
  "required": ["version", "model", "capabilities"],
@@ -16,9 +16,9 @@ import {geometryOf as portal} from './full-dryroom.mjs'
16
16
  const directory=await mkdtemp(join(tmpdir(),'editor-store-'))
17
17
  const store=createAssetStore(directory),workspace=createEditorWorkspace();let saved=null,storage=null
18
18
  const view=()=>{const v=workspace.read();return {...v,render:v.geometry.map(g=>{const mesh=(g.shape.provider==='portal'?portal:ordinary)(g);const json=mesh.toJSON();mesh.dispose();return {id:g.id,geometry:json,material:g.material}})}}
19
- const html=`<!doctype html><meta charset="utf-8"><title>V3 편집 연결 검증</title>
19
+ const html=`<!doctype html><meta charset="utf-8"><title>Figure 편집 연결 검증</title>
20
20
  <style>body{margin:0;font:14px system-ui;color:#263543;background:#eef2f6}header{padding:16px;display:flex;gap:10px;align-items:center;background:white;border-bottom:1px solid #ccd5dd}main{display:grid;grid-template-columns:1fr 340px;height:650px}aside{padding:18px;background:white;overflow:auto}h2{font-size:15px;margin:22px 0 12px}label{display:grid;grid-template-columns:110px 1fr 46px;align-items:center;gap:8px;margin:8px 0}input,button,select{height:36px;box-sizing:border-box;border:1px solid #bdc9d2;border-radius:5px;background:white;padding:5px 8px}input{min-width:0;width:100%}button{cursor:pointer}#canvas{overflow:hidden}#status{padding:12px}#error{color:#a12626;white-space:pre-wrap}small{color:#526574}</style>
21
- <header><strong>V3 편집 연결 검증</strong><button id="new">새 Figure</button><button id="add">상자 추가</button><select id="model"><option value="forklift">포크리프트</option><option value="robot">로봇암</option><option value="conveyor">컨베이어</option><option value="dryroom">드라이룸</option></select><button id="open">열기</button><button id="undo">되돌리기</button><button id="redo">다시 실행</button><button id="save">검증 저장</button><button id="reload">저장본 열기</button><button id="close">빈 화면</button></header>
21
+ <header><strong>Figure 편집 연결 검증</strong><button id="new">새 Figure</button><button id="add">상자 추가</button><select id="model"><option value="forklift">포크리프트</option><option value="robot">로봇암</option><option value="conveyor">컨베이어</option><option value="dryroom">드라이룸</option></select><button id="open">열기</button><button id="undo">되돌리기</button><button id="redo">다시 실행</button><button id="save">검증 저장</button><button id="reload">저장본 열기</button><button id="close">빈 화면</button></header>
22
22
  <main><section><div id="canvas"></div><div id="status"></div></section><aside><small>제품 화면이 아닌 격리된 실험 UI · 임시 파일에 저장</small><div id="fields"></div><p id="error" role="alert"></p></aside></main>
23
23
  <script type="module">
24
24
  import * as T from '/three.module.js';
@@ -1,6 +1,6 @@
1
1
  """Independent linear design comparison; not a production evaluator.
2
2
  Requires kiwisolver==1.4.9 in an isolated research environment.
3
- Run: python docs/prototypes/v3-linear-comparison.py
3
+ Run: python docs/prototypes/linear-comparison.py
4
4
  All lengths mm. No external research implementation is copied.
5
5
  """
6
6
  import itertools
@@ -1,5 +1,5 @@
1
1
  """Software orthographic rendering of the actual generated triangle meshes.
2
- Input: JSON from v3-dryroom-mesh.mjs on stdin. Requires Pillow, no product changes.
2
+ Input: JSON from dryroom-mesh.mjs on stdin. Requires Pillow, no product changes.
3
3
  Output path must be explicitly supplied. Not a screenshot of the product renderer.
4
4
  """
5
5
  import json,sys,math
@@ -30,29 +30,29 @@ function payloadVariants(){
30
30
  const variants=isPayload?payloadVariants():isForklift?[
31
31
  {title:'Source seed · rest pose',parts:pack(forklift.seed.parts.map(p=>({id:p.name,material:p.material,geometry:forklift.sourceGeometry(p)})))},
32
32
  ...[
33
- ['V3 · same rest pose',forklift.defaults],
34
- ['V3 · lift 1000 / reach 200 / drive 37',{...forklift.defaults,lift:1000,reach:200,drive:37}],
35
- ['V3 · longer forks / lift 2000 / reach 400',{forkLength:1275,lift:2000,reach:400,drive:73}]
33
+ ['Figure · same rest pose',forklift.defaults],
34
+ ['Figure · lift 1000 / reach 200 / drive 37',{...forklift.defaults,lift:1000,reach:200,drive:37}],
35
+ ['Figure · longer forks / lift 2000 / reach 400',{forkLength:1275,lift:2000,reach:400,drive:73}]
36
36
  ].map(([title,args])=>({title,parts:pack(forklift.evaluateFull(args).map(x=>({...x,geometry:forklift.geometryOf(x)})))}))
37
37
  ]:isRobot?[
38
38
  {title:'Source fixture · rest pose',parts:pack(robot.seed.parts.map(p=>({id:p.name,material:p.material??{token:'palette.neutral'},geometry:robot.sourceGeometry(p)})))},
39
39
  ...[
40
- ['V3 · same rest pose',robot.defaults],
41
- ['V3 · six joints + gripper',robotPose],
42
- ['V3 · changed link lengths + same joints',{...robotPose,upperLength:630,foreLength:570}]
40
+ ['Figure · same rest pose',robot.defaults],
41
+ ['Figure · six joints + gripper',robotPose],
42
+ ['Figure · changed link lengths + same joints',{...robotPose,upperLength:630,foreLength:570}]
43
43
  ].map(([title,args])=>({title,parts:pack(robot.evaluateFull(args).map(x=>({...x,geometry:robot.geometryOf(x)})))}))
44
44
  ]:isConveyor?[
45
45
  {title:'Source seed · overlapping end rollers',parts:pack(conveyor.sourceItems())},
46
46
  ...[
47
- ['V3 · reserved end roller space',conveyor.defaults],
48
- ['V3 · roller rotation 37 degrees',{...conveyor.defaults,angle:37}],
49
- ['V3 · resized assembly',{length:3000,width:900,rollerHeight:700,angle:37}]
47
+ ['Figure · reserved end roller space',conveyor.defaults],
48
+ ['Figure · roller rotation 37 degrees',{...conveyor.defaults,angle:37}],
49
+ ['Figure · resized assembly',{length:3000,width:900,rollerHeight:700,angle:37}]
50
50
  ].map(([title,args])=>({title,parts:pack(conveyor.evaluateFull(args).map(x=>({...x,geometry:conveyor.geometryOf(x)})))}))
51
51
  ]:[
52
52
  {title:'Source seed · six parts',parts:pack(seed.parts.map(p=>({id:p.name,material:p.material,geometry:sourceGeometry(p)})))},
53
- {title:'V3 · same dimensions',parts:fromFigure(defaults)},
54
- {title:'V3 · portal bottom trimmed to floor',parts:fromFigure({...defaults,bottom:0})},
55
- {title:'V3 · resized + lamps raised',parts:fromFigure({...defaults,width:4800,height:3600,depth:7200,bottom:0,lampLift:100})}
53
+ {title:'Figure · same dimensions',parts:fromFigure(defaults)},
54
+ {title:'Figure · portal bottom trimmed to floor',parts:fromFigure({...defaults,bottom:0})},
55
+ {title:'Figure · resized + lamps raised',parts:fromFigure({...defaults,width:4800,height:3600,depth:7200,bottom:0,lampLift:100})}
56
56
  ]
57
57
  const html=`<!doctype html><meta charset="utf-8"><style>body{margin:0;background:#e9edf2;font:16px sans-serif;color:#17283b}main{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}section{background:white;border-radius:8px;overflow:hidden}h3{margin:12px;font-size:16px}canvas{display:block;width:100%}p{margin:12px;font-size:12px}</style><main></main><script type="module">
58
58
  import * as T from '/three.module.js';
@@ -15,9 +15,9 @@ const graphEntry=url(graphSource.replace('./contract.js',contract))
15
15
  const modules=new Map()
16
16
  async function moduleUrl(name){
17
17
  if(modules.has(name))return modules.get(name)
18
- assert.match(name,/^v3-[a-z-]+\.js$/)
18
+ assert.match(name,/^[a-z-]+\.js$/)
19
19
  let code=await readFile(new URL('../../dist/'+name,import.meta.url),'utf8')
20
- for(const match of [...code.matchAll(/from ['"]\.\/(v3-[a-z-]+\.js)['"]/g)]){
20
+ for(const match of [...code.matchAll(/from ['"]\.\/([a-z-]+\.js)['"]/g)]){
21
21
  code=code.replace(match[0],'from '+JSON.stringify(await moduleUrl(match[1])))
22
22
  }
23
23
  const entry=url(code);modules.set(name,entry);return entry
@@ -64,7 +64,7 @@ try{
64
64
  console.log(JSON.stringify({browserWorkspace:editing},null,2))
65
65
  const authored=await page.evaluate(async({createEntry,assetEntry,workspaceEntry})=>{
66
66
  const c=await import(createEntry),a=await import(assetEntry),w=(await import(workspaceEntry)).createFigureEditorWorkspace()
67
- const empty=c.createFigureAsset('New V3'),emptyCount=a.compileFigureAsset(empty).evaluate().geometry.length
67
+ const empty=c.createFigureAsset('New Figure'),emptyCount=a.compileFigureAsset(empty).evaluate().geometry.length
68
68
  const model=c.addFigureBox(empty,{id:'gate',size:{x:800,y:2000,z:100},position:{x:0,y:1000,z:0}})
69
69
  const opened=w.open(a.serializeFigureAsset(model))
70
70
  w.apply({context:opened.context,origin:'ui',expectedRevision:0,changes:[{kind:'set-design',input:'gate.size.y',unit:'mm',value:1500}]})
@@ -1,4 +1,4 @@
1
- # Figure v3 선행 연구 검토와 채택 판단
1
+ # Figure 선행 연구 검토와 채택 판단
2
2
 
3
3
  > 현재 구현·검증·제품 연결 상태는 [구현·검증 현황](status.md)을 따른다. 본문의 단계별 연구/검증 기록은 제품 전체 완료 선언이 아니다.
4
4
 
@@ -1,4 +1,4 @@
1
- # V3 평가 — figure 레인, 2026-09-22
1
+ # Figure 평가 — figure 레인, 2026-09-22
2
2
 
3
3
  figure-model 0.1.32 (`54d30c2`) 기준. 문서를 읽고 판단한 것이 아니라 **돌려서 확인한 것**을 적는다.
4
4
  확인에 쓴 명령은 항목마다 적어 두었다.
@@ -6,7 +6,7 @@ figure-model 0.1.32 (`54d30c2`) 기준. 문서를 읽고 판단한 것이 아니
6
6
  ## 한 줄
7
7
 
8
8
  **수학은 선다. 전환 경로가 없다.** 커널의 핵심 주장은 실측으로 성립하고 구현도 하나로 모여 있다.
9
- 막는 것은 설계가 아니라 **살아 있는 자산 32 개를 V3 로 옮길 길이 아직 없다**는 것이다.
9
+ 막는 것은 설계가 아니라 **살아 있는 자산 32 개를 지금 형식으로 옮길 길이 아직 없다**는 것이다.
10
10
 
11
11
  ## 1. 선 것 — 확인함
12
12
 
@@ -16,10 +16,10 @@ figure-model 0.1.32 (`54d30c2`) 기준. 문서를 읽고 판단한 것이 아니
16
16
  `src/graph.ts` 를 다시 내보낸다. 실험 143 개가 **제품 소스를 시험하고 있다.**
17
17
 
18
18
  ```
19
- src/v3-*.ts 13 모듈 · 2,266 줄(시험 포함)
19
+ src/ Figure 모듈 13 개 · 2,266 줄(시험 포함)
20
20
  index.ts 10 개를 내보낸다 — compileFigureGraph · createFigureAsset · …
21
21
  npm test 461 통과 · 0 빨강
22
- node --test docs/prototypes/v3-*.test.mjs 143 통과 · 0 빨강
22
+ node --test docs/prototypes/*.test.mjs 143 통과 · 0 빨강
23
23
  ```
24
24
 
25
25
  ### 핵심 주장이 실측으로 성립한다
@@ -44,34 +44,34 @@ CYCLE: a: a -> b -> a
44
44
 
45
45
  ### 현황 문서가 정직하다
46
46
 
47
- `status.md` 가 격리 UI 의 성공을 제품 배포로 적지 않고, 「소스 455 통과」가 V3 기능 수가
47
+ `status.md` 가 격리 UI 의 성공을 제품 배포로 적지 않고, 「소스 455 통과」가 Figure 기능 수가
48
48
  아니라고 먼저 밝힌다. 오늘 이 집이 여러 번 틀린 자리를 그쪽은 안 틀렸다.
49
49
 
50
50
  ## 2. 위험 — 위험 순
51
51
 
52
52
  ### ① 전환 경로가 없다. 자산 32 개를 손으로 다시 쓴다
53
53
 
54
- **V2 → V3 변환기가 없다.** 이름으로도 동작으로도 찾지 못했다.
54
+ **옛 형식(V2)에서 지금 형식으로 바꾸는 변환기가 없다.** 이름으로도 동작으로도 찾지 못했다.
55
55
 
56
- `cutover.md` 는 「기존 자산도 V3 기준으로 변환하거나 재작성한다」고 적는데, **변환**에
56
+ `cutover.md` 는 「기존 자산도 지금 형식 기준으로 변환하거나 재작성한다」고 적는데, **변환**에
57
57
  해당하는 것이 없고 재작성만 있다. 네 대표 자산(드라이룸·컨베이어·로봇암·포크리프트)은
58
- V2 seed 를 읽어 **결과를 견주는 용도**로 쓰고, V3 쪽은 손으로 적은 그래프다.
58
+ V2 seed 를 읽어 **결과를 견주는 용도**로 쓰고, 지금 형식 쪽은 손으로 적은 그래프다.
59
59
 
60
60
  ```js
61
61
  // docs/prototypes/full-conveyor.mjs — 컨베이어 하나가 이 밀도로 60 줄
62
62
  add('endX','halfLength',-60); add('legX','halfLength',-260); add('frameZ','halfWidth',-18)
63
63
  ```
64
64
 
65
- 그리고 **되돌아갈 자리도 없다.** 전환 결정으로 figure-ui·figure-service 의 V2/V3 분기
65
+ 그리고 **되돌아갈 자리도 없다.** 전환 결정으로 figure-ui·figure-service 의 옛 형식(V2)·지금 형식 분기
66
66
  코드를 이미 지웠다. 활성 자산 32 개가 전부 V2 다.
67
67
 
68
- > 이것이 V3 가 나갈지 말지를 정하는 한 가지다. 나머지는 다 그 뒤의 일이다.
68
+ > 이것이 지금 형식이 나갈지 말지를 정하는 한 가지다. 나머지는 다 그 뒤의 일이다.
69
69
 
70
- ### ② v2 가 세운 게이트가 V3 로 안 넘어왔다
70
+ ### ② v2 가 세운 게이트가 지금 형식으로 안 넘어왔다
71
71
 
72
- ADR-0044 와 ADR-0051 이 닫은 문 셋이 V3 소스에 없다.
72
+ ADR-0044 와 ADR-0051 이 닫은 문 셋이 지금 소스에 없다.
73
73
 
74
- | v2 의 문 | V3 소스 | V3 문서 |
74
+ | v2 의 문 | 지금 소스 | 지금 문서 |
75
75
  |---|---|---|
76
76
  | `part-outside-base` (ADR-0044) | 0 | 0 |
77
77
  | `parts-off-placement-face` (ADR-0045) | 0 | 0 |
@@ -80,13 +80,13 @@ ADR-0044 와 ADR-0051 이 닫은 문 셋이 V3 소스에 없다.
80
80
 
81
81
  `src/capabilities.ts` 는 있지만 **다른 것을 본다** — 표면 소유권과 수용 조건이다. 능력
82
82
  이름은 `name()` 이 「빈 문자열이 아니면 통과」로 받는다. **`capabilities: ['hoist']` 가
83
- V3 에서 다시 지나간다.** 그 구멍은 09-16 에 닫은 것이고, 닫기 전에는 검증과 발행을 지나
83
+ 지금 형식에서 다시 지나간다.** 그 구멍은 09-16 에 닫은 것이고, 닫기 전에는 검증과 발행을 지나
84
84
  렌더링에서 죽었다.
85
85
 
86
- 셋 다 「그려지지 않는 것」이라 V3 로 옮기는 동안 조용히 빠지기 쉽다. **화면에는 아무 일도
86
+ 셋 다 「그려지지 않는 것」이라 지금 형식으로 옮기는 동안 조용히 빠지기 쉽다. **화면에는 아무 일도
87
87
  안 일어난다.**
88
88
 
89
- ### ③ 지금 V3 를 저작할 길은 그래프 코드뿐이다
89
+ ### ③ 지금 형식을 저작할 길은 그래프 코드뿐이다
90
90
 
91
91
  `status.md` 의 남은 작업이 「다른 primitive 추가, 삭제·재질·관계 편집」이다. 지금 서 있는
92
92
  것은 빈 자산과 상자 하나다. 설계는 모델러와 AI 가 template 으로 이 식을 만든다고 하는데,
@@ -112,11 +112,11 @@ V3 에서 다시 지나간다.** 그 구멍은 09-16 에 닫은 것이고, 닫
112
112
 
113
113
  ①이 나머지를 다 막는다. 그래서 순서는 이것이다.
114
114
 
115
- 1. **표본 하나로 전환 비용을 잰다.** 컨베이어 하나를 V2 원본에서 V3 로 옮기는 데 사람이 몇
115
+ 1. **표본 하나로 전환 비용을 잰다.** 컨베이어 하나를 V2 원본에서 지금 형식으로 옮기는 데 사람이 몇
116
116
  분 걸리는지. 32 개를 곱하면 이 결정이 바로 보인다. 자동 변환이 어디까지 가능한지도 그때
117
117
  드러난다 — `sizing`·`anchor` 의 상당수는 기계적으로 옮길 수 있어 보인다.
118
- 2. **②의 문 셋을 V3 계약에 어떻게 세울지 정한다.** 옮기는 것이 아니라 V3 의 말로 다시
119
- 세워야 한다. 능력 이름은 목록을 그대로 쓸 수 있고, `base` 포함은 V3 의 `NO_SPACE` ·
118
+ 2. **②의 문 셋을 지금 계약에 어떻게 세울지 정한다.** 옮기는 것이 아니라 지금 형식의 말로 다시
119
+ 세워야 한다. 능력 이름은 목록을 그대로 쓸 수 있고, `base` 포함은 지금 형식의 `NO_SPACE` ·
120
120
  `LAYOUT_DOMAIN` 과 겹쳐 보인다.
121
121
  3. 그 뒤에 모델러 연결.
122
122