完成项目的修改实现

This commit is contained in:
Cx330 committed 2026-08-18 16:06:28 +08:00
1 parent 3fd0cf1add
commit a5d24ee58f
22 files changed
+6344 -2

No files matched your search

File diff suppressed because it is too large. Load diff
+443
View File
@@ -0,0 +1,443 @@
 'use strict';
var FRAME_BLADE_STL_BASE64 = {
jia: {
left: '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 truncated
inner: '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 truncated
right: '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 truncated
},
yi: {
left: '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 truncated
inner: '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 truncated
right: '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 truncated
},
bing: {
left: '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 truncated
inner: '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 truncated
right: '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 Line truncated
}
};
var COMMON_ANGLES = [-84, -70, -56, -42, -28, -14, 0, 14, 28, 42, 56, 70, 84];
var SUPPORTED_BLADE_COUNTS = Object.freeze([5, 6, 7, 8, 9, 10, 11, 12, 13]);
var BLADE_ANGLE_STEP = 14;
var FAN_VARIANTS = {
jia: {
key: 'jia',
name: '大型甲',
seal: '甲',
shape: '圆弧扇头',
metric: '242 × 410',
xMin: -218,
xMax: 25,
yMin: -207,
yMax: 207,
baseZ: 0.05,
rMin: 65.45,
rMax: 215.105,
margin: 0,
edgeOverlap: 0.32,
tipOverlap: 0.22,
headArc: {
centerRadius: 190,
radius: 25.114938,
startRadius: 199.803977
},
rootTipRadius: 64.954,
rootMinRadius: 61.983,
rootInnerUpper: [[64.173, 3.438], [61.983, 6.2]],
angles: COMMON_ANGLES,
profile: [[61.983, 6.2], [96.438, 10.431], [130.894, 14.661], [165.349, 18.892], [199.803977, 23.12233], [203.068294, 21.447139], [206.053701, 19.314212], [208.69648, 16.769071], [210.940229, 13.866035], [212.73706, 10.667062], [214.048624, 7.240427], [214.84693, 3.659262], [215.114938, 0]],
outline: [[-64.173, 3.438], [-61.983, 6.2], [-96.438, 10.431], [-130.894, 14.661], [-165.349, 18.892], [-199.804, 23.122], [-210.94, 13.866], [-215.115, 0], [-210.94, -13.866], [-199.804, -23.122], [-165.349, -18.892], [-130.894, -14.661], [-96.438, -10.431], [-61.983, -6.2], [-64.173, -3.438], [-64.954, 0]]
},
yi: {
key: 'yi',
name: '大型乙',
seal: '乙',
shape: '折线圆头',
metric: '232 × 387',
xMin: -207,
xMax: 25,
yMin: -195,
yMax: 195,
baseZ: 0.05,
rMin: 69.2,
rMax: 203.85,
margin: 0,
rootTipRadius: 68.704,
rootMinRadius: 63.55,
rootInnerUpper: [[66.563, 3.223], [65.299, 5.038], [63.55, 6.392]],
angles: COMMON_ANGLES,
profile: [[63.55, 6.392], [177.497, 20.383], [183.817, 20.179], [189.774, 18.061], [194.806, 14.232], [198.433, 9.053], [201.091, 4.356], [204.276, 0]],
outline: [[-66.563, 3.223], [-65.299, 5.038], [-63.55, 6.392], [-177.497, 20.383], [-183.817, 20.179], [-189.774, 18.061], [-194.806, 14.232], [-198.433, 9.053], [-201.091, 4.356], [-204.276, 0], [-201.091, -4.356], [-198.433, -9.053], [-194.806, -14.232], [-189.774, -18.061], [-183.817, -20.179], [-177.497, -20.383], [-63.55, -6.392], [-65.299, -5.038], [-66.563, -3.223], [-68.704, 0]]
},
bing: {
key: 'bing',
name: '大型丙',
seal: '丙',
shape: '花瓣尖头',
metric: '235 × 383',
xMin: -205,
xMax: 31,
yMin: -193,
yMax: 193,
baseZ: 0.05,
rMin: 69.15,
rMax: 202.05,
margin: 0,
rootTipRadius: 68.656,
rootMinRadius: 61.5,
rootInnerUpper: [[63.968, 3.787], [61.5, 6.141]],
angles: COMMON_ANGLES,
profile: [[61.5, 6.141], [171.586, 19.658], [177.15, 19.553], [182.464, 17.905], [187.111, 14.844], [190.723, 10.611], [193.089, 7.386], [195.869, 4.508], [199.01, 2.031], [202.457, 0]],
outline: [[-63.968, 3.787], [-61.5, 6.141], [-171.586, 19.658], [-177.15, 19.553], [-182.464, 17.905], [-187.111, 14.844], [-190.723, 10.611], [-193.089, 7.386], [-195.869, 4.508], [-199.01, 2.031], [-202.457, 0], [-199.01, -2.031], [-195.869, -4.508], [-193.089, -7.386], [-190.723, -10.611], [-187.111, -14.844], [-182.464, -17.905], [-177.15, -19.553], [-171.586, -19.658], [-61.5, -6.141], [-63.968, -3.787], [-68.656, 0]]
}
};
var FAN = FAN_VARIANTS.yi;
var DETAIL = {
draft: {
radial: 72,
across: 14,
label: '快速预览'
},
standard: {
radial: 144,
across: 28,
label: '标准打印'
},
fine: {
radial: 240,
across: 48,
label: '高精度打印'
},
ultra: {
radial: 320,
across: 64,
label: '极致精度'
},
reference: {
radial: 512,
across: 96,
label: '参考作品级'
}
};
var TEXT_FONTS = Object.freeze({
modern: {
label: '现代黑体',
family: '"Microsoft YaHei UI", "Microsoft YaHei", sans-serif',
weight: 700
},
song: {
label: '雅宋',
family: '"STSong", "SimSun", "Songti SC", serif',
weight: 700
},
kai: {
label: '楷书',
family: '"STKaiti", "KaiTi", "Kaiti SC", serif',
weight: 700
},
xingkai: {
label: '行楷',
family: '"STXingkai", "KaiTi", "Kaiti SC", serif',
weight: 700
},
lishu: {
label: '隶书',
family: '"LiSu", "STLiti", "SimSun", serif',
weight: 700
},
youyuan: {
label: '圆体',
family: '"YouYuan", "Microsoft YaHei", sans-serif',
weight: 700
},
shuti: {
label: '舒体',
family: '"FZShuTi", "STKaiti", "KaiTi", serif',
weight: 700
},
hupo: {
label: '琥珀',
family: '"STHupo", "SimHei", sans-serif',
weight: 700
},
xinwei: {
label: '新魏碑意',
family: '"STXinwei", "华文新魏", "STLiti", serif',
weight: 400
},
caiyun: {
label: '彩云装饰',
family: '"STCaiyun", "华文彩云", "SimHei", sans-serif',
weight: 400
},
yaoti: {
label: '姚体古风',
family: '"FZYaoTi", "方正姚体", "STKaiti", serif',
weight: 400
},
zhongsong: {
label: '中宋雅正',
family: '"STZhongsong", "华文中宋", "SimSun", serif',
weight: 400
},
fangsong: {
label: '仿宋清雅',
family: '"FangSong", "仿宋", "SimSun", serif',
weight: 400
},
xihei: {
label: '细黑简雅',
family: '"STXihei", "华文细黑", "Microsoft YaHei", sans-serif',
weight: 400
}
});
var VECTOR_SHAPE_TOOLS = Object.freeze(['line', 'ellipse', 'rect', 'triangle', 'polygon', 'star', 'diamond', 'arrow', 'spline']);
var RATIO_SHAPE_TOOLS = Object.freeze(['ellipse', 'rect', 'triangle', 'polygon', 'star', 'diamond']);
var VECTOR_TOOL_LABELS = Object.freeze({
text: '文字',
line: '直线',
ellipse: '圆形',
rect: '方形',
triangle: '三角形',
polygon: '多边形',
star: '星形',
diamond: '菱形',
arrow: '箭头',
spline: '样条曲线'
});
var $ = (id) => document.getElementById(id);
var ui = {
fanVariants: $('fanVariants'),
bladeCountOptions: $('bladeCountOptions'),
bladeCountSummary: $('bladeCountSummary'),
bladeOrderHint: $('bladeOrderHint'),
variantSeal: $('variantSeal'),
modelTag: $('modelTag'),
metricSize: $('metricSize'),
frameToggleLabel: $('frameToggleLabel'),
adjustHint: $('adjustHint'),
fileInput: $('fileInput'),
dropzone: $('dropzone'),
fileChip: $('fileChip'),
demoLabel: $('demoLabel'),
wholeModeBtn: $('wholeModeBtn'),
bladeModeBtn: $('bladeModeBtn'),
bladePanel: $('bladePanel'),
bladeGrid: $('bladeGrid'),
dropTitle: $('dropTitle'),
dropHint: $('dropHint'),
maskCanvas: $('maskCanvas'),
maskWrap: $('maskWrap'),
maskBackdrop: $('maskBackdrop'),
expandMask: $('expandMask'),
brushCursor: $('brushCursor'),
textPlacementPreview: $('textPlacementPreview'),
textPreviewGlyph: $('textPreviewGlyph'),
sourceCanvas: $('sourceCanvas'),
maskFloatingToolbar: $('maskFloatingToolbar'),
floatToolContext: $('floatToolContext'),
floatZoomOut: $('floatZoomOut'),
floatZoomIn: $('floatZoomIn'),
floatRotateLeft: $('floatRotateLeft'),
floatRotateRight: $('floatRotateRight'),
floatResetImage: $('floatResetImage'),
floatTransformValue: $('floatTransformValue'),
floatImageRotation: $('floatImageRotation'),
floatImageRotationValue: $('floatImageRotationValue'),
floatBrushControl: $('floatBrushControl'),
floatBrushSize: $('floatBrushSize'),
floatBrushSizeValue: $('floatBrushSizeValue'),
floatTextControl: $('floatTextControl'),
floatTextValue: $('floatTextValue'),
floatTextSizeControl: $('floatTextSizeControl'),
floatTextSize: $('floatTextSize'),
floatTextSizeValue: $('floatTextSizeValue'),
floatTextArcControl: $('floatTextArcControl'),
floatTextArc: $('floatTextArc'),
floatTextArcValue: $('floatTextArcValue'),
floatTextFontControl: $('floatTextFontControl'),
floatTextFont: $('floatTextFont'),
floatTextStyleControl: $('floatTextStyleControl'),
floatTextStyle: $('floatTextStyle'),
floatPolygonSidesControl: $('floatPolygonSidesControl'),
floatPolygonSides: $('floatPolygonSides'),
floatPolygonSidesValue: $('floatPolygonSidesValue'),
floatSplineTensionControl: $('floatSplineTensionControl'),
floatSplineTension: $('floatSplineTension'),
floatSplineTensionValue: $('floatSplineTensionValue'),
floatShapeRatioControl: $('floatShapeRatioControl'),
floatShapeRatio: $('floatShapeRatio'),
floatColorControl: $('floatColorControl'),
floatEditorColor: $('floatEditorColor'),
floatUndoEdit: $('floatUndoEdit'),
floatDeleteSelected: $('floatDeleteSelected'),
floatClearEdit: $('floatClearEdit'),
coverBtn: $('coverBtn'),
containBtn: $('containBtn'),
imageScale: $('imageScale'),
offsetX: $('offsetX'),
offsetY: $('offsetY'),
imageRotation: $('imageRotation'),
rotateLeft: $('rotateLeft'),
rotateRight: $('rotateRight'),
zoomOut: $('zoomOut'),
zoomIn: $('zoomIn'),
resetImage: $('resetImage'),
brushSize: $('brushSize'),
brushEditorOptions: $('brushEditorOptions'),
shapeEditorOptions: $('shapeEditorOptions'),
shapeRatioControl: $('shapeRatioControl'),
shapeRatioHint: $('shapeRatioHint'),
polygonSidesControl: $('polygonSidesControl'),
splineTensionControl: $('splineTensionControl'),
polygonSides: $('polygonSides'),
splineTension: $('splineTension'),
textEditorOptions: $('textEditorOptions'),
textFontChips: $('textFontChips'),
textValue: $('textValue'),
textStyle: $('textStyle'),
textSize: $('textSize'),
textArc: $('textArc'),
editorColor: $('editorColor'),
editorColorControl: $('editorColorControl'),
undoEdit: $('undoEdit'),
clearEdit: $('clearEdit'),
deleteSelected: $('deleteSelected'),
selectionInfo: $('selectionInfo'),
selectionLabel: $('selectionLabel'),
editorHint: $('editorHint'),
minThickness: $('minThickness'),
maxThickness: $('maxThickness'),
reliefMargin: $('reliefMargin'),
contrast: $('contrast'),
reliefGamma: $('reliefGamma'),
blur: $('blur'),
invert: $('invert'),
detail: $('detail'),
includeFrame: $('includeFrame'),
threeMFStructure: $('threeMFStructure'),
threeMFHint: $('threeMFHint'),
resetAll: $('resetAll'),
exportBtn: $('exportBtn'),
export3mfBtn: $('export3mfBtn'),
status: $('status'),
precisionGuide: $('precisionGuide'),
precisionHint: $('precisionHint'),
precisionGrid: $('precisionGrid'),
glCanvas: $('glCanvas'),
busy: $('busy'),
busyText: $('busyText'),
topView: $('topView'),
isoView: $('isoView'),
fitView: $('fitView')
};
var bladeConfigSlots = Array.from({
length: COMMON_ANGLES.length
}, () => makeImageConfig());
var state = {
fanVariant: 'yi',
bladeCount: 11,
imageMode: 'whole',
selectedBlade: 0,
globalConfig: makeImageConfig(ui.sourceCanvas, '示例山水'),
bladeConfigSlots,
bladeConfigs: bladeConfigSlots.slice(),
sourcePixels: null,
maskLookup: null,
activeBounds: null,
frameTemplates: null,
frameWelds: null,
frameOriginalPositions: null,
frameOriginalNormals: null,
framePositions: null,
frameNormals: null,
frameRemovedTriangles: 0,
reliefPositions: null,
reliefNormals: null,
rebuildTimer: 0,
camera: {
pitch: 0.60,
yaw: -1.65,
zoom: 620
},
dragging: false,
lastX: 0,
lastY: 0,
imageDragging: false,
imagePointerX: 0,
imagePointerY: 0,
imageStartX: 0,
imageStartY: 0,
editorTool: 'move',
textFont: 'xingkai',
textStyle: 'fill',
textArc: 35,
shapeRatioMode: 'regular',
editorDrawing: false,
editorLastPoint: null,
editorPointerId: null,
editorShapeStart: null,
editorShapePoints: null,
editorPreview: null,
editorObjectDragging: false,
editorObjectDragStart: null,
editorObjectOriginal: null,
editorObjectMoved: false,
editorHoverX: null,
editorHoverY: null,
maskExpanded: false
};
function makeImageConfig(canvas=null, name='') {
var target = canvas || document.createElement('canvas');
if (!canvas) {
target.width = 720;
target.height = 220;
}
var editCanvas = document.createElement('canvas');
editCanvas.width = target.width;
editCanvas.height = target.height;
var paintCanvas = document.createElement('canvas');
paintCanvas.width = target.width;
paintCanvas.height = target.height;
return {
image: null,
name,
fit: 'cover',
rotation: 0,
scale: 100,
offsetX: 0,
offsetY: 0,
canvas: target,
editCanvas,
paintCanvas,
paintDirty: false,
vectorObjects: [],
nextVectorId: 1,
selectedObjectId: null,
editHistory: [],
editDirty: false,
pixels: null,
isDemo: false
};
}
function activeConfig() {
return state.imageMode === 'whole' ? state.globalConfig : state.bladeConfigs[state.selectedBlade];
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function deg(value) {
return value * Math.PI / 180;
}
function sanitizeName(name) {
return (name || '图片').replace(/\.[^.]+$/, '').replace(/[\\/:*?"<>|]+/g, '_').slice(0, 56) || '图片';
}
+611
View File
@@ -0,0 +1,611 @@
function buildRelief() {
var detail = DETAIL[ui.detail.value];
var minT = Number(ui.minThickness.value);
var maxT = Number(ui.maxThickness.value);
var margin = currentReliefMargin();
var gamma = Number(ui.reliefGamma.value);
var nr = detail.radial;
var nv = detail.across;
var weldedBlades = ui.includeFrame.checked && state.frameWelds ? state.frameWelds.filter(Boolean).length : 0;
var baseTrianglesPerBlade = 4 * nr * nv + rootCapTriangleCount();
var triangleCapacity = baseTrianglesPerBlade * FAN.angles.length + weldedBlades * 5;
var mesh = createMeshWriter(triangleCapacity);
for (let bladeIndex = 0; bladeIndex < FAN.angles.length; bladeIndex++) {
var angle = FAN.angles[bladeIndex];
var frameHole = ui.includeFrame.checked && state.frameWelds ? state.frameWelds[bladeIndex] : null;
var weldJ = Math.floor((nv - 1) / 2);
var reliefHole = null;
var top = Array.from({
length: nr
}, () => new Array(nv));
var bottom = Array.from({
length: nr
}, () => new Array(nv));
var makeReliefPair = (radius, localY, normalizedCross=null) => {
var world = rotatePoint(-radius, localY, angle);
var cross = normalizedCross === null ? clamp(localY / reliefHalfAt(radius, margin), -1, 1) : normalizedCross;
var lum = state.imageMode === 'blade' ? sampleBladeLuminance(bladeIndex, radius, cross) : sampleLuminance(world[0], world[1]);
var darkness = Math.pow(1 - lum, gamma);
var thickness = minT + darkness * (maxT - minT);
return {
top: [world[0], world[1], FAN.baseZ + thickness],
bottom: [world[0], world[1], FAN.baseZ]
};
}
;
for (let i = 0; i < nr; i++) {
var r = reliefRadiusAt(i, nr);
var half = reliefHalfAt(r, margin);
for (let j = 0; j < nv; j++) {
var cross = -1 + 2 * j / (nv - 1);
var localY = cross * half;
var pair = makeReliefPair(r, localY, cross);
top[i][j] = pair.top;
bottom[i][j] = pair.bottom;
}
}
for (let i = 0; i < nr - 1; i++) {
for (let j = 0; j < nv - 1; j++) {
var a = top[i][j]
, b = top[i + 1][j]
, c = top[i + 1][j + 1]
, d = top[i][j + 1];
pushTri(mesh, a, c, b);
pushTri(mesh, a, d, c);
var ba = bottom[i][j]
, bb = bottom[i + 1][j]
, bc = bottom[i + 1][j + 1]
, bd = bottom[i][j + 1];
pushTri(mesh, ba, bb, bc);
if (frameHole && i === 0 && j === weldJ) {
reliefHole = [ba, bc, bd];
} else {
pushTri(mesh, ba, bc, bd);
}
}
}
for (let i = 0; i < nr - 1; i++) {
var ta = top[i][0]
, tb = top[i + 1][0]
, ba = bottom[i][0]
, bb = bottom[i + 1][0];
pushTri(mesh, ta, tb, bb);
pushTri(mesh, ta, bb, ba);
var j = nv - 1;
var tc = top[i][j]
, td = top[i + 1][j]
, bc = bottom[i][j]
, bd = bottom[i + 1][j];
pushTri(mesh, tc, bd, td);
pushTri(mesh, tc, bc, bd);
}
var outerI = nr - 1;
var outerTip = makeReliefPair(outerReliefTipRadius(), 0, 0);
for (let j = 0; j < nv - 1; j++) {
var ta = top[outerI][j]
, tb = top[outerI][j + 1];
var ba = bottom[outerI][j]
, bb = bottom[outerI][j + 1];
pushTriFacing(mesh, ta, outerTip.top, tb, true);
pushTriFacing(mesh, ba, bb, outerTip.bottom, false);
}
var outerLower = {
top: top[outerI][0],
bottom: bottom[outerI][0]
};
var outerUpper = {
top: top[outerI][nv - 1],
bottom: bottom[outerI][nv - 1]
};
for (const [current,next] of [[outerLower, outerTip], [outerTip, outerUpper]]) {
pushTri(mesh, current.top, next.bottom, current.bottom);
pushTri(mesh, current.top, next.top, next.bottom);
}
var tip = makeReliefPair(FAN.rootTipRadius, 0, 0);
var upperRoot = FAN.rootInnerUpper.map( ([radius,half]) => makeReliefPair(radius, Math.max(.25, half - margin)));
var lowerRoot = FAN.rootInnerUpper.map( ([radius,half]) => makeReliefPair(radius, -Math.max(.25, half - margin)));
var joinBottom = {
top: top[0][0],
bottom: bottom[0][0]
};
var joinTop = {
top: top[0][nv - 1],
bottom: bottom[0][nv - 1]
};
for (let j = 0; j < nv - 1; j++) {
pushTriFacing(mesh, tip.top, top[0][j], top[0][j + 1], true);
pushTriFacing(mesh, tip.bottom, bottom[0][j], bottom[0][j + 1], false);
}
for (const polygon of [[tip, ...upperRoot, joinTop], [tip, ...lowerRoot, joinBottom]]) {
for (const triangle of triangulatePairPolygon(polygon)) {
pushTriFacing(mesh, triangle[0].top, triangle[1].top, triangle[2].top, true);
pushTriFacing(mesh, triangle[0].bottom, triangle[1].bottom, triangle[2].bottom, false);
}
}
var rootSide = [joinBottom, ...lowerRoot.slice().reverse(), tip, ...upperRoot, joinTop];
for (let index = 0; index < rootSide.length - 1; index++) {
var current = rootSide[index]
, next = rootSide[index + 1];
pushTri(mesh, current.top, current.bottom, next.bottom);
pushTri(mesh, current.top, next.bottom, next.top);
}
if (frameHole && reliefHole) {
var matchedRelief = matchTriangleLoop(frameHole, reliefHole);
for (let index = 0; index < 3; index++) {
var next = (index + 1) % 3;
var frameA = frameHole[index]
, frameB = frameHole[next];
var reliefA = matchedRelief[index]
, reliefB = matchedRelief[next];
pushTri(mesh, frameA, frameB, reliefB);
pushTri(mesh, frameA, reliefB, reliefA);
}
}
}
if (mesh.offset !== triangleCapacity * 9) {
throw new Error(`网格面数校验失败:预计 ${triangleCapacity.toLocaleString()},实际 ${(mesh.offset / 9).toLocaleString()}`);
}
state.reliefPositions = mesh.positions;
state.reliefNormals = mesh.normals;
updateMeshBuffers();
var reliefTriangles = state.reliefPositions.length / 9;
var frameTriangles = ui.includeFrame.checked && state.framePositions ? state.framePositions.length / 9 : 0;
var total = reliefTriangles + frameTriangles;
var weldStatus = ui.includeFrame.checked ? ' · 浮雕已焊接' : '';
ui.status.textContent = `${DETAIL[ui.detail.value].label}${weldStatus} · ${total.toLocaleString()} 个三角面 · 预计 STL ${(84 + total * 50) / 1048576 < 1 ? ((84 + total * 50) / 1024).toFixed(0) + ' KB' : ((84 + total * 50) / 1048576).toFixed(1) + ' MB'}`;
}
function closedReliefBladeSources() {
var detail = DETAIL[ui.detail.value];
var nr = detail.radial;
var nv = detail.across;
var baseTrianglesPerBlade = 4 * nr * nv + rootCapTriangleCount();
var sources = [];
var triangleCursor = 0;
for (let bladeIndex = 0; bladeIndex < FAN.angles.length; bladeIndex++) {
var wasWelded = Boolean(ui.includeFrame.checked && state.frameWelds && state.frameWelds[bladeIndex]);
var storedTriangleCount = baseTrianglesPerBlade + (wasWelded ? 5 : 0);
var sourceStart = triangleCursor * 9;
var sourceEnd = sourceStart + storedTriangleCount * 9;
if (!wasWelded) {
sources.push(state.reliefPositions.slice(sourceStart, sourceEnd));
} else {
var closed = new Float32Array(baseTrianglesPerBlade * 9);
var coreTriangleCount = baseTrianglesPerBlade - 1;
closed.set(state.reliefPositions.subarray(sourceStart, sourceStart + coreTriangleCount * 9));
var weldJ = Math.floor((nv - 1) / 2);
var r0 = FAN.rMin;
var r1 = reliefRadiusAt(1, nr);
var half0 = reliefHalfAt(r0);
var half1 = reliefHalfAt(r1);
var cross0 = -1 + 2 * weldJ / (nv - 1);
var cross1 = -1 + 2 * (weldJ + 1) / (nv - 1);
var angle = FAN.angles[bladeIndex];
var ba2 = rotatePoint(-r0, cross0 * half0, angle);
var bc2 = rotatePoint(-r1, cross1 * half1, angle);
var bd2 = rotatePoint(-r0, cross1 * half0, angle);
var offset = coreTriangleCount * 9;
closed.set([ba2[0], ba2[1], FAN.baseZ, bc2[0], bc2[1], FAN.baseZ, bd2[0], bd2[1], FAN.baseZ], offset);
sources.push(closed);
}
triangleCursor += storedTriangleCount;
}
if (triangleCursor * 9 !== state.reliefPositions.length) {
throw new Error('浮雕叶片拆分校验失败,请重新生成网格后再试');
}
return sources;
}
function writeSTL() {
if (!state.reliefPositions)
return;
var sources = ui.includeFrame.checked ? [state.framePositions, state.reliefPositions] : [state.reliefPositions];
var triangleCount = sources.reduce( (sum, source) => sum + source.length / 9, 0);
var buffer = new ArrayBuffer(84 + triangleCount * 50);
var bytes = new Uint8Array(buffer);
var header = new TextEncoder().encode(`DreamLife Translucent Relief - ${FAN.name} - millimeters`);
bytes.set(header.slice(0, 80), 0);
var view = new DataView(buffer);
view.setUint32(80, triangleCount, true);
var offset = 84;
for (const source of sources) {
for (let i = 0; i < source.length; i += 9) {
var a = [source[i], source[i + 1], source[i + 2]];
var b = [source[i + 3], source[i + 4], source[i + 5]];
var c = [source[i + 6], source[i + 7], source[i + 8]];
var n = triangleNormal(a, b, c);
view.setFloat32(offset, n[0], true);
view.setFloat32(offset + 4, n[1], true);
view.setFloat32(offset + 8, n[2], true);
var p = offset + 12;
for (const vertex of [a, b, c]) {
view.setFloat32(p, vertex[0], true);
view.setFloat32(p + 4, vertex[1], true);
view.setFloat32(p + 8, vertex[2], true);
p += 12;
}
view.setUint16(offset + 48, 0, true);
offset += 50;
}
}
var blob = new Blob([buffer],{
type: 'model/stl'
});
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
var assigned = state.bladeConfigs.filter( (config) => config.image).length;
var sourceName = state.imageMode === 'blade' ? `${assigned}片独立图片` : state.globalConfig.name;
link.download = `${sanitizeName(sourceName)}_${FAN.name}_${state.bladeCount}片_DreamLife_不含转轴.stl`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout( () => URL.revokeObjectURL(url), 2000);
ui.status.textContent = `已导出 ${link.download} · 不含转轴 · ${triangleCount.toLocaleString()} 个三角面`;
}
var CRC32_TABLE = ( () => {
var table = new Uint32Array(256);
for (let index = 0; index < 256; index++) {
var value = index;
for (let bit = 0; bit < 8; bit++)
value = (value >>> 1) ^ ((value & 1) ? 0xedb88320 : 0);
table[index] = value >>> 0;
}
return table;
}
)();
function crc32(chunks) {
var crc = 0xffffffff;
for (const chunk of chunks) {
for (let index = 0; index < chunk.length; index++) {
crc = CRC32_TABLE[(crc ^ chunk[index]) & 0xff] ^ (crc >>> 8);
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function currentSourceName() {
var assigned = state.bladeConfigs.filter( (config) => config.image).length;
return state.imageMode === 'blade' ? `${assigned}片独立图片` : state.globalConfig.name;
}
function xmlEscape(value) {
return String(value).replace(/[&<>"']/g, (character) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&apos;'
})[character]);
}
function format3MFNumber(value) {
var normalized = value === 0 ? 0 : Math.fround(value);
return Number(normalized.toPrecision(9)).toString();
}
function index3MFMesh(sources) {
var triangleCount = sources.reduce( (sum, source) => sum + source.length / 9, 0);
var triangleIndices = new Uint32Array(triangleCount * 3);
var vertices = new Float32Array(triangleIndices.length * 3);
var tableSize = 1024;
while (tableSize < triangleCount * 2)
tableSize *= 2;
var slots = new Int32Array(tableSize);
var mask = tableSize - 1;
var floatBits = new Float32Array(3);
var uintBits = new Uint32Array(floatBits.buffer);
var boundsMin = [Infinity, Infinity, Infinity];
var boundsMax = [-Infinity, -Infinity, -Infinity];
var vertexCount = 0;
var occurrence = 0;
var findOrAddVertex = (rawX, rawY, rawZ) => {
var x = rawX === 0 ? 0 : rawX;
var y = rawY === 0 ? 0 : rawY;
var z = rawZ === 0 ? 0 : rawZ;
floatBits[0] = x;
floatBits[1] = y;
floatBits[2] = z;
var hash = Math.imul(uintBits[0] ^ (uintBits[0] >>> 16), 0x45d9f3b);
hash ^= Math.imul(uintBits[1] ^ (uintBits[1] >>> 16), 0x119de1f3);
hash ^= Math.imul(uintBits[2] ^ (uintBits[2] >>> 16), 0x344b1a4d);
var slot = hash & mask;
while (slots[slot]) {
var existing = slots[slot] - 1;
var base = existing * 3;
if (vertices[base] === x && vertices[base + 1] === y && vertices[base + 2] === z)
return existing;
slot = (slot + 1) & mask;
}
var base = vertexCount * 3;
vertices[base] = x;
vertices[base + 1] = y;
vertices[base + 2] = z;
slots[slot] = vertexCount + 1;
boundsMin[0] = Math.min(boundsMin[0], x);
boundsMax[0] = Math.max(boundsMax[0], x);
boundsMin[1] = Math.min(boundsMin[1], y);
boundsMax[1] = Math.max(boundsMax[1], y);
boundsMin[2] = Math.min(boundsMin[2], z);
boundsMax[2] = Math.max(boundsMax[2], z);
return vertexCount++;
}
;
for (const source of sources) {
for (let index = 0; index < source.length; index += 3) {
triangleIndices[occurrence++] = findOrAddVertex(source[index], source[index + 1], source[index + 2]);
}
}
return {
triangleCount,
triangleIndices,
vertexCount,
vertices: vertices.subarray(0, vertexCount * 3),
boundsMin,
boundsMax
};
}
function build3MFModelChunks(meshEntries, title, structure) {
var encoder = new TextEncoder();
var chunks = [];
var lines = [];
var characterCount = 0;
var flush = () => {
if (!lines.length)
return;
chunks.push(encoder.encode(lines.join('')));
lines = [];
characterCount = 0;
}
;
var append = (line) => {
lines.push(line);
characterCount += line.length;
if (characterCount >= 1024 * 1024)
flush();
}
;
var safeTitle = xmlEscape(title);
append('<?xml version="1.0" encoding="UTF-8"?>');
append('<model unit="millimeter" xml:lang="zh-CN" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">');
append(`<metadata name="Title">${safeTitle}</metadata>`);
append('<metadata name="Application">DreamLife-Translucent_Relief</metadata>');
append(`<metadata name="Description">${xmlEscape(FAN.name)}折扇骨架与透光浮雕可拆分打印模型,不含转轴</metadata>`);
append(`<metadata name="CreationDate">${new Date().toISOString().slice(0, 10)}</metadata>`);
append('<resources>');
var boundsMin = [Infinity, Infinity, Infinity];
for (let entryIndex = 0; entryIndex < meshEntries.length; entryIndex++) {
var entry = meshEntries[entryIndex];
var mesh = entry.mesh;
var objectId = entryIndex + 1;
for (let axis = 0; axis < 3; axis++)
boundsMin[axis] = Math.min(boundsMin[axis], mesh.boundsMin[axis]);
append(`<object id="${objectId}" type="model" name="${xmlEscape(entry.name)}" partnumber="${xmlEscape(entry.partNumber)}"><mesh><vertices>`);
for (let vertex = 0; vertex < mesh.vertexCount; vertex++) {
var offset = vertex * 3;
append(`<vertex x="${format3MFNumber(mesh.vertices[offset])}" y="${format3MFNumber(mesh.vertices[offset + 1])}" z="${format3MFNumber(mesh.vertices[offset + 2])}"/>`);
}
append('</vertices><triangles>');
for (let triangle = 0; triangle < mesh.triangleCount; triangle++) {
var offset = triangle * 3;
append(`<triangle v1="${mesh.triangleIndices[offset]}" v2="${mesh.triangleIndices[offset + 1]}" v3="${mesh.triangleIndices[offset + 2]}"/>`);
}
append('</triangles></mesh></object>');
}
var buildObjectIds;
if (structure === 'parts') {
var assemblyId = meshEntries.length + 1;
append(`<object id="${assemblyId}" type="model" name="${safeTitle} 总成" partnumber="FAN-ASSEMBLY"><components>`);
for (let entryIndex = 0; entryIndex < meshEntries.length; entryIndex++) {
append(`<component objectid="${entryIndex + 1}"/>`);
}
append('</components></object>');
buildObjectIds = [assemblyId];
} else if (structure === 'objects') {
buildObjectIds = meshEntries.map( (entry, index) => index + 1);
} else {
buildObjectIds = [1];
}
append('</resources>');
var tx = Math.max(0, -boundsMin[0]);
var ty = Math.max(0, -boundsMin[1]);
var tz = Math.max(0, -boundsMin[2]);
var transform = `1 0 0 0 1 0 0 0 1 ${format3MFNumber(tx)} ${format3MFNumber(ty)} ${format3MFNumber(tz)}`;
append('<build>');
for (const objectId of buildObjectIds) {
var partNumber = structure === 'objects' ? meshEntries[objectId - 1].partNumber : 'FAN-ASSEMBLY';
append(`<item objectid="${objectId}" partnumber="${xmlEscape(partNumber)}" transform="${transform}"/>`);
}
append('</build></model>');
flush();
return chunks;
}
function zipStored(entries) {
var encoder = new TextEncoder();
var now = new Date();
var dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
var dosDate = ((Math.max(1980, now.getFullYear()) - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
var parts = [];
var records = [];
var offset = 0;
for (const entry of entries) {
var name = encoder.encode(entry.name);
var size = entry.chunks.reduce( (sum, chunk) => sum + chunk.length, 0);
if (size > 0xffffffff)
throw new Error('3MF 内容超过普通 ZIP 的 4 GB 上限');
var checksum = crc32(entry.chunks);
var header = new Uint8Array(30 + name.length);
var view = new DataView(header.buffer);
view.setUint32(0, 0x04034b50, true);
view.setUint16(4, 20, true);
view.setUint16(6, 0x0800, true);
view.setUint16(8, 0, true);
view.setUint16(10, dosTime, true);
view.setUint16(12, dosDate, true);
view.setUint32(14, checksum, true);
view.setUint32(18, size, true);
view.setUint32(22, size, true);
view.setUint16(26, name.length, true);
header.set(name, 30);
parts.push(header, ...entry.chunks);
records.push({
name,
size,
checksum,
offset
});
offset += header.length + size;
}
var centralStart = offset;
for (const record of records) {
var header = new Uint8Array(46 + record.name.length);
var view = new DataView(header.buffer);
view.setUint32(0, 0x02014b50, true);
view.setUint16(4, 20, true);
view.setUint16(6, 20, true);
view.setUint16(8, 0x0800, true);
view.setUint16(10, 0, true);
view.setUint16(12, dosTime, true);
view.setUint16(14, dosDate, true);
view.setUint32(16, record.checksum, true);
view.setUint32(20, record.size, true);
view.setUint32(24, record.size, true);
view.setUint16(28, record.name.length, true);
view.setUint32(42, record.offset, true);
header.set(record.name, 46);
parts.push(header);
offset += header.length;
}
var end = new Uint8Array(22);
var endView = new DataView(end.buffer);
endView.setUint32(0, 0x06054b50, true);
endView.setUint16(8, records.length, true);
endView.setUint16(10, records.length, true);
endView.setUint32(12, offset - centralStart, true);
endView.setUint32(16, centralStart, true);
parts.push(end);
return new Blob(parts,{
type: 'model/3mf'
});
}
function downloadBlob(blob, filename) {
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout( () => URL.revokeObjectURL(url), 5000);
}
function nextPaint() {
return new Promise( (resolve) => requestAnimationFrame( () => window.setTimeout(resolve, 0)));
}
function prepare3MFRawEntries(structure, title) {
if (structure === 'merged') {
return [{
name: title,
partNumber: 'FAN-MERGED',
sources: ui.includeFrame.checked ? [state.framePositions, state.reliefPositions] : [state.reliefPositions]
}];
}
var entries = [];
if (ui.includeFrame.checked) {
entries.push({
name: `${FAN.name}${state.bladeCount}片骨架`,
partNumber: 'FAN-FRAME',
sources: [state.frameOriginalPositions || state.framePositions]
});
}
closedReliefBladeSources().forEach( (source, bladeIndex) => {
var serial = String(bladeIndex + 1).padStart(2, '0');
entries.push({
name: `浮雕扇叶 ${serial}`,
partNumber: `FAN-BLADE-${serial}`,
sources: [source]
});
}
);
return entries;
}
async function write3MF() {
if (!state.reliefPositions)
return;
showBusy(true, '正在整理 3MF 网格');
try {
await nextPaint();
var structure = ui.threeMFStructure.value;
var title = `${sanitizeName(currentSourceName())}_${FAN.name}_${state.bladeCount}片_DreamLife_不含转轴`;
var rawEntries = prepare3MFRawEntries(structure, title);
var meshEntries = [];
for (let entryIndex = 0; entryIndex < rawEntries.length; entryIndex++) {
var entry = rawEntries[entryIndex];
ui.busyText.textContent = structure === 'merged' ? '正在整理整扇网格' : `正在整理零件 ${entryIndex + 1} / ${rawEntries.length}`;
if (entryIndex % 3 === 0)
await nextPaint();
meshEntries.push({
name: entry.name,
partNumber: entry.partNumber,
mesh: index3MFMesh(entry.sources)
});
}
ui.busyText.textContent = '正在打包 3MF 文件';
await nextPaint();
var modelChunks = build3MFModelChunks(meshEntries, title, structure);
var encoder = new TextEncoder();
var blob = zipStored([{
name: '[Content_Types].xml',
chunks: [encoder.encode('<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/></Types>')]
}, {
name: '_rels/.rels',
chunks: [encoder.encode('<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>')]
}, {
name: '3D/3dmodel.model',
chunks: modelChunks
}]);
var suffix = structure === 'parts' ? '_总成可拆零件' : structure === 'objects' ? '_独立对象' : '';
var filename = `${title}${suffix}.model.3mf`;
downloadBlob(blob, filename);
var vertexCount = meshEntries.reduce( (sum, entry) => sum + entry.mesh.vertexCount, 0);
var triangleCount = meshEntries.reduce( (sum, entry) => sum + entry.mesh.triangleCount, 0);
var structureLabel = structure === 'parts' ? `1 个总成 · ${meshEntries.length} 个可拆零件` : structure === 'objects' ? `${meshEntries.length} 个独立对象` : '1 个完整对象';
ui.status.textContent = `已导出 ${filename} · 不含转轴 · ${structureLabel} · ${vertexCount.toLocaleString()} 个顶点 · ${triangleCount.toLocaleString()} 个三角面`;
} catch (error) {
console.error(error);
ui.status.textContent = `3MF 导出失败:${error.message}`;
} finally {
showBusy(false);
}
}
+451
View File
@@ -0,0 +1,451 @@
function anglesForBladeCount(count) {
var safeCount = SUPPORTED_BLADE_COUNTS.includes(Number(count)) ? Number(count) : 11;
var half = (safeCount - 1) / 2;
return Array.from({
length: safeCount
}, (_, index) => (index - half) * BLADE_ANGLE_STEP);
}
function syncActiveBladeConfigs() {
var start = Math.floor((COMMON_ANGLES.length - state.bladeCount) / 2);
state.bladeConfigs = state.bladeConfigSlots.slice(start, start + state.bladeCount);
}
function outlineExtents(angles) {
var minX = Infinity
, maxX = -Infinity
, minY = Infinity
, maxY = -Infinity;
angles.forEach( (angle) => {
FAN.outline.forEach( ([x,y]) => {
var point = rotatePoint(x, y, angle);
minX = Math.min(minX, point[0]);
maxX = Math.max(maxX, point[0]);
minY = Math.min(minY, point[1]);
maxY = Math.max(maxY, point[1]);
}
);
}
);
return {
minX,
maxX,
minY,
maxY
};
}
function updateActiveBounds() {
var full = outlineExtents(anglesForBladeCount(11));
var active = outlineExtents(FAN.angles);
var lowerPadding = full.minY - FAN.yMin;
var upperPadding = FAN.yMax - full.maxY;
state.activeBounds = {
xMin: FAN.xMin,
xMax: FAN.xMax,
yMin: active.minY - lowerPadding,
yMax: active.maxY + upperPadding
};
}
function modelBounds() {
return state.activeBounds || {
xMin: FAN.xMin,
xMax: FAN.xMax,
yMin: FAN.yMin,
yMax: FAN.yMax
};
}
function widthAt(radius) {
if (FAN.headArc && radius >= FAN.headArc.startRadius) {
var offset = radius - FAN.headArc.centerRadius;
return Math.sqrt(Math.max(0, FAN.headArc.radius ** 2 - offset ** 2));
}
var p = FAN.profile;
for (let i = 0; i < p.length - 1; i++) {
if (radius >= p[i][0] && radius <= p[i + 1][0]) {
var t = (radius - p[i][0]) / (p[i + 1][0] - p[i][0]);
return p[i][1] + (p[i + 1][1] - p[i][1]) * t;
}
}
return radius < p[0][0] ? p[0][1] : p[p.length - 1][1];
}
function rotatePoint(x, y, angleDegrees) {
var a = deg(angleDegrees)
, c = Math.cos(a)
, s = Math.sin(a);
return [x * c - y * s, x * s + y * c];
}
function decodeBase64(base64) {
var binary = atob(base64);
var bytes = new Uint8Array(binary.length);
var chunk = 1 << 15;
for (let start = 0; start < binary.length; start += chunk) {
var end = Math.min(start + chunk, binary.length);
for (let i = start; i < end; i++)
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function parseBinarySTL(base64) {
var bytes = decodeBase64(base64);
var view = new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
var count = view.getUint32(80, true);
if (84 + count * 50 > bytes.byteLength)
throw new Error('内置骨架 STL 数据不完整');
var positions = new Float32Array(count * 9);
var normals = new Float32Array(count * 9);
var offset = 84;
for (let t = 0; t < count; t++,
offset += 50) {
var nx = view.getFloat32(offset, true)
, ny = view.getFloat32(offset + 4, true)
, nz = view.getFloat32(offset + 8, true);
var out = t * 9;
for (let v = 0; v < 3; v++) {
var source = offset + 12 + v * 12;
positions[out + v * 3] = view.getFloat32(source, true);
positions[out + v * 3 + 1] = view.getFloat32(source + 4, true);
positions[out + v * 3 + 2] = view.getFloat32(source + 8, true);
normals[out + v * 3] = nx;
normals[out + v * 3 + 1] = ny;
normals[out + v * 3 + 2] = nz;
}
}
return {
positions,
normals,
count
};
}
function assembleFrameFromTemplates() {
if (!state.frameTemplates)
throw new Error('扇叶骨架模板尚未载入');
// 叶片编号按预览画面从右向左排列:第 1 片位于右端,最后一片位于左端。
// 首尾护骨是非对称件,必须与画面方位一致,否则导出后会装反。
var roles = FAN.angles.map( (angle, index) => index === 0 ? 'right' : (index === FAN.angles.length - 1 ? 'left' : 'inner'));
var totalLength = roles.reduce( (sum, role) => sum + state.frameTemplates[role].positions.length, 0);
var positions = new Float32Array(totalLength);
var normals = new Float32Array(totalLength);
var output = 0;
roles.forEach( (role, bladeIndex) => {
var template = state.frameTemplates[role];
var radians = deg(FAN.angles[bladeIndex]);
var cosine = Math.cos(radians)
, sine = Math.sin(radians);
for (let index = 0; index < template.positions.length; index += 3) {
var x = template.positions[index]
, y = template.positions[index + 1];
positions[output + index] = x * cosine - y * sine;
positions[output + index + 1] = x * sine + y * cosine;
positions[output + index + 2] = template.positions[index + 2];
var nx = template.normals[index]
, ny = template.normals[index + 1];
normals[output + index] = nx * cosine - ny * sine;
normals[output + index + 1] = nx * sine + ny * cosine;
normals[output + index + 2] = template.normals[index + 2];
}
output += template.positions.length;
}
);
return {
positions,
normals,
count: totalLength / 9
};
}
function rebuildFrameFromTemplates() {
var frame = assembleFrameFromTemplates();
state.frameOriginalPositions = frame.positions;
state.frameOriginalNormals = frame.normals;
state.framePositions = frame.positions;
state.frameNormals = frame.normals;
state.frameRemovedTriangles = 0;
state.frameWelds = null;
prepareFrameWelds();
}
function removeDetachedPivotParts(mesh) {
var triangleCount = mesh.count;
if (triangleCount < 2)
return {
...mesh,
removedTriangles: 0,
componentCount: 1
};
var parent = new Int32Array(triangleCount);
var rank = new Uint8Array(triangleCount);
for (let triangle = 0; triangle < triangleCount; triangle++)
parent[triangle] = triangle;
var find = (item) => {
var root = item;
while (parent[root] !== root)
root = parent[root];
while (parent[item] !== item) {
var next = parent[item];
parent[item] = root;
item = next;
}
return root;
}
;
var union = (a, b) => {
var rootA = find(a)
, rootB = find(b);
if (rootA === rootB)
return;
if (rank[rootA] < rank[rootB])
[rootA,rootB] = [rootB, rootA];
parent[rootB] = rootA;
if (rank[rootA] === rank[rootB])
rank[rootA]++;
}
;
var vertexIds = new Int32Array(triangleCount * 3);
var vertexTable = new Map();
var vertexCount = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
var triangleOffset = triangle * 9;
for (let vertex = 0; vertex < 3; vertex++) {
var offset = triangleOffset + vertex * 3;
var key = `${Math.round(mesh.positions[offset] * 10000)},${Math.round(mesh.positions[offset + 1] * 10000)},${Math.round(mesh.positions[offset + 2] * 10000)}`;
var vertexId = vertexTable.get(key);
if (vertexId === undefined) {
vertexId = vertexCount++;
vertexTable.set(key, vertexId);
}
vertexIds[triangle * 3 + vertex] = vertexId;
}
}
var edgeOwners = new Map();
for (let triangle = 0; triangle < triangleCount; triangle++) {
var offset = triangle * 3;
var ids = [vertexIds[offset], vertexIds[offset + 1], vertexIds[offset + 2]];
for (const [aIndex,bIndex] of [[0, 1], [1, 2], [2, 0]]) {
var a = ids[aIndex]
, b = ids[bIndex];
var key = a < b ? `${a}:${b}` : `${b}:${a}`;
var owner = edgeOwners.get(key);
if (owner === undefined)
edgeOwners.set(key, triangle);
else
union(triangle, owner);
}
}
var componentSizes = new Map();
var largestRoot = 0;
var largestSize = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
var root = find(triangle);
var size = (componentSizes.get(root) || 0) + 1;
componentSizes.set(root, size);
if (size > largestSize) {
largestSize = size;
largestRoot = root;
}
}
if (componentSizes.size === 1)
return {
...mesh,
removedTriangles: 0,
componentCount: 1
};
var positions = new Float32Array(largestSize * 9);
var normals = new Float32Array(largestSize * 9);
var outputOffset = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
if (find(triangle) !== largestRoot)
continue;
var inputOffset = triangle * 9;
positions.set(mesh.positions.subarray(inputOffset, inputOffset + 9), outputOffset);
normals.set(mesh.normals.subarray(inputOffset, inputOffset + 9), outputOffset);
outputOffset += 9;
}
return {
positions,
normals,
count: largestSize,
removedTriangles: triangleCount - largestSize,
componentCount: componentSizes.size
};
}
function triangleNormal(a, b, c) {
var ux = b[0] - a[0]
, uy = b[1] - a[1]
, uz = b[2] - a[2];
var vx = c[0] - a[0]
, vy = c[1] - a[1]
, vz = c[2] - a[2];
var nx = uy * vz - uz * vy;
var ny = uz * vx - ux * vz;
var nz = ux * vy - uy * vx;
var len = Math.hypot(nx, ny, nz) || 1;
return [nx / len, ny / len, nz / len];
}
function createMeshWriter(triangleCapacity) {
return {
positions: new Float32Array(triangleCapacity * 9),
normals: new Float32Array(triangleCapacity * 9),
offset: 0
};
}
function pushTri(mesh, a, b, c) {
var n = triangleNormal(a, b, c);
var offset = mesh.offset;
mesh.positions[offset] = a[0];
mesh.positions[offset + 1] = a[1];
mesh.positions[offset + 2] = a[2];
mesh.positions[offset + 3] = b[0];
mesh.positions[offset + 4] = b[1];
mesh.positions[offset + 5] = b[2];
mesh.positions[offset + 6] = c[0];
mesh.positions[offset + 7] = c[1];
mesh.positions[offset + 8] = c[2];
for (let vertex = 0; vertex < 3; vertex++) {
mesh.normals[offset] = n[0];
mesh.normals[offset + 1] = n[1];
mesh.normals[offset + 2] = n[2];
offset += 3;
}
mesh.offset = offset;
}
function pushTriFacing(mesh, a, b, c, upward) {
var normal = triangleNormal(a, b, c);
var facesUp = normal[2] >= 0;
if (facesUp === upward)
pushTri(mesh, a, b, c);
else
pushTri(mesh, a, c, b);
}
function triangulatePairPolygon(pairs) {
var cross2D = (a, b, c) => {
var pa = a.bottom
, pb = b.bottom
, pc = c.bottom;
return (pb[0] - pa[0]) * (pc[1] - pa[1]) - (pb[1] - pa[1]) * (pc[0] - pa[0]);
}
;
var signedArea = pairs.reduce( (sum, pair, index) => {
var next = pairs[(index + 1) % pairs.length];
return sum + pair.bottom[0] * next.bottom[1] - next.bottom[0] * pair.bottom[1];
}
, 0);
var indices = pairs.map( (pair, index) => index);
if (signedArea < 0)
indices.reverse();
var triangles = [];
var pointInside = (point, a, b, c) => {
var c1 = cross2D(a, b, point);
var c2 = cross2D(b, c, point);
var c3 = cross2D(c, a, point);
return c1 >= -1e-8 && c2 >= -1e-8 && c3 >= -1e-8;
}
;
while (indices.length > 3) {
var clipped = false;
for (let position = 0; position < indices.length; position++) {
var previous = indices[(position + indices.length - 1) % indices.length];
var current = indices[position];
var next = indices[(position + 1) % indices.length];
if (cross2D(pairs[previous], pairs[current], pairs[next]) <= 1e-8)
continue;
var containsPoint = indices.some( (candidate) => candidate !== previous && candidate !== current && candidate !== next && pointInside(pairs[candidate], pairs[previous], pairs[current], pairs[next]));
if (containsPoint)
continue;
triangles.push([pairs[previous], pairs[current], pairs[next]]);
indices.splice(position, 1);
clipped = true;
break;
}
if (!clipped)
throw new Error('扇叶根部轮廓三角化失败');
}
triangles.push(indices.map( (index) => pairs[index]));
return triangles;
}
function prepareFrameWelds() {
if (!state.framePositions || state.frameWelds)
return;
var positions = state.framePositions;
var normals = state.frameNormals;
var used = new Set();
var welds = [];
for (let bladeIndex = 0; bladeIndex < FAN.angles.length; bladeIndex++) {
var target = rotatePoint(-(FAN.rootTipRadius + 1.3), 0, FAN.angles[bladeIndex]);
var bestTriangle = -1;
var bestScore = Infinity;
for (let triangle = 0; triangle < positions.length; triangle += 9) {
var triangleIndex = triangle / 9;
if (used.has(triangleIndex))
continue;
var z0 = positions[triangle + 2]
, z1 = positions[triangle + 5]
, z2 = positions[triangle + 8];
if (Math.abs(z0 - .1) > .0005 || Math.abs(z1 - .1) > .0005 || Math.abs(z2 - .1) > .0005)
continue;
var cx = (positions[triangle] + positions[triangle + 3] + positions[triangle + 6]) / 3;
var cy = (positions[triangle + 1] + positions[triangle + 4] + positions[triangle + 7]) / 3;
var score = (cx - target[0]) ** 2 + (cy - target[1]) ** 2;
if (score < bestScore) {
bestScore = score;
bestTriangle = triangleIndex;
}
}
if (bestTriangle < 0)
continue;
used.add(bestTriangle);
var offset = bestTriangle * 9;
welds[bladeIndex] = [[positions[offset], positions[offset + 1], positions[offset + 2]], [positions[offset + 3], positions[offset + 4], positions[offset + 5]], [positions[offset + 6], positions[offset + 7], positions[offset + 8]]];
}
var filteredPositions = [];
var filteredNormals = [];
for (let triangle = 0; triangle < positions.length; triangle += 9) {
if (used.has(triangle / 9))
continue;
for (let index = 0; index < 9; index++) {
filteredPositions.push(positions[triangle + index]);
filteredNormals.push(normals[triangle + index]);
}
}
state.framePositions = new Float32Array(filteredPositions);
state.frameNormals = new Float32Array(filteredNormals);
state.frameWelds = welds;
}
function matchTriangleLoop(frameLoop, reliefLoop) {
var permutations = [[0, 1, 2], [1, 2, 0], [2, 0, 1], [0, 2, 1], [2, 1, 0], [1, 0, 2]];
var best = permutations[0];
var bestScore = Infinity;
for (const permutation of permutations) {
var score = 0;
for (let index = 0; index < 3; index++) {
var frame = frameLoop[index];
var relief = reliefLoop[permutation[index]];
score += (frame[0] - relief[0]) ** 2 + (frame[1] - relief[1]) ** 2 + (frame[2] - relief[2]) ** 2;
}
if (score < bestScore) {
bestScore = score;
best = permutation;
}
}
return best.map( (index) => reliefLoop[index]);
}
+180
View File
@@ -0,0 +1,180 @@
function makeDemoImage() {
var canvas = document.createElement('canvas');
canvas.width = 1000;
canvas.height = 720;
var ctx = canvas.getContext('2d');
var sky = ctx.createLinearGradient(0, 0, 0, canvas.height);
sky.addColorStop(0, '#f7f3e7');
sky.addColorStop(.62, '#cdded8');
sky.addColorStop(1, '#7fa5a3');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(33,70,74,.15)';
for (let i = 0; i < 6; i++) {
ctx.beginPath();
ctx.ellipse(160 + i * 175, 164 + (i % 2) * 42, 126, 28, -.08, 0, Math.PI * 2);
ctx.fill();
}
var ridges = [{
y: 420,
color: '#7b9995',
amp: 130,
step: 145
}, {
y: 510,
color: '#496f72',
amp: 170,
step: 125
}, {
y: 610,
color: '#193f48',
amp: 190,
step: 112
}];
ridges.forEach( (ridge, layer) => {
ctx.beginPath();
ctx.moveTo(0, canvas.height);
ctx.lineTo(0, ridge.y);
for (let x = 0; x <= canvas.width + ridge.step; x += ridge.step) {
var peak = ridge.y - ridge.amp * (.54 + .46 * Math.sin(x * .014 + layer));
ctx.quadraticCurveTo(x + ridge.step * .45, peak, x + ridge.step, ridge.y + Math.sin(x * .021) * 22);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.closePath();
ctx.fillStyle = ridge.color;
ctx.globalAlpha = .44 + layer * .16;
ctx.fill();
}
);
ctx.globalAlpha = 1;
ctx.strokeStyle = '#163d45';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(742, 620);
ctx.bezierCurveTo(716, 476, 760, 340, 700, 192);
ctx.stroke();
ctx.lineWidth = 3;
for (let i = 0; i < 13; i++) {
var y = 230 + i * 25;
var side = i % 2 ? -1 : 1;
ctx.beginPath();
ctx.moveTo(716 + Math.sin(i) * 16, y);
ctx.quadraticCurveTo(790 + side * 18, y - 32, 842 + side * 44, y - 45);
ctx.stroke();
}
ctx.fillStyle = '#244f55';
for (let i = 0; i < 42; i++) {
var x = 690 + Math.sin(i * 2.1) * (55 + i % 7 * 9);
var y = 205 + (i * 17) % 310;
ctx.beginPath();
ctx.ellipse(x, y, 13, 5, (i % 5) * .42, 0, Math.PI * 2);
ctx.fill();
}
var img = new Image();
img.src = canvas.toDataURL('image/png');
return img;
}
function imageLuminance(image, u, v) {
if (!image)
return 1;
var px = clamp(u * (image.width - 1), 0, image.width - 1);
var py = clamp(v * (image.height - 1), 0, image.height - 1);
var x0 = Math.floor(px)
, y0 = Math.floor(py);
var x1 = Math.min(x0 + 1, image.width - 1)
, y1 = Math.min(y0 + 1, image.height - 1);
var tx = px - x0
, ty = py - y0;
var sample = (x, y) => {
var i = (y * image.width + x) * 4;
return (image.data[i] * .2126 + image.data[i + 1] * .7152 + image.data[i + 2] * .0722) / 255;
}
;
var top = sample(x0, y0) * (1 - tx) + sample(x1, y0) * tx;
var bottom = sample(x0, y1) * (1 - tx) + sample(x1, y1) * tx;
var lum = top * (1 - ty) + bottom * ty;
var contrast = Number(ui.contrast.value) / 100;
lum = clamp((lum - .5) * contrast + .5, 0, 1);
if (ui.invert.checked)
lum = 1 - lum;
return lum;
}
function sampleLuminance(x, y) {
var bounds = modelBounds();
var u = clamp((y - bounds.yMin) / (bounds.yMax - bounds.yMin), 0, 1);
var v = clamp((x - bounds.xMin) / (bounds.xMax - bounds.xMin), 0, 1);
return imageLuminance(state.sourcePixels, u, v);
}
function sampleBladeLuminance(bladeIndex, radius, cross) {
var pixels = state.bladeConfigs[bladeIndex].pixels;
if (!pixels)
return 1;
var u = clamp((radius - FAN.rMin) / (FAN.rMax - FAN.rMin), 0, 1);
var v = clamp((1 - cross) / 2, 0, 1);
return imageLuminance(pixels, u, v);
}
function readImageFile(file) {
return new Promise( (resolve, reject) => {
var reader = new FileReader();
reader.onload = () => {
var img = new Image();
img.onload = () => resolve({
file,
img
});
img.onerror = () => reject(new Error(`${file.name} 无法读取`));
img.src = reader.result;
}
;
reader.onerror = () => reject(new Error(`${file.name} 无法读取`));
reader.readAsDataURL(file);
}
);
}
async function loadFiles(fileList) {
var files = [...(fileList || [])].filter( (file) => file.type.startsWith('image/'));
if (!files.length) {
ui.status.textContent = '请选择 JPG、PNG 或 WEBP 图片。';
return;
}
try {
showBusy(true);
if (state.imageMode === 'whole') {
var loaded = await readImageFile(files[0]);
state.globalConfig.image = loaded.img;
state.globalConfig.name = loaded.file.name;
state.globalConfig.isDemo = false;
ui.status.textContent = '整扇图片已载入,正在生成浮雕。';
} else {
var available = FAN.angles.length - state.selectedBlade;
var loaded = await Promise.all(files.slice(0, available).map(readImageFile));
loaded.forEach( ({file, img}, offset) => {
var config = state.bladeConfigs[state.selectedBlade + offset];
config.image = img;
config.name = file.name;
config.isDemo = false;
}
);
ui.status.textContent = `已从第 ${state.selectedBlade + 1} 片开始载入 ${loaded.length} 张图片。`;
}
ui.fileInput.value = '';
applyConfigToControls();
scheduleRebuild({
redrawImage: true
});
} catch (error) {
console.error(error);
ui.status.textContent = `图片载入失败:${error.message}`;
showBusy(false);
}
}
+58
View File
@@ -0,0 +1,58 @@
function initialize() {
FAN.angles = anglesForBladeCount(state.bladeCount);
syncActiveBladeConfigs();
updateActiveBounds();
renderBladeButtons();
updateOutput(ui.imageScale, '%');
updateOutput(ui.offsetX, '%');
updateOutput(ui.offsetY, '%');
updateOutput(ui.imageRotation, '°');
updateOutput(ui.brushSize, ' px');
updateOutput(ui.textSize, ' px');
updateOutput(ui.textArc, '%');
updateOutput(ui.polygonSides, ' 边');
updateOutput(ui.splineTension, '%');
updateOutput(ui.minThickness);
updateOutput(ui.maxThickness);
updateOutput(ui.contrast, '%');
updateOutput(ui.reliefMargin, ' mm');
updateOutput(ui.reliefGamma);
updateOutput(ui.blur, ' px');
updatePrecisionGuide();
update3MFStructureHint();
updateVariantUi();
setTextFont(state.textFont);
setTextStyle(state.textStyle);
setTextArc(state.textArc);
setEditorTool('move');
bindEvents();
try {
initGL();
} catch (error) {
console.error(error);
ui.status.textContent = error.message;
}
var demo = makeDemoImage();
demo.onload = () => {
state.globalConfig.image = demo;
state.globalConfig.isDemo = true;
applyConfigToControls();
drawSource();
window.setTimeout( () => {
try {
loadFrameVariant(state.fanVariant);
buildRelief();
} catch (error) {
console.error(error);
ui.status.textContent = `骨架载入失败:${error.message}`;
} finally {
showBusy(false);
}
}
, 30);
}
;
}
initialize();
+927
View File
@@ -0,0 +1,927 @@
function updateVariantUi() {
[...ui.fanVariants.querySelectorAll('[data-variant]')].forEach( (button) => {
var active = button.dataset.variant === state.fanVariant;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
}
);
[...ui.bladeCountOptions.querySelectorAll('[data-blade-count]')].forEach( (button) => {
var active = Number(button.dataset.bladeCount) === state.bladeCount;
button.classList.toggle('active', active);
button.setAttribute('aria-checked', String(active));
}
);
var spread = (state.bladeCount - 1) * BLADE_ANGLE_STEP;
var bounds = modelBounds();
ui.variantSeal.textContent = FAN.seal;
ui.modelTag.textContent = `实时预览 · ${FAN.name} ${FAN.angles.length} 骨`;
ui.metricSize.textContent = `${Math.round(bounds.xMax - bounds.xMin)} × ${Math.round(bounds.yMax - bounds.yMin)}`;
ui.bladeCountSummary.textContent = `${state.bladeCount}片 · 展开${spread}°`;
ui.bladeOrderHint.textContent = `1 右侧 · ${state.bladeCount} 左侧`;
ui.frameToggleLabel.textContent = `包含「${FAN.name}」${state.bladeCount}片骨架(不含转轴),并将浮雕焊接到扇叶`;
}
function loadFrameVariant(key) {
var variant = FAN_VARIANTS[key];
if (!variant)
throw new Error('未找到所选扇形');
FAN = variant;
FAN.angles = anglesForBladeCount(state.bladeCount);
state.fanVariant = key;
state.maskLookup = null;
state.frameWelds = null;
updateActiveBounds();
var templateAssets = FRAME_BLADE_STL_BASE64[key];
state.frameTemplates = {
left: parseBinarySTL(templateAssets.left),
inner: parseBinarySTL(templateAssets.inner),
right: parseBinarySTL(templateAssets.right)
};
rebuildFrameFromTemplates();
updateVariantUi();
}
function setFanVariant(key) {
if (key === state.fanVariant || !FAN_VARIANTS[key])
return;
hideEditorPointerPreviews();
showBusy(true, '正在切换扇形骨架');
window.setTimeout( () => {
try {
loadFrameVariant(key);
updatePrecisionGuide();
drawSource();
buildRelief();
ui.status.textContent = `已切换为${FAN.name}(${FAN.shape}),图片和编辑内容已保留。`;
} catch (error) {
console.error(error);
ui.status.textContent = `扇形切换失败:${error.message}`;
} finally {
showBusy(false);
}
}
, 20);
}
function renderBladeButtons() {
ui.bladeGrid.replaceChildren();
FAN.angles.forEach( (angle, index) => {
var button = document.createElement('button');
button.type = 'button';
button.className = 'blade-button';
button.textContent = String(index + 1);
button.addEventListener('click', () => selectBlade(index));
ui.bladeGrid.appendChild(button);
}
);
updateBladeButtons();
}
function setBladeCount(nextCount) {
var count = Number(nextCount);
if (!SUPPORTED_BLADE_COUNTS.includes(count) || count === state.bladeCount)
return;
hideEditorPointerPreviews();
syncActiveConfig();
var previousAngle = FAN.angles[state.selectedBlade] ?? 0;
showBusy(true, `正在调整为 ${count} 片扇叶`);
window.setTimeout( () => {
try {
state.bladeCount = count;
FAN.angles = anglesForBladeCount(count);
syncActiveBladeConfigs();
state.selectedBlade = FAN.angles.reduce( (best, angle, index) => Math.abs(angle - previousAngle) < Math.abs(FAN.angles[best] - previousAngle) ? index : best, 0);
state.maskLookup = null;
updateActiveBounds();
if (state.frameTemplates)
rebuildFrameFromTemplates();
renderBladeButtons();
updateVariantUi();
updatePrecisionGuide();
applyConfigToControls();
drawSource();
buildRelief();
update3MFStructureHint();
ui.status.textContent = `已切换为 ${count} 片扇叶 · 展开 ${(count - 1) * BLADE_ANGLE_STEP}° · 图片和编辑内容已按中心位置保留。`;
} catch (error) {
console.error(error);
ui.status.textContent = `扇叶数量切换失败:${error.message}`;
} finally {
showBusy(false);
}
}
, 20);
}
function syncActiveConfig() {
var config = activeConfig();
config.scale = Number(ui.imageScale.value);
config.offsetX = Number(ui.offsetX.value);
config.offsetY = Number(ui.offsetY.value);
config.rotation = Number(ui.imageRotation.value);
}
function renderConfig(config, forceEditorLayer=false) {
var canvas = config.canvas;
var ctx = canvas.getContext('2d', {
willReadFrequently: true
});
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (config.image && config.image.complete) {
var img = config.image;
var rotation = ((config.rotation % 360) + 360) % 360;
var radians = deg(rotation);
var iw = Math.abs(img.naturalWidth * Math.cos(radians)) + Math.abs(img.naturalHeight * Math.sin(radians));
var ih = Math.abs(img.naturalWidth * Math.sin(radians)) + Math.abs(img.naturalHeight * Math.cos(radians));
var baseScale = config.fit === 'cover' ? Math.max(canvas.width / iw, canvas.height / ih) : Math.min(canvas.width / iw, canvas.height / ih);
var scale = baseScale * config.scale / 100;
var ox = config.offsetX / 100 * canvas.width;
var oy = config.offsetY / 100 * canvas.height;
ctx.translate(canvas.width / 2 + ox, canvas.height / 2 + oy);
ctx.rotate(deg(rotation));
ctx.filter = `blur(${Number(ui.blur.value)}px)`;
ctx.drawImage(img, -img.naturalWidth * scale / 2, -img.naturalHeight * scale / 2, img.naturalWidth * scale, img.naturalHeight * scale);
}
ctx.restore();
if (config.editDirty || forceEditorLayer)
ctx.drawImage(config.editCanvas, 0, 0, canvas.width, canvas.height);
config.pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
}
function update3MFStructureHint() {
var structure = ui.threeMFStructure.value;
var partCount = FAN.angles.length + (ui.includeFrame.checked ? 1 : 0);
if (structure === 'parts') {
ui.threeMFHint.textContent = `1 个总成内含 ${partCount} 个命名零件,相对位置锁定`;
} else if (structure === 'objects') {
ui.threeMFHint.textContent = `${partCount} 个独立对象,可在切片软件中逐件选择`;
} else {
ui.threeMFHint.textContent = '焊接为 1 个完整网格,适合直接切片';
}
}
function updatePrecisionGuide() {
var detail = DETAIL[ui.detail.value];
var maxHalf = Math.max(...FAN.profile.map( (point) => point[1])) - currentReliefMargin();
var radialStep = (FAN.rMax - FAN.rMin) / (detail.radial - 1);
var acrossStep = 2 * maxHalf / (detail.across - 1);
ui.precisionHint.textContent = `采样间距约 ${radialStep.toFixed(2)} × ${acrossStep.toFixed(2)} mm`;
ui.precisionGrid.textContent = `${detail.radial} × ${detail.across} / 片`;
}
function showBusy(show, label='正在重建浮雕') {
ui.busy.classList.toggle('show', show);
ui.exportBtn.disabled = show;
ui.export3mfBtn.disabled = show;
if (show)
ui.busyText.textContent = label;
}
function scheduleRebuild({redrawImage=false}={}) {
window.clearTimeout(state.rebuildTimer);
if (redrawImage) {
try {
drawSource();
} catch (error) {
console.error(error);
}
} else {
showBusy(true);
}
state.rebuildTimer = window.setTimeout( () => {
try {
if (redrawImage)
showBusy(true);
buildRelief();
} catch (error) {
console.error(error);
ui.status.textContent = `生成失败:${error.message}`;
} finally {
showBusy(false);
}
}
, 90);
}
function updateOutput(input, suffix='') {
var output = document.querySelector(`output[for="${input.id}"]`);
if (output)
output.textContent = `${input.value}${suffix}`;
}
function setImageControl(input, value) {
var min = Number(input.min);
var max = Number(input.max);
input.value = String(clamp(value, min, max));
updateOutput(input, '%');
updateFloatingTransformStatus();
}
function normalizeRotation(value) {
var normalized = ((value + 180) % 360 + 360) % 360 - 180;
return normalized === -180 && value > 0 ? 180 : normalized;
}
function setRotationControl(value) {
var rotation = normalizeRotation(value);
ui.imageRotation.value = String(rotation);
activeConfig().rotation = rotation;
updateOutput(ui.imageRotation, '°');
updateFloatingTransformStatus();
}
function resetImageTransform() {
var config = activeConfig();
config.rotation = 0;
config.scale = 100;
config.offsetX = 0;
config.offsetY = 0;
setImageControl(ui.imageScale, 100);
setImageControl(ui.offsetX, 0);
setImageControl(ui.offsetY, 0);
setRotationControl(0);
scheduleRebuild({
redrawImage: true
});
}
function bindRange(input, suffix, redrawImage) {
input.addEventListener('input', () => {
updateOutput(input, suffix);
if (redrawImage)
syncActiveConfig();
scheduleRebuild({
redrawImage
});
}
);
}
function setFit(mode) {
activeConfig().fit = mode;
ui.coverBtn.classList.toggle('active', mode === 'cover');
ui.containBtn.classList.toggle('active', mode === 'contain');
scheduleRebuild({
redrawImage: true
});
}
function updateBladeButtons() {
[...ui.bladeGrid.children].forEach( (button, index) => {
button.classList.toggle('active', index === state.selectedBlade);
button.classList.toggle('assigned', Boolean(state.bladeConfigs[index].image));
var name = state.bladeConfigs[index].name || '尚未设置图片';
button.title = `叶片 ${index + 1}:${name}`;
button.setAttribute('aria-pressed', String(index === state.selectedBlade));
}
);
}
function updateImageUi() {
if (state.imageMode === 'whole') {
var config = state.globalConfig;
ui.coverBtn.textContent = '铺满扇面';
ui.containBtn.textContent = '完整图片';
ui.dropTitle.textContent = '选择或拖入一张图片';
ui.dropHint.textContent = '页面全程离线,图片不会上传';
if (config.image && !config.isDemo) {
ui.fileChip.textContent = `${config.name} · ${config.image.naturalWidth} × ${config.image.naturalHeight}px`;
ui.fileChip.classList.add('show');
ui.demoLabel.style.display = 'none';
} else {
ui.fileChip.classList.remove('show');
ui.demoLabel.textContent = '内置示例';
ui.demoLabel.style.display = '';
}
} else {
var config = activeConfig();
var assigned = state.bladeConfigs.filter( (item) => item.image).length;
ui.coverBtn.textContent = '铺满叶片';
ui.containBtn.textContent = '完整图片';
ui.dropTitle.textContent = `为第 ${state.selectedBlade + 1} 片选择图片`;
ui.dropHint.textContent = '可多选图片,将按叶片顺序自动填入';
ui.fileChip.textContent = config.image ? `叶片 ${state.selectedBlade + 1} · ${config.name} · ${config.image.naturalWidth} × ${config.image.naturalHeight}px` : `叶片 ${state.selectedBlade + 1} 尚未设置 · 已完成 ${assigned} / ${FAN.angles.length} 片`;
ui.fileChip.classList.add('show');
ui.demoLabel.textContent = `叶片 ${state.selectedBlade + 1}`;
ui.demoLabel.style.display = '';
}
updateBladeButtons();
}
function applyConfigToControls() {
var config = activeConfig();
setImageControl(ui.imageScale, config.scale);
setImageControl(ui.offsetX, config.offsetX);
setImageControl(ui.offsetY, config.offsetY);
setRotationControl(config.rotation);
ui.coverBtn.classList.toggle('active', config.fit === 'cover');
ui.containBtn.classList.toggle('active', config.fit === 'contain');
updateImageUi();
}
function selectBlade(index) {
hideEditorPointerPreviews();
syncActiveConfig();
state.selectedBlade = clamp(index, 0, FAN.angles.length - 1);
applyConfigToControls();
drawSource();
updateSelectionUi();
ui.status.textContent = `正在编辑第 ${state.selectedBlade + 1} 片叶片,可单独缩放、移动和旋转。`;
}
function setImageMode(mode) {
if (mode === state.imageMode)
return;
hideEditorPointerPreviews();
syncActiveConfig();
state.imageMode = mode;
ui.wholeModeBtn.classList.toggle('active', mode === 'whole');
ui.bladeModeBtn.classList.toggle('active', mode === 'blade');
ui.bladePanel.hidden = mode === 'whole';
ui.fileInput.multiple = mode === 'blade';
applyConfigToControls();
updateSelectionUi();
scheduleRebuild({
redrawImage: true
});
}
function readImageFile(file) {
return new Promise( (resolve, reject) => {
var reader = new FileReader();
reader.onload = () => {
var img = new Image();
img.onload = () => resolve({
file,
img
});
img.onerror = () => reject(new Error(`${file.name} 无法读取`));
img.src = reader.result;
}
;
reader.onerror = () => reject(new Error(`${file.name} 无法读取`));
reader.readAsDataURL(file);
}
);
}
async function loadFiles(fileList) {
var files = [...(fileList || [])].filter( (file) => file.type.startsWith('image/'));
if (!files.length) {
ui.status.textContent = '请选择 JPG、PNG 或 WEBP 图片。';
return;
}
try {
showBusy(true);
if (state.imageMode === 'whole') {
var loaded = await readImageFile(files[0]);
state.globalConfig.image = loaded.img;
state.globalConfig.name = loaded.file.name;
state.globalConfig.isDemo = false;
ui.status.textContent = '整扇图片已载入,正在生成浮雕。';
} else {
var available = FAN.angles.length - state.selectedBlade;
var loaded = await Promise.all(files.slice(0, available).map(readImageFile));
loaded.forEach( ({file, img}, offset) => {
var config = state.bladeConfigs[state.selectedBlade + offset];
config.image = img;
config.name = file.name;
config.isDemo = false;
}
);
ui.status.textContent = `已从第 ${state.selectedBlade + 1} 片开始载入 ${loaded.length} 张图片。`;
}
ui.fileInput.value = '';
applyConfigToControls();
scheduleRebuild({
redrawImage: true
});
} catch (error) {
console.error(error);
ui.status.textContent = `图片载入失败:${error.message}`;
showBusy(false);
}
}
function resetAll() {
[state.globalConfig, ...state.bladeConfigSlots].forEach( (config) => {
config.fit = 'cover';
config.rotation = 0;
config.scale = 100;
config.offsetX = 0;
config.offsetY = 0;
}
);
var values = {
imageScale: 100,
offsetX: 0,
offsetY: 0,
minThickness: .60,
maxThickness: 2.20,
reliefMargin: 0,
contrast: 135,
reliefGamma: .95,
blur: .5,
detail: 'ultra'
};
Object.entries(values).forEach( ([key,value]) => {
ui[key].value = value;
}
);
ui.invert.checked = false;
ui.includeFrame.checked = true;
ui.threeMFStructure.value = 'merged';
applyConfigToControls();
updateOutput(ui.imageScale, '%');
updateOutput(ui.offsetX, '%');
updateOutput(ui.offsetY, '%');
updateOutput(ui.minThickness);
updateOutput(ui.maxThickness);
updateOutput(ui.contrast, '%');
updateOutput(ui.reliefMargin, ' mm');
updateOutput(ui.reliefGamma);
updateOutput(ui.blur, ' px');
updatePrecisionGuide();
update3MFStructureHint();
scheduleRebuild({
redrawImage: true
});
}
function bindEvents() {
ui.fanVariants.addEventListener('click', (event) => {
var button = event.target.closest('[data-variant]');
if (button)
setFanVariant(button.dataset.variant);
}
);
ui.bladeCountOptions.addEventListener('click', (event) => {
var button = event.target.closest('[data-blade-count]');
if (button)
setBladeCount(button.dataset.bladeCount);
}
);
ui.wholeModeBtn.addEventListener('click', () => setImageMode('whole'));
ui.bladeModeBtn.addEventListener('click', () => setImageMode('blade'));
ui.fileInput.addEventListener('change', () => loadFiles(ui.fileInput.files));
['dragenter', 'dragover'].forEach( (name) => ui.dropzone.addEventListener(name, (event) => {
event.preventDefault();
ui.dropzone.classList.add('is-dragging');
}
));
['dragleave', 'drop'].forEach( (name) => ui.dropzone.addEventListener(name, (event) => {
event.preventDefault();
ui.dropzone.classList.remove('is-dragging');
}
));
ui.dropzone.addEventListener('drop', (event) => loadFiles(event.dataTransfer.files));
ui.coverBtn.addEventListener('click', () => setFit('cover'));
ui.containBtn.addEventListener('click', () => setFit('contain'));
ui.rotateLeft.addEventListener('click', () => {
setRotationControl(Number(ui.imageRotation.value) - 90);
scheduleRebuild({
redrawImage: true
});
}
);
ui.rotateRight.addEventListener('click', () => {
setRotationControl(Number(ui.imageRotation.value) + 90);
scheduleRebuild({
redrawImage: true
});
}
);
ui.zoomOut.addEventListener('click', () => {
setImageControl(ui.imageScale, Number(ui.imageScale.value) - 10);
scheduleRebuild({
redrawImage: true
});
}
);
ui.zoomIn.addEventListener('click', () => {
setImageControl(ui.imageScale, Number(ui.imageScale.value) + 10);
scheduleRebuild({
redrawImage: true
});
}
);
ui.resetImage.addEventListener('click', resetImageTransform);
document.querySelectorAll('[data-editor-tool]').forEach( (button) => {
button.addEventListener('click', () => setEditorTool(button.dataset.editorTool));
}
);
document.querySelectorAll('[data-shape-ratio]').forEach( (button) => {
button.addEventListener('click', () => setShapeRatioMode(button.dataset.shapeRatio));
}
);
ui.undoEdit.addEventListener('click', undoEditorChange);
ui.clearEdit.addEventListener('click', clearEditorChanges);
ui.deleteSelected.addEventListener('click', deleteSelectedObject);
ui.brushSize.addEventListener('input', () => {
updateOutput(ui.brushSize, ' px');
ui.floatBrushSize.value = ui.brushSize.value;
ui.floatBrushSizeValue.textContent = ui.brushSize.value;
refreshBrushCursorSize();
refreshEditorPointerPreview();
}
);
ui.textSize.addEventListener('input', () => {
updateOutput(ui.textSize, ' px');
ui.floatTextSize.value = ui.textSize.value;
ui.floatTextSizeValue.textContent = ui.textSize.value;
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.textValue.addEventListener('input', () => {
ui.floatTextValue.value = ui.textValue.value;
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.textFontChips.querySelectorAll('[data-text-font]').forEach( (button) => {
button.addEventListener('click', () => setTextFont(button.dataset.textFont));
}
);
ui.textStyle.addEventListener('change', () => setTextStyle(ui.textStyle.value));
ui.textArc.addEventListener('input', () => setTextArc(ui.textArc.value));
ui.polygonSides.addEventListener('input', () => {
updateOutput(ui.polygonSides, ' 边');
ui.floatPolygonSides.value = ui.polygonSides.value;
ui.floatPolygonSidesValue.textContent = ui.polygonSides.value;
}
);
ui.splineTension.addEventListener('input', () => {
updateOutput(ui.splineTension, '%');
ui.floatSplineTension.value = ui.splineTension.value;
ui.floatSplineTensionValue.textContent = `${ui.splineTension.value}%`;
}
);
ui.editorColor.addEventListener('input', () => {
ui.floatEditorColor.value = ui.editorColor.value;
refreshBrushCursorSize();
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.floatBrushSize.addEventListener('input', () => {
ui.brushSize.value = ui.floatBrushSize.value;
ui.floatBrushSizeValue.textContent = ui.floatBrushSize.value;
updateOutput(ui.brushSize, ' px');
refreshBrushCursorSize();
refreshEditorPointerPreview();
}
);
ui.floatTextValue.addEventListener('input', () => {
ui.textValue.value = ui.floatTextValue.value;
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.floatTextFont.addEventListener('change', () => setTextFont(ui.floatTextFont.value));
ui.floatTextStyle.addEventListener('change', () => setTextStyle(ui.floatTextStyle.value));
ui.floatTextSize.addEventListener('input', () => {
ui.textSize.value = ui.floatTextSize.value;
ui.floatTextSizeValue.textContent = ui.floatTextSize.value;
updateOutput(ui.textSize, ' px');
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.floatTextArc.addEventListener('input', () => setTextArc(ui.floatTextArc.value));
ui.floatPolygonSides.addEventListener('input', () => {
ui.polygonSides.value = ui.floatPolygonSides.value;
ui.floatPolygonSidesValue.textContent = ui.floatPolygonSides.value;
updateOutput(ui.polygonSides, ' 边');
}
);
ui.floatSplineTension.addEventListener('input', () => {
ui.splineTension.value = ui.floatSplineTension.value;
ui.floatSplineTensionValue.textContent = `${ui.floatSplineTension.value}%`;
updateOutput(ui.splineTension, '%');
}
);
ui.floatShapeRatio.addEventListener('change', () => setShapeRatioMode(ui.floatShapeRatio.value));
ui.floatEditorColor.addEventListener('input', () => {
ui.editorColor.value = ui.floatEditorColor.value;
refreshBrushCursorSize();
applyTextControlsToSelected();
refreshEditorPointerPreview();
}
);
ui.floatRotateLeft.addEventListener('click', () => {
setRotationControl(Number(ui.imageRotation.value) - 90);
scheduleRebuild({
redrawImage: true
});
}
);
ui.floatRotateRight.addEventListener('click', () => {
setRotationControl(Number(ui.imageRotation.value) + 90);
scheduleRebuild({
redrawImage: true
});
}
);
ui.floatImageRotation.addEventListener('input', () => {
setRotationControl(Number(ui.floatImageRotation.value));
scheduleRebuild({
redrawImage: true
});
}
);
ui.floatZoomOut.addEventListener('click', () => {
setImageControl(ui.imageScale, Number(ui.imageScale.value) - 10);
scheduleRebuild({
redrawImage: true
});
}
);
ui.floatZoomIn.addEventListener('click', () => {
setImageControl(ui.imageScale, Number(ui.imageScale.value) + 10);
scheduleRebuild({
redrawImage: true
});
}
);
ui.floatResetImage.addEventListener('click', resetImageTransform);
ui.floatUndoEdit.addEventListener('click', undoEditorChange);
ui.floatDeleteSelected.addEventListener('click', deleteSelectedObject);
ui.floatClearEdit.addEventListener('click', clearEditorChanges);
ui.expandMask.addEventListener('click', () => setMaskExpanded(!state.maskExpanded));
ui.maskBackdrop.addEventListener('click', () => setMaskExpanded(false));
window.addEventListener('resize', fitExpandedMask);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && state.maskExpanded)
setMaskExpanded(false);
if ((event.key === 'Delete' || event.key === 'Backspace') && state.editorTool === 'select' && !/INPUT|TEXTAREA|SELECT/.test(event.target.tagName)) {
event.preventDefault();
deleteSelectedObject();
}
}
);
ui.maskCanvas.addEventListener('pointerdown', (event) => {
event.preventDefault();
updateEditorPointerPreview(event);
if (state.editorTool !== 'move') {
var point = state.editorTool === 'text' ? pointerOnTextEditCanvas(event) : pointerOnActiveEditCanvas(event);
if (!point) {
ui.status.textContent = state.imageMode === 'blade' ? `请在第 ${state.selectedBlade + 1} 片叶片的高亮范围内编辑。` : '请在扇面范围内编辑。';
return;
}
var config = activeConfig();
if (state.editorTool === 'select') {
clearEditorPreview(false);
var object = hitTestVectorObject(config, point);
config.selectedObjectId = object ? object.id : null;
if (object) {
pushEditHistory(config);
state.editorObjectDragging = true;
state.editorPointerId = event.pointerId;
state.editorObjectDragStart = point;
state.editorObjectOriginal = JSON.parse(JSON.stringify(object));
state.editorObjectMoved = false;
if (object.type === 'text')
syncTextControlsFromSelected(object);
ui.maskCanvas.setPointerCapture(event.pointerId);
}
setEditorTool('select');
return;
}
pushEditHistory(config);
if (state.editorTool === 'text') {
if (!placeEditorText(config, point))
config.editHistory.pop();
return;
}
state.editorDrawing = true;
state.editorPointerId = event.pointerId;
state.editorLastPoint = point;
if (VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
state.editorShapeStart = point;
state.editorShapePoints = [point];
var preview = createShapeObject(config, state.editorTool, point, point, state.editorShapePoints);
renderEditorPreview(config, preview);
} else {
paintEditorSegment(config, point, point, state.editorTool === 'eraser');
}
ui.maskCanvas.setPointerCapture(event.pointerId);
return;
}
state.imageDragging = true;
state.imagePointerX = event.clientX;
state.imagePointerY = event.clientY;
state.imageStartX = Number(ui.offsetX.value);
state.imageStartY = Number(ui.offsetY.value);
ui.maskWrap.classList.add('is-adjusting');
ui.maskCanvas.setPointerCapture(event.pointerId);
}
);
ui.maskCanvas.addEventListener('pointerenter', updateEditorPointerPreview);
ui.maskCanvas.addEventListener('pointermove', (event) => {
updateEditorPointerPreview(event);
if (state.editorObjectDragging && event.pointerId === state.editorPointerId) {
var point = pointerOnActiveEditCanvas(event);
var config = activeConfig();
var object = selectedVectorObject(config);
if (point && object && state.editorObjectOriginal) {
Object.keys(object).forEach( (key) => delete object[key]);
Object.assign(object, JSON.parse(JSON.stringify(state.editorObjectOriginal)));
var dx = point[0] - state.editorObjectDragStart[0];
var dy = point[1] - state.editorObjectDragStart[1];
translateVectorObject(object, dx, dy);
state.editorObjectMoved = Math.hypot(dx, dy) > .5;
refreshCommittedEditor(config, true);
}
return;
}
if (state.editorDrawing && event.pointerId === state.editorPointerId) {
var point = pointerOnActiveEditCanvas(event);
if (point) {
var config = activeConfig();
if (VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
if (state.editorTool === 'spline') {
var last = state.editorShapePoints[state.editorShapePoints.length - 1];
if (!last || Math.hypot(point[0] - last[0], point[1] - last[1]) > 2)
state.editorShapePoints.push(point);
}
var preview = createShapeObject(config, state.editorTool, state.editorShapeStart, point, state.editorShapePoints);
renderEditorPreview(config, preview);
} else
paintEditorSegment(config, state.editorLastPoint || point, point, state.editorTool === 'eraser');
state.editorLastPoint = point;
} else
state.editorLastPoint = null;
return;
}
if (!state.imageDragging)
return;
var rect = ui.maskCanvas.getBoundingClientRect();
var pointerDX = event.clientX - state.imagePointerX;
var pointerDY = event.clientY - state.imagePointerY;
var dx;
var dy;
if (state.imageMode === 'blade') {
// Convert the screen drag to the selected blade's own radial/across axes.
// Source X runs from root to tip; source Y runs from upper to lower edge.
var bounds = modelBounds();
var modelDX = pointerDY / rect.height * (bounds.xMax - bounds.xMin);
var modelDY = pointerDX / rect.width * (bounds.yMax - bounds.yMin);
var local = rotatePoint(modelDX, modelDY, -FAN.angles[state.selectedBlade]);
var maxHalf = Math.max(...FAN.profile.map( (point) => point[1]));
dx = -local[0] / (FAN.rMax - FAN.rMin) * 100;
dy = -local[1] / (2 * maxHalf) * 100;
} else {
dx = pointerDX / rect.width * 100;
dy = pointerDY / rect.height * 100;
}
setImageControl(ui.offsetX, Math.round(state.imageStartX + dx));
setImageControl(ui.offsetY, Math.round(state.imageStartY + dy));
scheduleRebuild({
redrawImage: true
});
}
);
var stopImageDrag = () => {
var config = activeConfig();
if (state.editorObjectDragging) {
if (!state.editorObjectMoved)
config.editHistory.pop();
state.editorObjectDragging = false;
state.editorPointerId = null;
state.editorObjectDragStart = null;
state.editorObjectOriginal = null;
if (state.editorObjectMoved)
scheduleRebuild();
state.editorObjectMoved = false;
refreshCommittedEditor(config, true);
}
if (state.editorDrawing && VECTOR_SHAPE_TOOLS.includes(state.editorTool)) {
var preview = state.editorPreview?.object;
var start = state.editorShapeStart;
var end = state.editorLastPoint || start;
var valid = preview && (state.editorTool === 'spline' ? (preview.points || []).length > 2 : Math.hypot(end[0] - start[0], end[1] - start[1]) > 3);
state.editorDrawing = false;
state.editorPointerId = null;
state.editorShapeStart = null;
state.editorShapePoints = null;
state.editorLastPoint = null;
if (valid)
commitVectorObject(config, JSON.parse(JSON.stringify(preview)));
else {
config.editHistory.pop();
clearEditorPreview(true);
}
}
state.imageDragging = false;
ui.maskWrap.classList.remove('is-adjusting');
finishEditorStroke();
}
;
ui.maskCanvas.addEventListener('pointerup', stopImageDrag);
ui.maskCanvas.addEventListener('pointercancel', stopImageDrag);
ui.maskCanvas.addEventListener('pointerleave', () => {
if (state.editorTool === 'text')
return;
if (!state.editorDrawing && !state.editorObjectDragging && !state.imageDragging)
hideEditorPointerPreviews();
}
);
ui.maskCanvas.addEventListener('wheel', (event) => {
if (state.editorTool !== 'move')
return;
event.preventDefault();
var factor = Math.exp(-event.deltaY * .0015);
setImageControl(ui.imageScale, Math.round(Number(ui.imageScale.value) * factor));
scheduleRebuild({
redrawImage: true
});
}
, {
passive: false
});
bindRange(ui.imageScale, '%', true);
bindRange(ui.offsetX, '%', true);
bindRange(ui.offsetY, '%', true);
bindRange(ui.imageRotation, '°', true);
bindRange(ui.blur, ' px', true);
bindRange(ui.contrast, '%', false);
bindRange(ui.minThickness, '', false);
bindRange(ui.maxThickness, '', false);
bindRange(ui.reliefGamma, '', false);
ui.reliefMargin.addEventListener('input', () => {
updateOutput(ui.reliefMargin, ' mm');
updatePrecisionGuide();
scheduleRebuild({
redrawImage: true
});
}
);
ui.invert.addEventListener('change', () => scheduleRebuild());
ui.detail.addEventListener('change', () => {
updatePrecisionGuide();
scheduleRebuild();
}
);
ui.threeMFStructure.addEventListener('change', update3MFStructureHint);
ui.includeFrame.addEventListener('change', () => {
update3MFStructureHint();
updateMeshBuffers();
scheduleRebuild();
}
);
ui.resetAll.addEventListener('click', resetAll);
ui.exportBtn.addEventListener('click', writeSTL);
ui.export3mfBtn.addEventListener('click', write3MF);
ui.glCanvas.addEventListener('pointerdown', (event) => {
state.dragging = true;
state.lastX = event.clientX;
state.lastY = event.clientY;
ui.glCanvas.setPointerCapture(event.pointerId);
}
);
ui.glCanvas.addEventListener('pointermove', (event) => {
if (!state.dragging)
return;
state.camera.yaw += (event.clientX - state.lastX) * .007;
state.camera.pitch = clamp(state.camera.pitch + (event.clientY - state.lastY) * .007, -1.45, 1.45);
state.lastX = event.clientX;
state.lastY = event.clientY;
}
);
ui.glCanvas.addEventListener('pointerup', () => {
state.dragging = false;
}
);
ui.glCanvas.addEventListener('wheel', (event) => {
event.preventDefault();
state.camera.zoom = clamp(state.camera.zoom * Math.exp(event.deltaY * .001), 260, 880);
}
, {
passive: false
});
ui.topView.addEventListener('click', () => {
state.camera.pitch = 0;
state.camera.yaw = -Math.PI / 2;
state.camera.zoom = 690;
}
);
ui.isoView.addEventListener('click', () => {
state.camera.pitch = .60;
state.camera.yaw = -Math.PI / 2 - .08;
state.camera.zoom = 620;
}
);
ui.fitView.addEventListener('click', () => {
state.camera.zoom = 620;
}
);
}
+175
View File
@@ -0,0 +1,175 @@
var gl, program, bufferPosition, bufferNormal, bufferColor, vertexCount = 0;
function createShader(glContext, type, source) {
var shader = glContext.createShader(type);
glContext.shaderSource(shader, source);
glContext.compileShader(shader);
if (!glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
throw new Error(glContext.getShaderInfoLog(shader));
return shader;
}
function initGL() {
gl = ui.glCanvas.getContext('webgl', {
antialias: true,
alpha: true
});
if (!gl)
throw new Error('当前浏览器不支持 WebGL 预览,但仍可导出 STL');
var vertex = createShader(gl, gl.VERTEX_SHADER, `
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute vec3 aColor;
uniform mat4 uMVP;
uniform mat4 uModel;
varying vec3 vNormal;
varying vec3 vColor;
varying vec3 vPos;
void main() {
vec4 world = uModel * vec4(aPosition, 1.0);
gl_Position = uMVP * vec4(aPosition, 1.0);
vNormal = normalize(mat3(uModel) * aNormal);
vColor = aColor;
vPos = world.xyz;
}
`);
var fragment = createShader(gl, gl.FRAGMENT_SHADER, `
precision mediump float;
varying vec3 vNormal;
varying vec3 vColor;
varying vec3 vPos;
void main() {
vec3 n = normalize(vNormal);
vec3 lightA = normalize(vec3(-0.45, -0.35, 0.82));
vec3 lightB = normalize(vec3(0.75, 0.25, 0.44));
float diffuse = max(dot(n, lightA), 0.0) * 0.72 + max(dot(n, lightB), 0.0) * 0.18;
float rim = pow(1.0 - abs(n.z), 2.0) * 0.18;
vec3 color = vColor * (0.42 + diffuse) + rim * vec3(0.75, 0.92, 0.90);
gl_FragColor = vec4(color, 1.0);
}
`);
program = gl.createProgram();
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(program));
bufferPosition = gl.createBuffer();
bufferNormal = gl.createBuffer();
bufferColor = gl.createBuffer();
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
requestAnimationFrame(renderGL);
}
function updateMeshBuffers() {
if (!gl || !state.reliefPositions || !state.framePositions)
return;
var include = ui.includeFrame.checked;
var frameLength = include ? state.framePositions.length : 0;
var totalLength = frameLength + state.reliefPositions.length;
var positions = new Float32Array(totalLength);
var normals = new Float32Array(totalLength);
var colors = new Float32Array(totalLength);
var offset = 0;
if (include) {
positions.set(state.framePositions, 0);
normals.set(state.frameNormals, 0);
for (let i = 0; i < frameLength; i += 3) {
colors[i] = .14;
colors[i + 1] = .29;
colors[i + 2] = .31;
}
offset = frameLength;
}
positions.set(state.reliefPositions, offset);
normals.set(state.reliefNormals, offset);
var minT = Number(ui.minThickness.value);
var maxT = Number(ui.maxThickness.value);
var thicknessRange = Math.max(.01, maxT - minT);
for (let i = offset; i < totalLength; i += 3) {
var thickness = clamp(positions[i + 2] - FAN.baseZ, minT, maxT);
var darkness = (thickness - minT) / thicknessRange;
colors[i] = .91 + (.31 - .91) * darkness;
colors[i + 1] = .97 + (.56 - .97) * darkness;
colors[i + 2] = .91 + (.54 - .91) * darkness;
}
vertexCount = totalLength / 3;
gl.bindBuffer(gl.ARRAY_BUFFER, bufferPosition);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, bufferNormal);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, bufferColor);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
}
function mat4Multiply(a, b) {
var out = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
out[col * 4 + row] = a[row] * b[col * 4] + a[4 + row] * b[col * 4 + 1] + a[8 + row] * b[col * 4 + 2] + a[12 + row] * b[col * 4 + 3];
}
}
return out;
}
function perspective(fov, aspect, near, far) {
var f = 1 / Math.tan(fov / 2);
var nf = 1 / (near - far);
return new Float32Array([f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0]);
}
function translation(x, y, z) {
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]);
}
function rotationX(a) {
var c = Math.cos(a)
, s = Math.sin(a);
return new Float32Array([1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1]);
}
function rotationZ(a) {
var c = Math.cos(a)
, s = Math.sin(a);
return new Float32Array([c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
}
function renderGL() {
requestAnimationFrame(renderGL);
if (!gl || !program || !vertexCount)
return;
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var width = Math.max(1, Math.floor(ui.glCanvas.clientWidth * dpr));
var height = Math.max(1, Math.floor(ui.glCanvas.clientHeight * dpr));
if (ui.glCanvas.width !== width || ui.glCanvas.height !== height) {
ui.glCanvas.width = width;
ui.glCanvas.height = height;
}
gl.viewport(0, 0, width, height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(program);
var center = translation(90, 0, -4.5);
var rotation = mat4Multiply(rotationX(state.camera.pitch), rotationZ(state.camera.yaw));
var model = mat4Multiply(rotation, center);
var view = translation(0, 0, -state.camera.zoom);
var proj = perspective(deg(36), width / height, 1, 1600);
var mvp = mat4Multiply(proj, mat4Multiply(view, model));
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uMVP'), false, mvp);
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uModel'), false, model);
var positionLoc = gl.getAttribLocation(program, 'aPosition');
gl.bindBuffer(gl.ARRAY_BUFFER, bufferPosition);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
var normalLoc = gl.getAttribLocation(program, 'aNormal');
gl.bindBuffer(gl.ARRAY_BUFFER, bufferNormal);
gl.enableVertexAttribArray(normalLoc);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
var colorLoc = gl.getAttribLocation(program, 'aColor');
gl.bindBuffer(gl.ARRAY_BUFFER, bufferColor);
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
}