@hatiolab/figure-model 0.1.29 → 0.1.31
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/README.md +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/release-contract.d.ts +2 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +45 -3
- package/dist/release-contract.js.map +1 -1
- package/dist/types.d.ts +16 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -4
- package/dist/types.js.map +1 -1
- package/dist/v3-asset-types.d.ts +116 -0
- package/dist/v3-asset-types.d.ts.map +1 -0
- package/dist/v3-asset-types.js +2 -0
- package/dist/v3-asset-types.js.map +1 -0
- package/dist/v3-asset.d.ts +8 -0
- package/dist/v3-asset.d.ts.map +1 -0
- package/dist/v3-asset.js +135 -0
- package/dist/v3-asset.js.map +1 -0
- package/dist/v3-authoring-session.d.ts +119 -0
- package/dist/v3-authoring-session.d.ts.map +1 -0
- package/dist/v3-authoring-session.js +132 -0
- package/dist/v3-authoring-session.js.map +1 -0
- package/dist/v3-capabilities.d.ts +3 -0
- package/dist/v3-capabilities.d.ts.map +1 -0
- package/dist/v3-capabilities.js +221 -0
- package/dist/v3-capabilities.js.map +1 -0
- package/dist/v3-contract.d.ts +7 -0
- package/dist/v3-contract.d.ts.map +1 -0
- package/dist/v3-contract.js +12 -0
- package/dist/v3-contract.js.map +1 -0
- package/dist/v3-create.d.ts +25 -0
- package/dist/v3-create.d.ts.map +1 -0
- package/dist/v3-create.js +50 -0
- package/dist/v3-create.js.map +1 -0
- package/dist/v3-document.d.ts +5 -0
- package/dist/v3-document.d.ts.map +1 -0
- package/dist/v3-document.js +31 -0
- package/dist/v3-document.js.map +1 -0
- package/dist/v3-editor-workspace.d.ts +334 -0
- package/dist/v3-editor-workspace.d.ts.map +1 -0
- package/dist/v3-editor-workspace.js +130 -0
- package/dist/v3-editor-workspace.js.map +1 -0
- package/dist/v3-graph-types.d.ts +91 -0
- package/dist/v3-graph-types.d.ts.map +1 -0
- package/dist/v3-graph-types.js +2 -0
- package/dist/v3-graph-types.js.map +1 -0
- package/dist/v3-graph.d.ts +5 -0
- package/dist/v3-graph.d.ts.map +1 -0
- package/dist/v3-graph.js +631 -0
- package/dist/v3-graph.js.map +1 -0
- package/dist/v3-json.d.ts +13 -0
- package/dist/v3-json.d.ts.map +1 -0
- package/dist/v3-json.js +94 -0
- package/dist/v3-json.js.map +1 -0
- package/dist/v3-part-edit.d.ts +12 -0
- package/dist/v3-part-edit.d.ts.map +1 -0
- package/dist/v3-part-edit.js +53 -0
- package/dist/v3-part-edit.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +12 -11
- package/dist/validate.js.map +1 -1
- package/docs/assembly-constraints-v3.md +141 -0
- package/docs/prototypes/README.md +73 -0
- package/docs/prototypes/render-dryroom.py +60 -0
- package/docs/prototypes/render-full-dryroom.mjs +86 -0
- package/docs/prototypes/shapeassembly-probe-results.json +325 -0
- package/docs/prototypes/shapeassembly-probe.py +84 -0
- package/docs/prototypes/v3-asset-samples.mjs +15 -0
- package/docs/prototypes/v3-asset-store.mjs +52 -0
- package/docs/prototypes/v3-asset-store.test.mjs +59 -0
- package/docs/prototypes/v3-asset.mjs +1 -0
- package/docs/prototypes/v3-asset.schema.json +44 -0
- package/docs/prototypes/v3-asset.test.mjs +68 -0
- package/docs/prototypes/v3-authoring-session.mjs +1 -0
- package/docs/prototypes/v3-authoring-session.test.mjs +65 -0
- package/docs/prototypes/v3-capability-plan.mjs +1 -0
- package/docs/prototypes/v3-capability-plan.test.mjs +56 -0
- package/docs/prototypes/v3-capability-samples.mjs +23 -0
- package/docs/prototypes/v3-document.mjs +2 -0
- package/docs/prototypes/v3-document.schema.json +66 -0
- package/docs/prototypes/v3-document.test.mjs +55 -0
- package/docs/prototypes/v3-dryroom-mesh.mjs +60 -0
- package/docs/prototypes/v3-dryroom-mesh.test.mjs +30 -0
- package/docs/prototypes/v3-editor-browser.mjs +97 -0
- package/docs/prototypes/v3-editor-workspace.mjs +1 -0
- package/docs/prototypes/v3-editor-workspace.test.mjs +35 -0
- package/docs/prototypes/v3-feature-pattern.test.mjs +131 -0
- package/docs/prototypes/v3-full-conveyor.mjs +60 -0
- package/docs/prototypes/v3-full-conveyor.test.mjs +61 -0
- package/docs/prototypes/v3-full-dryroom.mjs +67 -0
- package/docs/prototypes/v3-full-dryroom.test.mjs +54 -0
- package/docs/prototypes/v3-full-forklift.mjs +40 -0
- package/docs/prototypes/v3-full-forklift.test.mjs +37 -0
- package/docs/prototypes/v3-full-robot.mjs +35 -0
- package/docs/prototypes/v3-full-robot.test.mjs +68 -0
- package/docs/prototypes/v3-kernel.mjs +147 -0
- package/docs/prototypes/v3-kernel.test.mjs +144 -0
- package/docs/prototypes/v3-layout.test.mjs +281 -0
- package/docs/prototypes/v3-linear-comparison.py +183 -0
- package/docs/prototypes/v3-more-samples.test.mjs +182 -0
- package/docs/prototypes/v3-payload-collision.mjs +54 -0
- package/docs/prototypes/v3-payload-collision.test.mjs +68 -0
- package/docs/prototypes/v3-payload-fit.mjs +58 -0
- package/docs/prototypes/v3-payload-fit.test.mjs +59 -0
- package/docs/prototypes/v3-repeated-binding.mjs +13 -0
- package/docs/prototypes/v3-repeated-binding.test.mjs +46 -0
- package/docs/prototypes/v3-runtime-browser.mjs +76 -0
- package/docs/prototypes/v3-samples.test.mjs +144 -0
- package/docs/prototypes/v3-semantics.test.mjs +55 -0
- package/docs/prototypes/v3-spatial-plan.test.mjs +102 -0
- package/docs/prototypes/v3-typed-plan.mjs +3 -0
- package/docs/prototypes/v3-typed-plan.test.mjs +70 -0
- package/docs/prototypes/v3-typed-repeat.test.mjs +96 -0
- package/docs/v3-asset-persistence.md +66 -0
- package/docs/v3-authoring-transactions.md +57 -0
- package/docs/v3-capability-bindings.md +109 -0
- package/docs/v3-common-kernel-experiment.md +116 -0
- package/docs/v3-core-semantics.md +303 -0
- package/docs/v3-cutover.md +27 -0
- package/docs/v3-design.md +388 -0
- package/docs/v3-document-contract.md +55 -0
- package/docs/v3-dryroom-profile-validation.md +87 -0
- package/docs/v3-editor-integration.md +59 -0
- package/docs/v3-full-conveyor-validation.md +69 -0
- package/docs/v3-full-dryroom-validation.md +55 -0
- package/docs/v3-full-forklift-validation.md +41 -0
- package/docs/v3-full-robot-validation.md +64 -0
- package/docs/v3-layout-system.md +318 -0
- package/docs/v3-minimal-model.md +255 -0
- package/docs/v3-operator-contracts.md +148 -0
- package/docs/v3-payload-collision-validation.md +42 -0
- package/docs/v3-payload-contact-validation.md +56 -0
- package/docs/v3-repeated-binding-validation.md +42 -0
- package/docs/v3-research-review.md +160 -0
- package/docs/v3-runtime-extraction.md +107 -0
- package/docs/v3-sample-validation-expanded.md +106 -0
- package/docs/v3-sample-validation.md +97 -0
- package/docs/v3-shapeassembly-reproduction.md +99 -0
- package/docs/v3-spatial-contracts.md +84 -0
- package/docs/v3-status.md +41 -0
- package/docs/v3-storage-integration.md +58 -0
- package/docs/v3-typed-repeat-contracts.md +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Figure v3 — 조립 관계와 반응형 제약
|
|
2
|
+
|
|
3
|
+
> 상태: 아래 내용은 **폐기된 초기 초안**이다. 구현 계약으로 사용하지 않는다.
|
|
4
|
+
> [재설계 상세안](v3-design.md)이 이 문서를 대체한다. 특히 병렬 `fit/relative/face` 제약,
|
|
5
|
+
> 관계 없는 초안의 저장 금지, 2배 검사 통과를 안전성으로 간주하는 조건은 철회한다.
|
|
6
|
+
|
|
7
|
+
## 결정 요약
|
|
8
|
+
|
|
9
|
+
Figure v2는 독립 부품의 기하·재질·축별 크기 반응을 잘 표현한다. 그러나 부품들이 **어떤
|
|
10
|
+
조립 의도로 붙어 있는지**는 표현하지 못한다. `anchor`는 항상 기준 상자 면을 기준으로 하고,
|
|
11
|
+
`parent`는 관절 운동 계층일 뿐 크기·배치 종속성이 아니다. 따라서 게이트는 갈라짐을 측정할 수
|
|
12
|
+
있어도 portal·gate·lamp 중 무엇을 어떤 관계로 복구해야 하는지는 알 수 없다.
|
|
13
|
+
|
|
14
|
+
v3는 `anchor`가 기준 상자에만 매달리는 한계를 교정하고, `assemblies`를 정본 계약으로
|
|
15
|
+
도입한다. 조립체는 기준 부품, 멤버, 그리고 검증·수정 가능한 관계 제약을 선언한다. v2의
|
|
16
|
+
독립 부품 해석은 과거 데이터 읽기용 이행 경로일 뿐, 새 모델의 품질 기준은 아니다.
|
|
17
|
+
|
|
18
|
+
## 사용자 약속
|
|
19
|
+
|
|
20
|
+
저작자는 `anchor.max`를 조합해서 장비를 맞추지 않는다. 모델러는 다음처럼 의도를 입력한다.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
portal-in 에 붙이기
|
|
24
|
+
gate-in-left, gate-in-right: 개구부 좌/우 면에 맞춤
|
|
25
|
+
lamp-in: portal 상단에서 60 mm 위
|
|
26
|
+
readout: portal 전면의 상대 좌표 (0.5, 0.82)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
그 뒤 기준 부품의 크기·비율·위치가 바뀌면 종속 부품이 함께 계산된다. AI도 같은 제약을
|
|
30
|
+
읽고 후보를 만들며, 발행 게이트는 깨진 제약을 보여 주고 안전한 복합 수정을 초안에 적용한다.
|
|
31
|
+
|
|
32
|
+
## 데이터 계약
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
interface FigureAssembly {
|
|
36
|
+
name: string // 저장 식별자, 예: 'portal-in-system'
|
|
37
|
+
root: string // parts[].name — 조립체 기준 부품
|
|
38
|
+
members: string[] // root를 제외한 종속 부품, 중복 금지
|
|
39
|
+
constraints: AssemblyConstraint[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type AssemblyConstraint =
|
|
43
|
+
| { kind: 'face'; subject: string; subjectFace: Face; target: string; targetFace: Face; offset?: number }
|
|
44
|
+
| { kind: 'relative'; subject: string; target: string; position: Partial<Vec3>; size?: Partial<Vec3> }
|
|
45
|
+
| { kind: 'fit'; subject: string; target: string; axes: Axis[]; inset?: Partial<Vec3> }
|
|
46
|
+
| { kind: 'ratio'; subject: string; target: string; axis: Axis; targetAxis: Axis; ratio: number }
|
|
47
|
+
|
|
48
|
+
type Face = 'min-x' | 'max-x' | 'min-y' | 'max-y' | 'min-z' | 'max-z'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 의미
|
|
52
|
+
|
|
53
|
+
- `face`: 두 면의 간격을 고정한다. lamp 하단 ↔ portal 상단 `offset: 60`이 이 경우다.
|
|
54
|
+
- `relative`: 기준 부품의 국소 상자 안 상대 위치·크기를 유지한다. HMI와 라벨에 쓴다.
|
|
55
|
+
- `fit`: gate처럼 목표 부품의 개구부/외곽을 여러 축에서 채운다. 절대 `span`의 조립체 버전이다.
|
|
56
|
+
- `ratio`: 한 축이 다른 부품 축의 비율을 따른다. 현재 `aspect-y`가 기준 상자 축만 보던 한계를
|
|
57
|
+
보완한다.
|
|
58
|
+
|
|
59
|
+
제약은 **계산 가능한 기하 계약**이다. 설명 문자열이나 AI의 추론 결과를 저장하지 않는다.
|
|
60
|
+
|
|
61
|
+
## 해석 순서
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
FigureSource v3
|
|
65
|
+
→ 부품의 기본 transform·sizing 계산
|
|
66
|
+
→ assembly root의 최종 상자 계산
|
|
67
|
+
→ assembly constraint를 위상 순서로 적용
|
|
68
|
+
→ 최종 blueprint / scene transform
|
|
69
|
+
→ 제약 검사 + 1×·2× 크기 반응 검사
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
제약 대상은 같은 조립체 안에서만 참조할 수 있다. 조립체끼리 순환 참조하거나, root가 자기
|
|
73
|
+
후손을 참조하는 것은 검증 오류다. 관절의 `parent` 계층은 운동 변환을 담당하고, 조립 제약은
|
|
74
|
+
rest pose와 인스턴스 크기 반응을 담당한다. 두 역할을 섞지 않는다.
|
|
75
|
+
|
|
76
|
+
## 검증과 자동 수정
|
|
77
|
+
|
|
78
|
+
발행 게이트는 기존의 덩어리 검사에 더해 다음을 낸다.
|
|
79
|
+
|
|
80
|
+
| 코드 | 의미 | 처리 |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `assembly-target-missing` | root/member/target 이름이 없음 | 저장 차단 |
|
|
83
|
+
| `assembly-cycle` | 관계 계산 순환 | 저장 차단 |
|
|
84
|
+
| `assembly-constraint-broken` | 기준 크기에서 면·상대치수 불일치 | 발행 차단 |
|
|
85
|
+
| `assembly-scales-apart` | 확대 시 선언한 관계가 깨짐 | 발행 차단 |
|
|
86
|
+
| `assembly-ambiguous` | 이행된 부품이 물리적으로 닿지만 관계가 선언되지 않음 | 전환 검토 |
|
|
87
|
+
|
|
88
|
+
자동 수정은 anchor 후보를 무작위 조합하지 않는다.
|
|
89
|
+
|
|
90
|
+
1. 깨진 제약과 root를 찾는다.
|
|
91
|
+
2. 제약 해석기로 멤버의 transform/반응 규칙을 다시 계산한다.
|
|
92
|
+
3. 1×와 일곱 가지 2× 배율을 모두 재검사한다.
|
|
93
|
+
4. 모든 차단 항목이 사라질 때만 `recommendedRepair`를 만든다.
|
|
94
|
+
5. 모델러의 **권장 수정 적용**은 초안에만 반영한다. 저장·발행은 별도다.
|
|
95
|
+
|
|
96
|
+
## 드라이룸 예시
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"name": "portal-in-system",
|
|
101
|
+
"root": "portal-in",
|
|
102
|
+
"members": ["gate-in-left", "gate-in-right", "lamp-in", "lamp-in-glow", "readout"],
|
|
103
|
+
"constraints": [
|
|
104
|
+
{ "kind": "fit", "subject": "gate-in-left", "target": "portal-in", "axes": ["x", "y"], "inset": { "x": 20 } },
|
|
105
|
+
{ "kind": "fit", "subject": "gate-in-right", "target": "portal-in", "axes": ["x", "y"], "inset": { "x": 20 } },
|
|
106
|
+
{ "kind": "face", "subject": "lamp-in", "subjectFace": "min-y", "target": "portal-in", "targetFace": "max-y", "offset": 60 },
|
|
107
|
+
{ "kind": "relative", "subject": "readout", "target": "portal-in", "position": { "x": 0.5, "y": 0.82, "z": 1 } }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
이 계약은 “lamp의 `anchor.y`를 max로 바꿔 본다”가 아니라 “lamp가 portal 상단에서 60mm를
|
|
113
|
+
유지한다”를 보존한다. 따라서 기준 상자의 비율 변화에도 수정을 설명하고 검증할 수 있다.
|
|
114
|
+
|
|
115
|
+
## 이행 원칙
|
|
116
|
+
|
|
117
|
+
1. `version: 3`은 새 저작·AI·발행의 기준 계약이다. 새 모델은 기준 상자 anchor만으로
|
|
118
|
+
조립 의도를 표현하지 않는다.
|
|
119
|
+
2. v2는 읽을 수 있어야 하지만, v2 해석을 영구 표준으로 보존하지 않는다. 전환기는 물리적
|
|
120
|
+
근접·이름·기존 anchor를 근거로 조립 관계 후보를 만들고, 사용자가 의도를 확인한다.
|
|
121
|
+
3. 확인된 v3 전환 결과가 기존 동작보다 조립·확대 품질이 낮으면 전환을 거절한다. “깨지지
|
|
122
|
+
않음”보다 “의도를 더 정확히 보존함”이 기준이다.
|
|
123
|
+
4. 과거에 이미 발행된 판은 당시 원본과 해석기를 함께 보관해 재현성을 유지한다. 이는 v2
|
|
124
|
+
설계를 계속 발전시키겠다는 뜻이 아니라 과거 화면을 거짓으로 바꾸지 않기 위한 기록 보존이다.
|
|
125
|
+
5. 새 Figure와 새 AI 후보는 v3 제약이 없는 상태로 저장되지 않는다. 독립 부품뿐인 단순
|
|
126
|
+
Figure는 빈 조립체 목록을 명시한다.
|
|
127
|
+
|
|
128
|
+
## 완료 기준
|
|
129
|
+
|
|
130
|
+
- 드라이룸의 portal/gate/lamp/readout을 조립체로 선언할 수 있다.
|
|
131
|
+
- portal의 X·Y·Z 및 복합 배율 7종에서 선언한 면·상대치수가 유지된다.
|
|
132
|
+
- 의도적으로 독립인 port/slot은 조립체에 넣지 않아도 거짓 차단되지 않는다.
|
|
133
|
+
- AI가 새 Figure와 편집 후보 모두에서 유효한 assembly 제약을 생성한다.
|
|
134
|
+
- 게이트는 깨진 조립 제약에 대해 검증된 복합 초안 수정안을 제시한다.
|
|
135
|
+
- v2 전환 표본은 새 관계 계약 아래에서 기존보다 낮은 조립·확대 품질을 내지 않는다.
|
|
136
|
+
|
|
137
|
+
## 명시적 비목표
|
|
138
|
+
|
|
139
|
+
- 물리 시뮬레이션·충돌 해석을 Figure 모델에 넣지 않는다.
|
|
140
|
+
- 모든 근접 부품을 자동으로 조립체로 확정하지 않는다. 근접은 증거일 뿐 의도가 아니다.
|
|
141
|
+
- AI가 관계를 추측한 결과를 사용자 확인 없이 정본에 저장하지 않는다.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# v3 레이아웃 설계 검증 예제
|
|
2
|
+
|
|
3
|
+
`node --test docs/prototypes/v3-typed-repeat.test.mjs`: 단위/좌표계 그래프와 반복 조립체 통합.
|
|
4
|
+
[배치 의도 변경과 검증 한계](../v3-typed-repeat-contracts.md)를 함께 확인한다.
|
|
5
|
+
|
|
6
|
+
`node --test docs/prototypes/v3-typed-plan.test.mjs`: 단위가 있는 scalar 컴파일러.
|
|
7
|
+
[연산·형상 계약과 미지원 범위](../v3-operator-contracts.md)를 참조한다.
|
|
8
|
+
|
|
9
|
+
현재 설계 방향: [r3 핵심 의미론](../v3-core-semantics.md).
|
|
10
|
+
`node --test docs/prototypes/v3-semantics.test.mjs`는 부착식과 반복식을 독립 검사한다.
|
|
11
|
+
기존 커널을 r3에 맞게 구현했다는 의미가 아니다.
|
|
12
|
+
|
|
13
|
+
## 공통 커널 실험
|
|
14
|
+
|
|
15
|
+
`node --test docs/prototypes/v3-kernel.test.mjs`
|
|
16
|
+
|
|
17
|
+
모델별 분기 없는 실행기와 실제 샘플에서 유도한 선언을 분리했다.
|
|
18
|
+
범위·213개 크기/상태 조합·반례·미지원 항목은
|
|
19
|
+
[공통 커널 실험 기록](../v3-common-kernel-experiment.md)을 따른다.
|
|
20
|
+
제품 v3 구현이나 전체 샘플 변환 완료가 아니다.
|
|
21
|
+
|
|
22
|
+
## 외부 연구 구현 probe
|
|
23
|
+
|
|
24
|
+
`shapeassembly-probe.py`는 제품 코드가 아니라 외부 고정 revision의 interpreter 호출기다.
|
|
25
|
+
외부 소스/라이브러리를 이 저장소에 포함하지 않는다. 단독 실행에는 별도의 연구용 checkout과
|
|
26
|
+
가상환경이 필요하다. 입력·출력은 `shapeassembly-probe-results.json`, 해석·라이선스 경계·
|
|
27
|
+
재현 명령은 [실행 기록](../v3-shapeassembly-reproduction.md)을 따른다.
|
|
28
|
+
|
|
29
|
+
아래 내용은 그와 별개인 자체 r1 수치 예제다.
|
|
30
|
+
|
|
31
|
+
이 폴더는 제품 모듈에 포함되지 않는 실행 가능한 설계 예제다.
|
|
32
|
+
package.json, 의존성, 서버, 소비자 runtime을 변경하지 않는다.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node --test docs/prototypes/v3-layout.test.mjs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2026-09-20 실행 결과: 18개 테스트 통과.
|
|
39
|
+
|
|
40
|
+
## 확인한 것
|
|
41
|
+
|
|
42
|
+
- capped grow/shrink의 제한·가중 배분·공간 보존: seed 고정 800개 조합.
|
|
43
|
+
- 100개 문 폭에서 외곽 여백·중앙 간격·완전 개방 시 내측 끝점.
|
|
44
|
+
- 고정 크기, gap 초과, 단일/빈 목록, fr 최소/최대, auto 다중 span.
|
|
45
|
+
- 회전한 정렬 기준점의 일치와 전역 강체 변환에 대한 일관성.
|
|
46
|
+
- min/max 전환의 구간별 동작, 비율 합계와 gap의 overflow.
|
|
47
|
+
- 값 DAG의 순서 불변성, 순환/중복 writer/알 수 없는 참조 거절.
|
|
48
|
+
|
|
49
|
+
초기 문 이동량 W/2는 중앙 gap/2만큼 더 이동하는 오류였다.
|
|
50
|
+
열린 문 안쪽 끝이 정확히 개구부 끝에 놓이는 독립 기대값으로 오류를 검출했고,
|
|
51
|
+
정확한 이동량 (W-gap)/2로 문서와 예제를 함께 수정했다.
|
|
52
|
+
|
|
53
|
+
## 확인하지 않은 것
|
|
54
|
+
|
|
55
|
+
- production schema/compiler 또는 실제 3D 엔진의 실행.
|
|
56
|
+
- 모든 축·모든 shape·중첩 컨테이너 전체 조합의 올바름.
|
|
57
|
+
- 실제 드라이룸/롤 프레스 자산의 수정 결과.
|
|
58
|
+
- 전체 지원 치수 범위의 형식 증명, float 오차의 보수적 인증.
|
|
59
|
+
- 모든 회전의 geometry 포함, 면 접촉, 관절 충돌, 미적 품질.
|
|
60
|
+
- 실제 CPU/GPU/메모리 성능이나 사용자 작업 시간 개선.
|
|
61
|
+
|
|
62
|
+
autoTracks는 intrinsic 기여 단계만, gridFr는 fr 배분 단계만 검증한다.
|
|
63
|
+
완전한 Grid evaluator나 validator로 재사용해서는 안 된다.
|
|
64
|
+
evaluateValues도 최소 DAG 예제이며 제품 수준 schema·오류 보고·깊이 제한을 갖추지 않았다.
|
|
65
|
+
wall-clock test 실행 시간은 제품 레이아웃 성능으로 보고하지 않는다.
|
|
66
|
+
|
|
67
|
+
## 다음 검증 순서
|
|
68
|
+
|
|
69
|
+
1. 정본 schema와 실제 geometry provider의 intrinsic/feature 계약 고정.
|
|
70
|
+
2. 중첩 Measure/Allocate/Arrange 통합 evaluator 및 독립 기준 결과 비교.
|
|
71
|
+
3. v2 자산의 고정 revision fixture를 확보해 외형·관절·발행 계약 비교.
|
|
72
|
+
4. renderer에서 중복 scale/shear/장면 잔류 없이 전체 layout 원자 교체 검증.
|
|
73
|
+
5. UI/AI 동일 transaction의 preview·apply·undo·저장 E2E와 비용 실측.
|
|
@@ -0,0 +1,60 @@
|
|
|
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.
|
|
3
|
+
Output path must be explicitly supplied. Not a screenshot of the product renderer.
|
|
4
|
+
"""
|
|
5
|
+
import json,sys,math
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PIL import Image,ImageDraw,ImageFont
|
|
8
|
+
data=json.load(sys.stdin)
|
|
9
|
+
im=Image.new('RGB',(1440,1000),'#101923');draw=ImageDraw.Draw(im)
|
|
10
|
+
font=ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc',22)
|
|
11
|
+
small=ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc',17)
|
|
12
|
+
title=ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc',29)
|
|
13
|
+
draw.text((30,20),'DRY ROOM / actual seed portal mesh',font=title,fill='white')
|
|
14
|
+
draw.text((30,62),'Orthographic mesh render, not the application viewport. Original asset unchanged.',font=small,fill='#a9bdcc')
|
|
15
|
+
for col,panel in enumerate(data['panels']):
|
|
16
|
+
x0=25+col*720
|
|
17
|
+
draw.text((x0,108),'BEFORE | bottom -60 mm' if col==0 else 'AFTER | bottom 0 mm',font=font,fill='#ffd189' if col==0 else '#85dfc7')
|
|
18
|
+
for zoom in [False,True]:
|
|
19
|
+
y0=155 if not zoom else 715
|
|
20
|
+
w,h=675,520 if not zoom else 215
|
|
21
|
+
canvas=Image.new('RGB',(w,h),'#192733');d=ImageDraw.Draw(canvas)
|
|
22
|
+
scale=.151 if not zoom else .235
|
|
23
|
+
def project(v):
|
|
24
|
+
x,y,z=v;z-=3050
|
|
25
|
+
return (w/2+(x+.40*z)*scale, (h-50 if not zoom else h-70)-y*scale+.16*z*scale)
|
|
26
|
+
pixels=np.full((h,w,3),(25,39,51),dtype=np.uint8)
|
|
27
|
+
zbuffer=np.full((h,w),-np.inf)
|
|
28
|
+
for part in panel['parts']:
|
|
29
|
+
for tri in part['triangles']:
|
|
30
|
+
a,b,c=tri
|
|
31
|
+
u=[b[i]-a[i] for i in range(3)];v=[c[i]-a[i] for i in range(3)]
|
|
32
|
+
normal=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]
|
|
33
|
+
norm=math.sqrt(sum(x*x for x in normal))
|
|
34
|
+
illumination=.58+.42*abs(sum(x*y for x,y in zip(normal,[.3,.4,.866]))/norm)
|
|
35
|
+
base=(51,184,176) if part['id']=='placedPortal' else (245,185,60)
|
|
36
|
+
color=tuple(int(x*illumination) for x in base)
|
|
37
|
+
poly=[project(v) for v in tri]
|
|
38
|
+
(ax,ay),(bx,by),(cx,cy)=poly
|
|
39
|
+
den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
|
|
40
|
+
if abs(den)<1e-9:continue
|
|
41
|
+
loX=max(0,math.floor(min(v[0] for v in poly)));hiX=min(w-1,math.ceil(max(v[0] for v in poly)))
|
|
42
|
+
loY=max(0,math.floor(min(v[1] for v in poly)));hiY=min(h-1,math.ceil(max(v[1] for v in poly)))
|
|
43
|
+
if loX>hiX or loY>hiY:continue
|
|
44
|
+
yy,xx=np.mgrid[loY:hiY+1,loX:hiX+1];xx=xx+.5;yy=yy+.5
|
|
45
|
+
wa=((by-cy)*(xx-cx)+(cx-bx)*(yy-cy))/den
|
|
46
|
+
wb=((cy-ay)*(xx-cx)+(ax-cx)*(yy-cy))/den;wc=1-wa-wb
|
|
47
|
+
depths=[v[2]-.40*v[0]+.16*v[1] for v in tri]
|
|
48
|
+
zz=wa*depths[0]+wb*depths[1]+wc*depths[2]
|
|
49
|
+
region=zbuffer[loY:hiY+1,loX:hiX+1]
|
|
50
|
+
mask=(wa>=-1e-8)&(wb>=-1e-8)&(wc>=-1e-8)&(zz>region)
|
|
51
|
+
region[mask]=zz[mask];pixels[loY:hiY+1,loX:hiX+1][mask]=color
|
|
52
|
+
canvas=Image.fromarray(pixels);d=ImageDraw.Draw(canvas)
|
|
53
|
+
# Reference line on front face plane; no opaque floor hiding the original protrusion.
|
|
54
|
+
a=project([-2000,0,3200]);b=project([2000,0,3200]);d.line([a,b],fill='#ff6b73',width=3)
|
|
55
|
+
d.text((12,h-28),'Y = 0 / floor reference',font=small,fill='#ff9ba1')
|
|
56
|
+
if zoom:d.text((12,10),'BOTTOM DETAIL',font=small,fill='white')
|
|
57
|
+
im.paste(canvas,(x0,y0))
|
|
58
|
+
draw.text((x0,682),'Top 2900 mm / opening top 2200 mm / lamp unchanged',font=small,fill='#a9bdcc')
|
|
59
|
+
draw.text((30,954),'Mesh checks: closed, oriented edges; 28 triangles per portal. Both inlet and outlet tested.',font=small,fill='#a9bdcc')
|
|
60
|
+
im.save(sys.argv[1])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Isolated browser experiment; never connects to the product server or edits assets.
|
|
2
|
+
import {createServer} from 'node:http'
|
|
3
|
+
import {readFile} from 'node:fs/promises'
|
|
4
|
+
import {chromium} from '/Users/super/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs'
|
|
5
|
+
import {seed,defaults,evaluateFull,geometryOf,sourceGeometry} from './v3-full-dryroom.mjs'
|
|
6
|
+
import * as conveyor from './v3-full-conveyor.mjs'
|
|
7
|
+
import * as robot from './v3-full-robot.mjs'
|
|
8
|
+
import * as forklift from './v3-full-forklift.mjs'
|
|
9
|
+
import {forkliftBindings} from './v3-capability-samples.mjs'
|
|
10
|
+
import {compilePayloadInspection} from './v3-payload-fit.mjs'
|
|
11
|
+
import {compileCollisionInspection} from './v3-payload-collision.mjs'
|
|
12
|
+
const isConveyor=process.argv.includes('--conveyor')
|
|
13
|
+
const isRobot=process.argv.includes('--robot')
|
|
14
|
+
const isCollision=process.argv.includes('--collision')
|
|
15
|
+
const isPayload=process.argv.includes('--payload')||isCollision
|
|
16
|
+
const isForklift=process.argv.includes('--forklift')||isPayload
|
|
17
|
+
const pack=(items)=>items.map(x=>({id:x.id,material:x.material,geometry:x.geometry.toJSON()}))
|
|
18
|
+
const fromV3=args=>pack(evaluateFull(args).map(x=>({...x,geometry:geometryOf(x)})))
|
|
19
|
+
const robotPose={...robot.defaults,'base-pan':30,'shoulder-lift':-35,'elbow-flex':65,'wrist-pitch':-25,'wrist-yaw':40,'tool-roll':20,gripper:50}
|
|
20
|
+
function payloadVariants(){
|
|
21
|
+
const sample=forkliftBindings(),p=(isCollision?compileCollisionInspection:compilePayloadInspection)(sample.model,sample.declaration)
|
|
22
|
+
const scenarios=isCollision?[['Clear placement',[260,50,75],620,100,700],['Upper forks penetrated',[260,250,75],620,500,700],['Floating',[260,200,75],620,100,700],['Curved body: unresolved',[260,343,-1485],50,50,50]]:[['Contact',[260,50,0],620],['Floating 150mm',[260,200,0],620],['Penetrates 20mm',[260,30,0],620],['Between forks: no contact',[260,50,0],100]]
|
|
23
|
+
return scenarios.map(([title,center,width,height=100,depth=850])=>{
|
|
24
|
+
const q={bindingId:'load-low',type:'PALLET',occupied:0,dimensions:[width,height,depth],center,yawDeg:0},r=p.evaluate(forklift.defaults,q)
|
|
25
|
+
const b=r.evaluation.bindings[0],t=b.pose.r.map((row,j)=>b.pose.t[j]+row.reduce((v,x,i)=>v+x*center[i],0))
|
|
26
|
+
const placed={shape:{provider:'box',dimensions:{width,height,depth}},pose:{r:b.pose.r,t}}
|
|
27
|
+
return {title:title+' · contact '+r.inspection.contact.status+(isCollision?' / collision '+r.inspection.collision.status:''),parts:pack([...forklift.evaluateFull().map(x=>({...x,material:isCollision&&r.inspection.collision.pairs.some(p=>p.geometryId===x.id&&p.status==='failed')?{token:'palette.status-danger'}:x.material,geometry:forklift.geometryOf(x)})),{id:'payload',material:{token:'palette.accent'},geometry:forklift.geometryOf(placed)}])}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
const variants=isPayload?payloadVariants():isForklift?[
|
|
31
|
+
{title:'Source seed · rest pose',parts:pack(forklift.seed.parts.map(p=>({id:p.name,material:p.material,geometry:forklift.sourceGeometry(p)})))},
|
|
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}]
|
|
36
|
+
].map(([title,args])=>({title,parts:pack(forklift.evaluateFull(args).map(x=>({...x,geometry:forklift.geometryOf(x)})))}))
|
|
37
|
+
]:isRobot?[
|
|
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
|
+
...[
|
|
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}]
|
|
43
|
+
].map(([title,args])=>({title,parts:pack(robot.evaluateFull(args).map(x=>({...x,geometry:robot.geometryOf(x)})))}))
|
|
44
|
+
]:isConveyor?[
|
|
45
|
+
{title:'Source seed · overlapping end rollers',parts:pack(conveyor.sourceItems())},
|
|
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}]
|
|
50
|
+
].map(([title,args])=>({title,parts:pack(conveyor.evaluateFull(args).map(x=>({...x,geometry:conveyor.geometryOf(x)})))}))
|
|
51
|
+
]:[
|
|
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:fromV3(defaults)},
|
|
54
|
+
{title:'V3 · portal bottom trimmed to floor',parts:fromV3({...defaults,bottom:0})},
|
|
55
|
+
{title:'V3 · resized + lamps raised',parts:fromV3({...defaults,width:4800,height:3600,depth:7200,bottom:0,lampLift:100})}
|
|
56
|
+
]
|
|
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
|
+
import * as T from '/three.module.js';
|
|
59
|
+
const variants=${JSON.stringify(variants)};
|
|
60
|
+
const palette={'palette.primary':'#154473','palette.accent':'#f79c40','palette.neutral':'#96989b','palette.dark':'#27292c','palette.light':'#d6d7d9','palette.surface':'#56585b','palette.status-danger':'#f61b30','palette.status-warning':'#e68d1a','palette.status-success':'#1cb648','palette.status-info':'#158bde'};
|
|
61
|
+
window.results=[];window.pixels=[];
|
|
62
|
+
for(const v of variants){
|
|
63
|
+
const section=document.createElement('section');section.innerHTML='<h3>'+v.title+'</h3>';document.querySelector('main').append(section);
|
|
64
|
+
const renderer=new T.WebGLRenderer({antialias:true,preserveDrawingBuffer:true});renderer.setSize(640,420);renderer.setClearColor('#f7f9fb');section.append(renderer.domElement);
|
|
65
|
+
const scene=new T.Scene();scene.add(new T.HemisphereLight(0xffffff,0x687381,2));const light=new T.DirectionalLight(0xffffff,3);light.position.set(5000,9000,9000);scene.add(light);
|
|
66
|
+
// Metal needs reflected illumination; a hemisphere light alone leaves it nearly black.
|
|
67
|
+
const envCanvas=document.createElement('canvas');envCanvas.width=512;envCanvas.height=256;const ctx=envCanvas.getContext('2d');const grad=ctx.createLinearGradient(0,0,0,256);grad.addColorStop(0,'#ffffff');grad.addColorStop(.5,'#b8c4d4');grad.addColorStop(1,'#4e5c6b');ctx.fillStyle=grad;ctx.fillRect(0,0,512,256);ctx.fillStyle='#ffffff';ctx.fillRect(70,30,90,80);const envTex=new T.CanvasTexture(envCanvas);envTex.colorSpace=T.SRGBColorSpace;const pmrem=new T.PMREMGenerator(renderer);scene.environment=pmrem.fromEquirectangular(envTex).texture;
|
|
68
|
+
for(const p of v.parts){const m=p.material,e=m.emissive;const color=palette[e?.on?e.token:m.token]??'#96989b';const mat=new T.MeshStandardMaterial({color,metalness:m.preset==='metal'?1:0,roughness:m.preset==='rubber'?.9:m.preset==='metal'?.3:.4,flatShading:true,transparent:!!m.transparent,emissive:e?.on?color:0,emissiveIntensity:e?.on?e.intensity:0});scene.add(new T.Mesh(new T.BufferGeometryLoader().parse(p.geometry),mat));}
|
|
69
|
+
const camera=new T.PerspectiveCamera(38,640/420,1,50000);camera.position.set(...${JSON.stringify(isForklift?[5200,4200,6200]:isRobot?[2300,1900,2900]:isConveyor?[3000,2400,3300]:[10000,8000,13500])});camera.lookAt(0,${isForklift?1250:isRobot?750:isConveyor?350:1300},0);renderer.render(scene,camera);
|
|
70
|
+
const gl=renderer.getContext(),px=new Uint8Array(640*420*4);gl.readPixels(0,0,640,420,gl.RGBA,gl.UNSIGNED_BYTE,px);window.pixels.push(px);
|
|
71
|
+
window.results.push({title:v.title,calls:renderer.info.render.calls,triangles:renderer.info.render.triangles});
|
|
72
|
+
section.insertAdjacentHTML('beforeend','<p>'+renderer.info.render.calls+' draw calls / '+renderer.info.render.triangles+' triangles · identical camera</p>');
|
|
73
|
+
}
|
|
74
|
+
let different=0,max=0;for(let i=0;i<pixels[0].length;i++){const d=Math.abs(pixels[0][i]-pixels[1][i]);if(d)different++;max=Math.max(max,d)}window.comparison={differentChannels:different,maxChannelDifference:max};window.ready=true;
|
|
75
|
+
</script>`
|
|
76
|
+
const server=createServer(async(req,res)=>{try{if(req.url==='/'){res.setHeader('Content-Type','text/html');res.end(html)}else if(['/three.module.js','/three.core.js'].includes(req.url)){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('../../../things-scene/node_modules/three/build'+req.url,import.meta.url)))}else{res.statusCode=404;res.end()}}catch(e){res.statusCode=500;res.end(String(e))}})
|
|
77
|
+
await new Promise(r=>server.listen(0,'127.0.0.1',r));let browser
|
|
78
|
+
try{
|
|
79
|
+
browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
|
|
80
|
+
const page=await browser.newPage({viewport:{width:1328,height:990},deviceScaleFactor:1});const errors=[];page.on('pageerror',e=>errors.push(String(e)));
|
|
81
|
+
await page.goto('http://127.0.0.1:'+server.address().port);await page.waitForFunction(()=>window.ready);
|
|
82
|
+
const screenshot='/private/tmp/figure-v3-full-'+(isCollision?'collision':isPayload?'payload':isForklift?'forklift':isRobot?'robot':isConveyor?'conveyor':'dryroom')+'.png';
|
|
83
|
+
await page.screenshot({path:screenshot,fullPage:true});
|
|
84
|
+
console.log(JSON.stringify({...await page.evaluate(()=>({results:window.results,comparison:window.comparison})),errors,screenshot},null,2));
|
|
85
|
+
if(errors.length)process.exitCode=1;
|
|
86
|
+
}finally{await browser?.close();server.close()}
|