three-slicer 0.1.0 → 0.1.1
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 +342 -24
- package/components/README.md +1 -1
- package/components/dist/SettingsPanel.js +3 -4
- package/engine/index.d.ts +3 -1
- package/engine/src/data.js +12 -0
- package/engine/src/settings.js +1 -1
- package/engine/src/slicer.worker.js +2 -1
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/toggle_eval.js +1 -2
- package/package.json +54 -26
- package/types/components.d.ts +19 -0
- package/types/data/config-schema.json.d.ts +55 -0
- package/types/data/invalidation-map.json.d.ts +15 -0
- package/types/data/toggle-rules.json.d.ts +20 -0
- package/types/data/ui-tree.json.d.ts +19 -0
- package/types/data.d.ts +17 -0
- package/types/gen_settings_types.mjs +70 -0
- package/types/settings-keys.d.ts +1704 -0
- package/types/settings.d.ts +19 -0
- package/types/toggle.d.ts +22 -0
- package/types/viewer-loaders.d.ts +33 -0
- package/types/viewer-toolpath.d.ts +105 -0
- package/types/viewer.d.ts +15 -0
- package/types/wasm.d.ts +5 -0
- package/types/worker.d.ts +3 -0
- package/viewer/README.md +1 -1
- package/viewer/dist/Viewport.js +674 -617
- package/viewer/dist/model_loaders.js +220 -108
- package/viewer/styles.css +2 -0
- package/data/config-schema-builddump.json +0 -12921
- package/viewer/src/Viewport.jsx +0 -1353
- package/viewer/src/assets/add.svg +0 -1
- package/viewer/src/assets/arrange.svg +0 -1
- package/viewer/src/assets/boolean.svg +0 -1
- package/viewer/src/assets/cut.svg +0 -1
- package/viewer/src/assets/delete.svg +0 -1
- package/viewer/src/assets/deleteall.svg +0 -1
- package/viewer/src/assets/duplicate.svg +0 -1
- package/viewer/src/assets/measure.svg +0 -1
- package/viewer/src/assets/mmu.svg +0 -1
- package/viewer/src/assets/move.svg +0 -1
- package/viewer/src/assets/negative.svg +0 -1
- package/viewer/src/assets/onbed.svg +0 -1
- package/viewer/src/assets/open.svg +0 -1
- package/viewer/src/assets/orient.svg +0 -1
- package/viewer/src/assets/paint.svg +0 -1
- package/viewer/src/assets/rotate.svg +0 -1
- package/viewer/src/assets/scale.svg +0 -1
- package/viewer/src/assets/seam.svg +0 -1
- package/viewer/src/assets/split.svg +0 -1
- package/viewer/src/assets/textmark.svg +0 -1
- package/viewer/src/assets/varlayer.svg +0 -1
- package/viewer/src/icons.js +0 -23
- package/viewer/src/make_worker.js +0 -9
- package/viewer/src/model_loaders.js +0 -178
- package/viewer/src/shadow_host.jsx +0 -30
- package/viewer/src/toolpath_gpu.js +0 -430
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
// 24단계: 원본 libvgcode 툴패스 렌더러의 충실 포팅 (CPU 지오메트리 빌더 폐기 → GPU 인스턴싱).
|
|
2
|
-
// 원본: src/libvgcode/{SegmentTemplate.cpp, ShadersES.hpp(Segments_Vertex_Shader_ES), ViewerImpl.cpp(extract_pos_and_or_hwa)}.
|
|
3
|
-
// 구조: 8정점 다이아몬드 템플릿(24인덱스) × InstancedBufferGeometry, 세그먼트 데이터는 DataTexture(RGBA32F/32UI)
|
|
4
|
-
// + RawShaderMaterial(GLSL3). 정점 셰이더는 원본 ES 변형을 그대로 포팅(알고리즘 무변경).
|
|
5
|
-
// CPU 는 지오메트리를 만들지 않는다 — PathVertex 스트림(끝점+height/width/color) + 세그먼트 조인각 사전계산만.
|
|
6
|
-
|
|
7
|
-
// 원본 SegmentTemplate.cpp:18 VERTEX_DATA (8정점 다이아몬드의 24 삼각형 인덱스, 원본 그대로).
|
|
8
|
-
// /1-------6\ 단면=다이아몬드(0=상,3=하,2/7=앞뒤 스파이크,5/6/4/1=측/상하)
|
|
9
|
-
// 2--0-------5--7
|
|
10
|
-
// \3-------4/
|
|
11
|
-
export const VERTEX_DATA = [
|
|
12
|
-
0, 1, 2, 0, 2, 3, // front spike
|
|
13
|
-
0, 3, 4, 0, 4, 5, // right/bottom body
|
|
14
|
-
0, 5, 6, 0, 6, 1, // left/top body
|
|
15
|
-
5, 4, 7, 5, 7, 6, // back spike
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
// 툴패스 타입 색 (0=travel,1=wall,2=sparse,3=solid,4=skirt/brim,5=support,6=raft,7=gap,8=thin,9=bridge,10=iron,11=prime)
|
|
19
|
-
export const TYPE_COLOR = {
|
|
20
|
-
0: [0.42, 0.45, 0.50], 1: [0.85, 0.51, 0.17], 2: [0.21, 0.45, 0.76],
|
|
21
|
-
3: [0.35, 0.75, 0.85], 4: [0.16, 0.68, 0.40], 5: [0.66, 0.42, 0.85], 6: [0.55, 0.45, 0.35],
|
|
22
|
-
7: [0.95, 0.85, 0.25], 8: [0.90, 0.35, 0.65], 9: [0.90, 0.25, 0.25],
|
|
23
|
-
10: [0.60, 0.82, 0.55], 11: [0.30, 0.72, 0.70],
|
|
24
|
-
}
|
|
25
|
-
function packColor(c) { // [r,g,b] 0..1 → r<<16|g<<8|b (원본 decode_color 역연산, f32 로 정확 표현 <2^24)
|
|
26
|
-
const r = Math.round(c[0] * 255), g = Math.round(c[1] * 255), b = Math.round(c[2] * 255)
|
|
27
|
-
return (r << 16) | (g << 8) | b
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── CPU 데이터 준비 (순수 함수 — node 테스트 가능) ────────────────────────────
|
|
31
|
-
// 커널 layers[{z,paths(stride8),widths[]}] → PathVertex 스트림 + 세그먼트 인덱스.
|
|
32
|
-
// 원본 extract_pos_and_or_hwa 를 따름: position.z -= 0.5*height, angle = atan2(prev×this, prev·this).
|
|
33
|
-
// 연결된 압출 세그먼트(끝점 일치·동일 타입)는 정점을 공유 → 조인각이 계산되어 마이터 조인 형성.
|
|
34
|
-
// 트래블(type 0)은 인스턴싱 대상 아님 → 별도 라인 스트림.
|
|
35
|
-
export function buildSegmentData(layers, defaultLineWidth) {
|
|
36
|
-
const L = layers.length
|
|
37
|
-
const lw = defaultLineWidth > 0 ? defaultLineWidth : 0.42
|
|
38
|
-
// 레이어별 비드 높이 = z 증분(첫 레이어=z0, raft/first_layer 자동 반영)
|
|
39
|
-
const layerH = new Array(L)
|
|
40
|
-
for (let i = 0; i < L; i++) { const z = layers[i].z; layerH[i] = Math.max(0.02, i === 0 ? z : z - layers[i - 1].z) }
|
|
41
|
-
|
|
42
|
-
// PathVertex 스트림 (raw)
|
|
43
|
-
const vx = [], vy = [], vz = [], vh = [], vw = [], vtype = [], vlayer = []
|
|
44
|
-
const realNext = [] // realNext[i]=true → 세그먼트 (i,i+1) 이 실제 그려지는 압출 세그먼트
|
|
45
|
-
const segIdA = [], segLayer = []
|
|
46
|
-
const typeLengths = new Float64Array(16) // 25단계 S6.3: 타입별 총 압출 길이(역할 비율 범례용)
|
|
47
|
-
const travel = [], travelLayer = [] // 트래블: [x0,y0,z0,x1,y1,z1] per seg
|
|
48
|
-
let lastIdx = -1, lastX = 0, lastY = 0, lastZ = 0, lastType = -1, curLayer = 0
|
|
49
|
-
const EPS = 1e-4
|
|
50
|
-
const push = (x, y, z, t, h, w) => { vx.push(x); vy.push(y); vz.push(z); vtype.push(t); vh.push(h); vw.push(w); vlayer.push(curLayer); realNext.push(false); return vx.length - 1 }
|
|
51
|
-
|
|
52
|
-
for (let li = 0; li < L; li++) {
|
|
53
|
-
const paths = layers[li].paths, widths = layers[li].widths, h = layerH[li]
|
|
54
|
-
curLayer = li
|
|
55
|
-
if (!paths) continue
|
|
56
|
-
for (let k = 0; k < paths.length; k += 8) {
|
|
57
|
-
const type = paths[k + 3]
|
|
58
|
-
const x0 = paths[k], y0 = paths[k + 1], z0 = paths[k + 2], x1 = paths[k + 4], y1 = paths[k + 5], z1 = paths[k + 6]
|
|
59
|
-
if (type === 0) { travel.push(x0, y0, z0, x1, y1, z1); travelLayer.push(li); continue }
|
|
60
|
-
const w = (widths && widths[k / 8] > 0) ? widths[k / 8] : lw
|
|
61
|
-
if (type < 16) typeLengths[type] += Math.hypot(x1 - x0, y1 - y0) // 역할별 길이 누적
|
|
62
|
-
let idA
|
|
63
|
-
if (lastIdx >= 0 && lastType === type &&
|
|
64
|
-
Math.abs(lastX - x0) < EPS && Math.abs(lastY - y0) < EPS && Math.abs(lastZ - z0) < EPS) {
|
|
65
|
-
idA = lastIdx // 이전 세그먼트 끝점 재사용(연결) → 정점 공유
|
|
66
|
-
push(x1, y1, z1, type, h, w) // 끝점 B 를 idA+1 에 추가
|
|
67
|
-
} else {
|
|
68
|
-
idA = push(x0, y0, z0, type, h, w) // 새 런: 시작 A
|
|
69
|
-
push(x1, y1, z1, type, h, w) // 끝 B (= idA+1)
|
|
70
|
-
}
|
|
71
|
-
realNext[idA] = true
|
|
72
|
-
lastIdx = idA + 1; lastX = x1; lastY = y1; lastZ = z1; lastType = type
|
|
73
|
-
segIdA.push(idA); segLayer.push(li)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const nV = vx.length, nSeg = segIdA.length
|
|
78
|
-
// 텍스처 배열 (RGBA). 색은 별도 텍스처 대신 hwa.w 에 패킹 — 정점당 texelFetch 1회 절감 + 텍스처 1장 제거.
|
|
79
|
-
const position = new Float32Array(nV * 4)
|
|
80
|
-
const hwa = new Float32Array(nV * 4)
|
|
81
|
-
let maxAbs = 0, hasNaN = false
|
|
82
|
-
let bx0 = Infinity, by0 = Infinity, bz0 = Infinity, bx1 = -Infinity, by1 = -Infinity, bz1 = -Infinity // frustum culling용 bbox
|
|
83
|
-
for (let i = 0; i < nV; i++) {
|
|
84
|
-
const h = vh[i]
|
|
85
|
-
// 원본: position.z -= 0.5*height (다이아몬드 중심을 비드 중앙에 배치)
|
|
86
|
-
const px = vx[i], py = vy[i], pz = vz[i] - 0.5 * h
|
|
87
|
-
position[i * 4] = px; position[i * 4 + 1] = py; position[i * 4 + 2] = pz
|
|
88
|
-
// angle = atan2(prev×this, prev·this) — 원본 extract_pos_and_or_hwa
|
|
89
|
-
const prevValid = i > 0 && realNext[i - 1]
|
|
90
|
-
const thisValid = realNext[i]
|
|
91
|
-
let angle = 0
|
|
92
|
-
if (prevValid || thisValid) {
|
|
93
|
-
const pdx = prevValid ? vx[i] - vx[i - 1] : 0, pdy = prevValid ? vy[i] - vy[i - 1] : 0, pdz = prevValid ? vz[i] - vz[i - 1] : 0
|
|
94
|
-
const tdx = thisValid ? vx[i + 1] - vx[i] : 0, tdy = thisValid ? vy[i + 1] - vy[i] : 0, tdz = thisValid ? vz[i + 1] - vz[i] : 0
|
|
95
|
-
angle = Math.atan2(pdx * tdy - pdy * tdx, pdx * tdx + pdy * tdy + pdz * tdz)
|
|
96
|
-
}
|
|
97
|
-
hwa[i * 4] = h; hwa[i * 4 + 1] = vw[i]; hwa[i * 4 + 2] = angle
|
|
98
|
-
hwa[i * 4 + 3] = packColor(TYPE_COLOR[vtype[i]] || TYPE_COLOR[1]) // .w = packed color (f32 로 <2^24 정확)
|
|
99
|
-
if (!Number.isFinite(px) || !Number.isFinite(py) || !Number.isFinite(pz) || !Number.isFinite(angle)) hasNaN = true
|
|
100
|
-
maxAbs = Math.max(maxAbs, Math.abs(px), Math.abs(py), Math.abs(pz))
|
|
101
|
-
if (px < bx0) bx0 = px; if (px > bx1) bx1 = px
|
|
102
|
-
if (py < by0) by0 = py; if (py > by1) by1 = py
|
|
103
|
-
if (pz < bz0) bz0 = pz; if (pz > bz1) bz1 = pz
|
|
104
|
-
}
|
|
105
|
-
// 세그먼트 인덱스 (레이어 순서) + 레이어별 누적 프리픽스(가시범위 O(1))
|
|
106
|
-
// .r=id_a, .g=레이어(이중 슬라이더의 하한 컷을 셰이더가 O(1) 판정 → 텍스처 재업로드 불필요)
|
|
107
|
-
const segIndex = new Uint32Array(nSeg * 4)
|
|
108
|
-
for (let s = 0; s < nSeg; s++) { segIndex[s * 4] = segIdA[s]; segIndex[s * 4 + 1] = segLayer[s] }
|
|
109
|
-
// 뷰 타입 컬러링용 per-vertex 메타(값→색 재계산 전용, 순수)
|
|
110
|
-
const meta = { vType: new Uint8Array(nV), vWidth: new Float32Array(nV), vHeight: new Float32Array(nV), vLayer: new Int32Array(nV) }
|
|
111
|
-
for (let i = 0; i < nV; i++) { meta.vType[i] = vtype[i]; meta.vWidth[i] = vw[i]; meta.vHeight[i] = vh[i]; meta.vLayer[i] = vlayer[i] }
|
|
112
|
-
const layerSegPrefix = new Int32Array(L + 1) // prefix[n] = layer<n 세그먼트 수 (segLayer 는 오름차순)
|
|
113
|
-
{ let s = 0; for (let n = 0; n < L; n++) { while (s < nSeg && segLayer[s] === n) s++; layerSegPrefix[n + 1] = s } }
|
|
114
|
-
// 트래블 (레이어 순서) + 프리픽스
|
|
115
|
-
const nTrav = travelLayer.length
|
|
116
|
-
const travelPos = new Float32Array(nTrav * 6)
|
|
117
|
-
for (let i = 0; i < travelPos.length; i++) travelPos[i] = travel[i]
|
|
118
|
-
for (let i = 0; i < travelPos.length; i += 3) { // 트래블도 bbox 에 포함(같은 sphere 로 컬링)
|
|
119
|
-
const x = travelPos[i], y = travelPos[i + 1], z = travelPos[i + 2]
|
|
120
|
-
if (x < bx0) bx0 = x; if (x > bx1) bx1 = x
|
|
121
|
-
if (y < by0) by0 = y; if (y > by1) by1 = y
|
|
122
|
-
if (z < bz0) bz0 = z; if (z > bz1) bz1 = z
|
|
123
|
-
}
|
|
124
|
-
const travelPrefix = new Int32Array(L + 1)
|
|
125
|
-
{ let s = 0; for (let n = 0; n < L; n++) { while (s < nTrav && travelLayer[s] === n) s++; travelPrefix[n + 1] = s } }
|
|
126
|
-
|
|
127
|
-
const bbox = nV + nTrav > 0 ? { min: [bx0, by0, bz0], max: [bx1, by1, bz1] } : null
|
|
128
|
-
return { position, hwa, segIndex, nV, nSeg, layerSegPrefix, travelPos, travelPrefix, nTrav, layerCount: L, maxAbs, hasNaN, meta, typeLengths, bbox }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// S6.3: 역할별 비율 범례 데이터 — 타입별 길이 % (시간은 커널이 role별 미노출 → 길이 비율로 근사, 문서화).
|
|
132
|
-
export const TYPE_LABEL = { 1: '벽', 2: '스파스', 3: '솔리드', 4: '스커트', 5: '서포트', 6: '래프트', 7: '갭필', 8: '씬월', 9: '브리지', 10: '아이어닝', 11: '프라임' }
|
|
133
|
-
export function roleRatios(typeLengths) {
|
|
134
|
-
let total = 0; for (let t = 1; t < 16; t++) total += typeLengths[t] || 0
|
|
135
|
-
const out = []
|
|
136
|
-
if (total <= 0) return out
|
|
137
|
-
for (let t = 1; t < 16; t++) { const l = typeLengths[t] || 0; if (l > 0) out.push({ type: t, label: TYPE_LABEL[t] || ('t' + t), pct: 100 * l / total, color: TYPE_COLOR[t] || TYPE_COLOR[1] }) }
|
|
138
|
-
return out.sort((a, b) => b.pct - a.pct)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── 뷰 타입 컬러링 (원본 libvgcode ColorRange 방식) ────────────────────────────
|
|
142
|
-
// DEFAULT_RANGES_COLORS (src/libvgcode/include/ColorRange.hpp:14) — 파랑→빨강 11색 히트맵.
|
|
143
|
-
export const DEFAULT_RANGES_COLORS = [
|
|
144
|
-
[11, 44, 122], [19, 89, 133], [28, 136, 145], [4, 214, 15], [170, 242, 0], [252, 249, 3],
|
|
145
|
-
[245, 206, 10], [227, 136, 32], [209, 104, 48], [194, 82, 60], [148, 38, 22],
|
|
146
|
-
].map(c => [c[0] / 255, c[1] / 255, c[2] / 255])
|
|
147
|
-
// 원본 ColorRange::get_color_at (Linear): step=(hi-lo)/(N-1), t=(v-lo)/step, 인접 팔레트색 lerp.
|
|
148
|
-
function rangeColorAt(v, lo, hi, pal) {
|
|
149
|
-
const N = pal.length
|
|
150
|
-
if (!(hi > lo)) return pal[0]
|
|
151
|
-
const step = (hi - lo) / (N - 1)
|
|
152
|
-
const gt = (v - lo) / step
|
|
153
|
-
const li = Math.max(0, Math.min(N - 1, Math.floor(gt)))
|
|
154
|
-
const hiI = Math.max(0, Math.min(N - 1, li + 1))
|
|
155
|
-
const f = gt - li
|
|
156
|
-
const a = pal[li], b = pal[hiI]
|
|
157
|
-
return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f]
|
|
158
|
-
}
|
|
159
|
-
// 뷰 타입 목록 (데스크톱 EViewType 중 우선 6종). value(i)=정점 i 의 뷰값, cont=연속(히트맵)/false=고정색.
|
|
160
|
-
export const VIEW_TYPES = [
|
|
161
|
-
{ key: 'feature', label: 'Feature type', cont: false, unit: '' },
|
|
162
|
-
{ key: 'speed', label: 'Speed', cont: true, unit: 'mm/s' },
|
|
163
|
-
{ key: 'height', label: 'Layer Height', cont: true, unit: 'mm' },
|
|
164
|
-
{ key: 'width', label: 'Line Width', cont: true, unit: 'mm' },
|
|
165
|
-
{ key: 'fan', label: 'Fan Speed', cont: true, unit: '%' },
|
|
166
|
-
{ key: 'temp', label: 'Temperature', cont: true, unit: '°C' },
|
|
167
|
-
]
|
|
168
|
-
// per-vertex 뷰값 산출. speed/fan/temp 는 커널 toolpath 에 없어 설정값에서 유도(값 싼 쪽, 커널 무변경).
|
|
169
|
-
// ctx: { speedByType:{type:val}, firstLayerSpeed, fanByType or fanFirstLayers, tempNormal, tempFirst, closeFanLayers }
|
|
170
|
-
function viewValue(viewType, meta, i, ctx) {
|
|
171
|
-
const t = meta.vType[i], layer = meta.vLayer[i], first = layer === 0
|
|
172
|
-
switch (viewType) {
|
|
173
|
-
case 'height': return meta.vHeight[i]
|
|
174
|
-
case 'width': return meta.vWidth[i]
|
|
175
|
-
case 'speed': return first ? ctx.firstLayerSpeed : (ctx.speedByType[t] ?? ctx.speedByType[1])
|
|
176
|
-
case 'fan': return (layer < ctx.closeFanLayers) ? 0 : (t === 9 ? 100 : ctx.fanNormal) // bridge(9)=100%, 첫 N층=0
|
|
177
|
-
case 'temp': return first ? ctx.tempFirst : ctx.tempNormal
|
|
178
|
-
default: return 0
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// 뷰 타입 → per-vertex 색 Float32Array(nV*4, .r=packed) + 범위 min/max. feature 는 고정색.
|
|
182
|
-
export function computeColors(data, viewType, ctx) {
|
|
183
|
-
const { meta, nV } = data
|
|
184
|
-
const color = new Float32Array(nV * 4)
|
|
185
|
-
const vt = VIEW_TYPES.find(v => v.key === viewType) || VIEW_TYPES[0]
|
|
186
|
-
if (!vt.cont) { // Feature type: 타입 고정색
|
|
187
|
-
for (let i = 0; i < nV; i++) color[i * 4] = packColor(TYPE_COLOR[meta.vType[i]] || TYPE_COLOR[1])
|
|
188
|
-
return { color, min: 0, max: 0, viewType, label: vt.label, unit: vt.unit, cont: false }
|
|
189
|
-
}
|
|
190
|
-
// 연속 뷰: 값 범위(압출 정점만) → 히트맵
|
|
191
|
-
let lo = Infinity, hi = -Infinity
|
|
192
|
-
const vals = new Float32Array(nV)
|
|
193
|
-
for (let i = 0; i < nV; i++) { const v = viewValue(viewType, meta, i, ctx); vals[i] = v; if (v < lo) lo = v; if (v > hi) hi = v }
|
|
194
|
-
if (!Number.isFinite(lo)) { lo = 0; hi = 1 }
|
|
195
|
-
for (let i = 0; i < nV; i++) { const c = rangeColorAt(vals[i], lo, hi, DEFAULT_RANGES_COLORS); color[i * 4] = packColor(c) }
|
|
196
|
-
return { color, min: lo, max: hi, viewType, label: vt.label, unit: vt.unit, cont: true }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ── GLSL ES 3.0 셰이더 (원본 Segments_Vertex_Shader_ES 충실 포팅) ─────────────
|
|
200
|
-
// 변경점(포팅 필수, 알고리즘 무변경): #version 은 three(GLSL3)가 부여 / precision 블록 명시 /
|
|
201
|
-
// float 리터럴 접미사 'f' 제거(255.0f→255.0, WebGL2 ANGLE 호환) / vertex_id 는 float attribute.
|
|
202
|
-
// texelFetch 는 원본 ES 변형 그대로 sampler2D + tex_coord(id→(u,v)) — samplerBuffer 부재 대응(원본 의미 보존).
|
|
203
|
-
const SEG_VS = `
|
|
204
|
-
precision highp float;
|
|
205
|
-
precision highp int;
|
|
206
|
-
precision highp sampler2D;
|
|
207
|
-
precision highp usampler2D;
|
|
208
|
-
#define POINTY_CAPS
|
|
209
|
-
#define FIX_TWISTING
|
|
210
|
-
const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);
|
|
211
|
-
const float light_top_diffuse = 0.6 * 0.8;
|
|
212
|
-
const float light_top_specular = 0.6 * 0.125;
|
|
213
|
-
const float light_top_shininess = 20.0;
|
|
214
|
-
const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);
|
|
215
|
-
const float light_front_diffuse = 0.6 * 0.3;
|
|
216
|
-
const float ambient = 0.3;
|
|
217
|
-
const float emission = 0.15;
|
|
218
|
-
const vec3 UP = vec3(0, 0, 1);
|
|
219
|
-
uniform mat4 view_matrix;
|
|
220
|
-
uniform mat4 projection_matrix;
|
|
221
|
-
uniform vec3 camera_position;
|
|
222
|
-
uniform sampler2D position_tex;
|
|
223
|
-
uniform sampler2D height_width_angle_tex;
|
|
224
|
-
uniform int layer_lo; // 25단계: 이중 슬라이더 하한(레이어). 범위 밖 세그먼트는 셰이더가 O(1) 클립.
|
|
225
|
-
uniform int layer_hi; // 상한은 instanceCount 로 컷(레이어 순 정렬).
|
|
226
|
-
in float vertex_id_float;
|
|
227
|
-
in uint seg_id_a_u; // 인스턴스 어트리뷰트(구 segment_index_tex.r) — 어트리뷰트 fetch 가 texelFetch 보다 쌈
|
|
228
|
-
in uint seg_layer_u; // 인스턴스 어트리뷰트(구 segment_index_tex.g)
|
|
229
|
-
out vec3 color;
|
|
230
|
-
vec3 decode_color(float col) {
|
|
231
|
-
int c = int(round(col));
|
|
232
|
-
int r = (c >> 16) & 0xFF;
|
|
233
|
-
int g = (c >> 8) & 0xFF;
|
|
234
|
-
int b = (c >> 0) & 0xFF;
|
|
235
|
-
float f = 1.0 / 255.0;
|
|
236
|
-
return f * vec3(r, g, b);
|
|
237
|
-
}
|
|
238
|
-
float lighting(vec3 eye_position, vec3 eye_normal) {
|
|
239
|
-
float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);
|
|
240
|
-
float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);
|
|
241
|
-
float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);
|
|
242
|
-
return ambient + top_diffuse + front_diffuse + top_specular + emission;
|
|
243
|
-
}
|
|
244
|
-
ivec2 tex_coord(sampler2D sampler, int id) {
|
|
245
|
-
ivec2 tex_size = textureSize(sampler, 0);
|
|
246
|
-
return (tex_size.y == 1) ? ivec2(id, 0) : ivec2(id % tex_size.x, id / tex_size.x);
|
|
247
|
-
}
|
|
248
|
-
void main() {
|
|
249
|
-
int vertex_id = int(vertex_id_float);
|
|
250
|
-
int seg_layer = int(seg_layer_u);
|
|
251
|
-
if (seg_layer < layer_lo || seg_layer > layer_hi) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); return; } // 범위 밖 → 클립
|
|
252
|
-
int id_a = int(seg_id_a_u);
|
|
253
|
-
int id_b = id_a + 1;
|
|
254
|
-
vec3 pos_a = texelFetch(position_tex, tex_coord(position_tex, id_a), 0).xyz;
|
|
255
|
-
vec3 pos_b = texelFetch(position_tex, tex_coord(position_tex, id_b), 0).xyz;
|
|
256
|
-
vec3 line = pos_b - pos_a;
|
|
257
|
-
float line_len = length(line);
|
|
258
|
-
vec3 line_dir;
|
|
259
|
-
if (line_len < 1e-4)
|
|
260
|
-
line_dir = vec3(1.0, 0.0, 0.0);
|
|
261
|
-
else
|
|
262
|
-
line_dir = line / line_len;
|
|
263
|
-
vec3 line_right_dir;
|
|
264
|
-
if (abs(dot(line_dir, UP)) > 0.9) {
|
|
265
|
-
line_right_dir = normalize(cross(vec3(1, 0, 0), line_dir));
|
|
266
|
-
}
|
|
267
|
-
else
|
|
268
|
-
line_right_dir = normalize(cross(line_dir, UP));
|
|
269
|
-
vec3 line_up_dir = normalize(cross(line_right_dir, line_dir));
|
|
270
|
-
const vec2 horizontal_vertical_view_signs_array[16] = vec2[](
|
|
271
|
-
vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, 0.0), vec2(0.0, -1.0),
|
|
272
|
-
vec2(0.0, -1.0), vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, 0.0),
|
|
273
|
-
vec2(0.0, 1.0), vec2(-1.0, 0.0), vec2(0.0, 0.0), vec2(1.0, 0.0),
|
|
274
|
-
vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(-1.0, 0.0), vec2(0.0, 0.0)
|
|
275
|
-
);
|
|
276
|
-
int id = vertex_id < 4 ? id_a : id_b;
|
|
277
|
-
vec3 endpoint_pos = vertex_id < 4 ? pos_a : pos_b;
|
|
278
|
-
vec4 hwa_color = texelFetch(height_width_angle_tex, tex_coord(height_width_angle_tex, id), 0); // .xyz=h/w/angle, .w=packed color
|
|
279
|
-
vec3 height_width_angle = hwa_color.xyz;
|
|
280
|
-
#ifdef FIX_TWISTING
|
|
281
|
-
int closer_id = (dot(camera_position - pos_a, camera_position - pos_a) < dot(camera_position - pos_b, camera_position - pos_b)) ? id_a : id_b;
|
|
282
|
-
vec3 closer_pos = (closer_id == id_a) ? pos_a : pos_b;
|
|
283
|
-
vec3 camera_view_dir = normalize(closer_pos - camera_position);
|
|
284
|
-
vec3 closer_height_width_angle = texelFetch(height_width_angle_tex, tex_coord(height_width_angle_tex, closer_id), 0).xyz;
|
|
285
|
-
vec3 diagonal_dir_border = normalize(closer_height_width_angle.x * line_up_dir + closer_height_width_angle.y * line_right_dir);
|
|
286
|
-
#else
|
|
287
|
-
vec3 camera_view_dir = normalize(endpoint_pos - camera_position);
|
|
288
|
-
vec3 diagonal_dir_border = normalize(height_width_angle.x * line_up_dir + height_width_angle.y * line_right_dir);
|
|
289
|
-
#endif
|
|
290
|
-
bool is_vertical_view = abs(dot(camera_view_dir, line_up_dir)) / abs(dot(diagonal_dir_border, line_up_dir)) >
|
|
291
|
-
abs(dot(camera_view_dir, line_right_dir)) / abs(dot(diagonal_dir_border, line_right_dir));
|
|
292
|
-
vec2 signs = horizontal_vertical_view_signs_array[vertex_id + 8 * int(is_vertical_view)];
|
|
293
|
-
#ifndef POINTY_CAPS
|
|
294
|
-
if (vertex_id == 2 || vertex_id == 7) signs = -horizontal_vertical_view_signs_array[(vertex_id - 2) + 8 * int(is_vertical_view)];
|
|
295
|
-
#endif
|
|
296
|
-
float view_right_sign = sign(dot(-camera_view_dir, line_right_dir));
|
|
297
|
-
float view_top_sign = sign(dot(-camera_view_dir, line_up_dir));
|
|
298
|
-
float half_height = 0.5 * height_width_angle.x;
|
|
299
|
-
float half_width = 0.5 * height_width_angle.y;
|
|
300
|
-
vec3 horizontal_dir = half_width * line_right_dir;
|
|
301
|
-
vec3 vertical_dir = half_height * line_up_dir;
|
|
302
|
-
float horizontal_sign = signs.x * view_right_sign;
|
|
303
|
-
float vertical_sign = signs.y * view_top_sign;
|
|
304
|
-
vec3 pos = endpoint_pos + horizontal_sign * horizontal_dir + vertical_sign * vertical_dir;
|
|
305
|
-
if (vertex_id == 2 || vertex_id == 7) {
|
|
306
|
-
float line_dir_sign = (vertex_id == 2) ? -1.0 : 1.0;
|
|
307
|
-
if (height_width_angle.z == 0.0) {
|
|
308
|
-
#ifdef POINTY_CAPS
|
|
309
|
-
pos += line_dir_sign * line_dir * half_width;
|
|
310
|
-
#endif
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
pos += line_dir_sign * line_dir * half_width * sin(abs(height_width_angle.z) * 0.5);
|
|
314
|
-
pos += sign(height_width_angle.z) * horizontal_dir * cos(abs(height_width_angle.z) * 0.5);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;
|
|
318
|
-
vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;
|
|
319
|
-
vec3 color_base = decode_color(hwa_color.w);
|
|
320
|
-
color = color_base * lighting(eye_position, eye_normal);
|
|
321
|
-
gl_Position = projection_matrix * vec4(eye_position, 1.0);
|
|
322
|
-
}
|
|
323
|
-
`
|
|
324
|
-
|
|
325
|
-
const SEG_FS = `
|
|
326
|
-
precision highp float;
|
|
327
|
-
in vec3 color;
|
|
328
|
-
out vec4 fragment_color;
|
|
329
|
-
void main() {
|
|
330
|
-
fragment_color = vec4(color, 1.0);
|
|
331
|
-
}
|
|
332
|
-
`
|
|
333
|
-
|
|
334
|
-
// ── three.js 인스턴싱 메시 생성 ────────────────────────────────────────────────
|
|
335
|
-
// view_matrix = camera.matrixWorldInverse * mesh.matrixWorld (커널 z-up 로컬 → eye).
|
|
336
|
-
// camera_position 은 메시 로컬(커널 z-up) 좌표로 변환(셰이더 UP=(0,0,1) 이 커널 z-up 과 일치).
|
|
337
|
-
export function makeToolpath(THREE, data) {
|
|
338
|
-
const floatTex = (arr, count) => {
|
|
339
|
-
const W = Math.min(2048, Math.max(1, count)), H = Math.max(1, Math.ceil(count / W))
|
|
340
|
-
const buf = new Float32Array(W * H * 4); buf.set(arr.subarray(0, Math.min(arr.length, W * H * 4)))
|
|
341
|
-
const t = new THREE.DataTexture(buf, W, H, THREE.RGBAFormat, THREE.FloatType)
|
|
342
|
-
t.minFilter = t.magFilter = THREE.NearestFilter; t.generateMipmaps = false; t.needsUpdate = true
|
|
343
|
-
return t
|
|
344
|
-
}
|
|
345
|
-
const posTex = floatTex(data.position, data.nV)
|
|
346
|
-
const hwaTex = floatTex(data.hwa, data.nV) // .w = packed color
|
|
347
|
-
|
|
348
|
-
const geo = new THREE.InstancedBufferGeometry()
|
|
349
|
-
// 원본 SegmentTemplate 방식 복원: 8정점 + 24인덱스(VERTEX_DATA). 비인덱스 24정점 전개 대비
|
|
350
|
-
// 정점 셰이더 실행이 인스턴스당 24→8회 — 같은 삼각형·같은 순서라 픽셀 동일.
|
|
351
|
-
geo.setIndex(VERTEX_DATA)
|
|
352
|
-
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(8 * 3), 3)) // 더미(정점 8개)
|
|
353
|
-
geo.setAttribute('vertex_id_float', new THREE.BufferAttribute(new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]), 1))
|
|
354
|
-
// 세그먼트 인덱스는 텍스처 대신 인스턴스 어트리뷰트 — segIndex(Uint32 [id_a,layer,0,0]) 배열을 그대로 인터리브.
|
|
355
|
-
const segBuf = new THREE.InstancedInterleavedBuffer(data.segIndex, 4)
|
|
356
|
-
geo.setAttribute('seg_id_a_u', new THREE.InterleavedBufferAttribute(segBuf, 1, 0))
|
|
357
|
-
geo.setAttribute('seg_layer_u', new THREE.InterleavedBufferAttribute(segBuf, 1, 1))
|
|
358
|
-
geo.instanceCount = 0
|
|
359
|
-
|
|
360
|
-
const mat = new THREE.RawShaderMaterial({
|
|
361
|
-
glslVersion: THREE.GLSL3,
|
|
362
|
-
uniforms: {
|
|
363
|
-
view_matrix: { value: new THREE.Matrix4() },
|
|
364
|
-
projection_matrix: { value: new THREE.Matrix4() },
|
|
365
|
-
camera_position: { value: new THREE.Vector3() },
|
|
366
|
-
position_tex: { value: posTex },
|
|
367
|
-
height_width_angle_tex: { value: hwaTex },
|
|
368
|
-
layer_lo: { value: 0 },
|
|
369
|
-
layer_hi: { value: data.layerCount },
|
|
370
|
-
},
|
|
371
|
-
vertexShader: SEG_VS, fragmentShader: SEG_FS,
|
|
372
|
-
side: THREE.DoubleSide,
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
const mesh = new THREE.Mesh(geo, mat)
|
|
376
|
-
// 플레이트 단위 frustum culling — 더미 position 이라 자동 계산 불가, buildSegmentData bbox 로 수동 설정.
|
|
377
|
-
// 화면 밖 플레이트만 스킵하므로 시각 변화 0 (실측: 다중 플레이트 줌인 24→110fps).
|
|
378
|
-
let sphere = null
|
|
379
|
-
if (data.bbox) {
|
|
380
|
-
const { min, max } = data.bbox
|
|
381
|
-
const c = new THREE.Vector3((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2)
|
|
382
|
-
const r = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2 + 2 // +2mm: 비드 반폭 여유
|
|
383
|
-
sphere = new THREE.Sphere(c, r)
|
|
384
|
-
}
|
|
385
|
-
mesh.frustumCulled = !!sphere
|
|
386
|
-
if (sphere) geo.boundingSphere = sphere
|
|
387
|
-
const _inv = new THREE.Matrix4(), _cw = new THREE.Vector3()
|
|
388
|
-
mesh.onBeforeRender = (renderer, scene, camera) => {
|
|
389
|
-
mat.uniforms.projection_matrix.value.copy(camera.projectionMatrix)
|
|
390
|
-
mat.uniforms.view_matrix.value.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld)
|
|
391
|
-
_inv.copy(mesh.matrixWorld).invert()
|
|
392
|
-
camera.getWorldPosition(_cw).applyMatrix4(_inv)
|
|
393
|
-
mat.uniforms.camera_position.value.copy(_cw)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// 트래블: 별도 LineSegments(레이어 순서 → setDrawRange 로 가시범위)
|
|
397
|
-
const travGeo = new THREE.BufferGeometry()
|
|
398
|
-
travGeo.setAttribute('position', new THREE.BufferAttribute(data.travelPos, 3))
|
|
399
|
-
travGeo.setDrawRange(0, 0)
|
|
400
|
-
const travLines = new THREE.LineSegments(travGeo, new THREE.LineBasicMaterial({ color: 0x6b727a }))
|
|
401
|
-
travLines.frustumCulled = !!sphere; travLines.visible = false
|
|
402
|
-
if (sphere) travGeo.boundingSphere = sphere
|
|
403
|
-
|
|
404
|
-
let travelOn = false, visLo = 0, visHi = data.layerCount - 1
|
|
405
|
-
const applyTravelRange = () => {
|
|
406
|
-
const ts = data.travelPrefix[visLo], te = data.travelPrefix[visHi + 1]
|
|
407
|
-
travGeo.setDrawRange(travelOn ? ts * 2 : 0, travelOn ? (te - ts) * 2 : 0)
|
|
408
|
-
}
|
|
409
|
-
// 25단계: 이중 슬라이더 [lo..hi] 레이어 범위. 상한=instanceCount 컷, 하한=셰이더 layer_lo 클립(둘 다 O(1)).
|
|
410
|
-
const setLayerRange = (lo, hi) => {
|
|
411
|
-
const L = data.layerCount
|
|
412
|
-
visLo = Math.max(0, Math.min(L - 1, lo | 0)); visHi = Math.max(visLo, Math.min(L - 1, hi | 0))
|
|
413
|
-
mat.uniforms.layer_lo.value = visLo; mat.uniforms.layer_hi.value = visHi
|
|
414
|
-
geo.instanceCount = data.layerSegPrefix[visHi + 1]
|
|
415
|
-
applyTravelRange()
|
|
416
|
-
}
|
|
417
|
-
const setVisibleLayers = (n) => setLayerRange(0, (n | 0) - 1) // 하위호환: 하위 n 레이어 표시
|
|
418
|
-
const setTravelVisible = (v) => { travelOn = !!v; travLines.visible = travelOn; applyTravelRange() }
|
|
419
|
-
// 뷰 타입 색 재계산 결과 업로드 — packed color 를 hwa.w 로 (computeColors 반환 포맷 [i*4]=packed 은 불변).
|
|
420
|
-
const setColors = (arr) => {
|
|
421
|
-
const d = hwaTex.image.data, n = Math.min(arr.length, d.length) / 4
|
|
422
|
-
for (let i = 0; i < n; i++) d[i * 4 + 3] = arr[i * 4]
|
|
423
|
-
hwaTex.needsUpdate = true
|
|
424
|
-
}
|
|
425
|
-
const dispose = () => {
|
|
426
|
-
geo.dispose(); mat.dispose(); posTex.dispose(); hwaTex.dispose()
|
|
427
|
-
travGeo.dispose(); travLines.material.dispose()
|
|
428
|
-
}
|
|
429
|
-
return { mesh, travLines, setVisibleLayers, setLayerRange, setTravelVisible, setColors, dispose, nSeg: data.nSeg, layerCount: data.layerCount }
|
|
430
|
-
}
|