Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be38d1b679 |
@ -87,12 +87,12 @@ header p{font-size:14px;color:#aaa;margin-top:6px}
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>房间类型</h2>
|
<h2>房间类型</h2>
|
||||||
<div class="options" id="roomOptions"></div>
|
<div class="options" id="room可选效果"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>地板类型</h2>
|
<h2>地板类型</h2>
|
||||||
<div class="options" id="typeOptions">
|
<div class="options" id="type可选效果">
|
||||||
<div class="option selected" data-type="wood"><div class="name">木地板</div><div class="meta">Hardwood</div></div>
|
<div class="option selected" data-type="wood"><div class="name">木地板</div><div class="meta">Hardwood</div></div>
|
||||||
<div class="option" data-type="tile"><div class="name">瓷砖</div><div class="meta">Tile / Stone</div></div>
|
<div class="option" data-type="tile"><div class="name">瓷砖</div><div class="meta">Tile / Stone</div></div>
|
||||||
<div class="option" data-type="vinyl"><div class="name">乙烯基地板</div><div class="meta">Luxury Vinyl / SPC</div></div>
|
<div class="option" data-type="vinyl"><div class="name">乙烯基地板</div><div class="meta">Luxury Vinyl / SPC</div></div>
|
||||||
@ -102,18 +102,18 @@ header p{font-size:14px;color:#aaa;margin-top:6px}
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>铺设方式</h2>
|
<h2>铺设方式</h2>
|
||||||
<div class="options" id="patternOptions"></div>
|
<div class="options" id="pattern可选效果"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" id="sizePanel" style="display:none">
|
<div class="panel" id="sizePanel" style="display:none">
|
||||||
<h2>尺寸</h2>
|
<h2>尺寸</h2>
|
||||||
<div class="options" id="sizeOptions"></div>
|
<div class="options" id="size可选效果"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2 id="floorTitle">选择地板款式</h2>
|
<h2 id="floorTitle">选择地板款式</h2>
|
||||||
<div class="toolbar"><input id="floorSearch" placeholder="搜索款式、颜色、材质、SKU"></div>
|
<div class="toolbar"><input id="floorSearch" placeholder="搜索款式、颜色、材质、SKU"></div>
|
||||||
<div class="floor-grid" id="floorOptions"></div>
|
<div class="floor-grid" id="floor可选效果"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
@ -128,30 +128,8 @@ header p{font-size:14px;color:#aaa;margin-top:6px}
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>可选效果</h2>
|
<h2>可选效果</h2>
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<div class="toggle" style="display:flex;flex-direction:column;align-items:flex-start;gap:6px;cursor:default">
|
<label class="toggle" id="groutToggle"><input id="grout" type="checkbox">瓷砖勾缝线<div class="small">仅瓷砖可用</div></label>
|
||||||
<span style="font-weight:700">勾缝线</span>
|
<label class="toggle" id="baseboardToggle"><input id="baseboard" type="checkbox">白色踢脚线<div class="small">沿墙地交界生成</div></label>
|
||||||
<select id="groutSelect" style="width:100%;background:#0f3460;color:#eee;border:1px solid #34456c;border-radius:6px;padding:6px">
|
|
||||||
<option value="">不使用</option>
|
|
||||||
<option value="white">白色</option>
|
|
||||||
<option value="tile">与瓷砖同色</option>
|
|
||||||
<option value="lightgray">浅灰色</option>
|
|
||||||
<option value="gray">灰色</option>
|
|
||||||
<option value="beige">米色</option>
|
|
||||||
<option value="darkgray">深灰色</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="toggle" style="display:flex;flex-direction:column;align-items:flex-start;gap:6px;cursor:default">
|
|
||||||
<span style="font-weight:700">踢脚线</span>
|
|
||||||
<select id="baseboardSelect" style="width:100%;background:#0f3460;color:#eee;border:1px solid #34456c;border-radius:6px;padding:6px">
|
|
||||||
<option value="">不使用</option>
|
|
||||||
<option value="white">白色</option>
|
|
||||||
<option value="floor">与地板同色</option>
|
|
||||||
<option value="cream">米白</option>
|
|
||||||
<option value="lightgray">浅灰</option>
|
|
||||||
<option value="darkgray">深灰</option>
|
|
||||||
<option value="wood">木色(深棕)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -185,13 +163,13 @@ header p{font-size:14px;color:#aaa;margin-top:6px}
|
|||||||
const $=id=>document.getElementById(id);
|
const $=id=>document.getElementById(id);
|
||||||
const state={options:null,type:'wood',file:null,originalUrl:null,room:null,pattern:null,floor:null,timer:null,lastStatus:null};
|
const state={options:null,type:'wood',file:null,originalUrl:null,room:null,pattern:null,floor:null,timer:null,lastStatus:null};
|
||||||
|
|
||||||
function showError(title, detail){
|
function show错误(title, detail){
|
||||||
$('errorBox').style.display='block';
|
$('errorBox').style.display='block';
|
||||||
$('errorDetail').textContent=(title||'生成失败')+(detail?'\n\n'+detail:'');
|
$('errorDetail').textContent=(title||'生成失败')+(detail?'\n\n'+detail:'');
|
||||||
$('resultPlaceholder').style.display='flex';
|
$('resultPlaceholder').style.display='flex';
|
||||||
$('resultPlaceholder').innerHTML='生成失败';
|
$('resultPlaceholder').innerHTML='生成失败';
|
||||||
}
|
}
|
||||||
function clearError(){ $('errorBox').style.display='none'; $('errorDetail').textContent=''; }
|
function clear错误(){ $('errorBox').style.display='none'; $('errorDetail').textContent=''; }
|
||||||
function setProgress(step,msg,width){
|
function setProgress(step,msg,width){
|
||||||
$('statusBox').style.display='block';
|
$('statusBox').style.display='block';
|
||||||
$('stepLabel').textContent=step;
|
$('stepLabel').textContent=step;
|
||||||
@ -218,14 +196,6 @@ function selectIn(container,item,el,key){
|
|||||||
function checkReady(){
|
function checkReady(){
|
||||||
$('generateBtn').disabled=!(state.file&&state.pattern&&state.floor);
|
$('generateBtn').disabled=!(state.file&&state.pattern&&state.floor);
|
||||||
}
|
}
|
||||||
state.materialSource='image';
|
|
||||||
$('materialSourceOptions').onclick=e=>{
|
|
||||||
const opt=e.target.closest('.option');
|
|
||||||
if(!opt)return;
|
|
||||||
[...$('materialSourceOptions').querySelectorAll('.option')].forEach(x=>x.classList.remove('selected'));
|
|
||||||
opt.classList.add('selected');
|
|
||||||
state.materialSource=opt.dataset.source;
|
|
||||||
};
|
|
||||||
// Material source selector
|
// Material source selector
|
||||||
state.materialSource='image';
|
state.materialSource='image';
|
||||||
$('materialSourceOptions').onclick=e=>{
|
$('materialSourceOptions').onclick=e=>{
|
||||||
@ -236,7 +206,7 @@ $('materialSourceOptions').onclick=e=>{
|
|||||||
state.materialSource=opt.dataset.source;
|
state.materialSource=opt.dataset.source;
|
||||||
};
|
};
|
||||||
function render尺寸s(floor){
|
function render尺寸s(floor){
|
||||||
const el=$('sizeOptions'); el.innerHTML=''; state.sizeVariant=null;
|
const el=$('size可选效果'); el.innerHTML=''; state.sizeVariant=null;
|
||||||
const variants=floor.variants||[];
|
const variants=floor.variants||[];
|
||||||
$('sizePanel').style.display='block';
|
$('sizePanel').style.display='block';
|
||||||
// Collect all sizes: main entry first, then variants
|
// Collect all sizes: main entry first, then variants
|
||||||
@ -255,7 +225,7 @@ function cardText(f){
|
|||||||
return [f.name,f.color_tone,f.finish,f.material,f.sku].filter(Boolean).join(' ');
|
return [f.name,f.color_tone,f.finish,f.material,f.sku].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
function renderRooms(){
|
function renderRooms(){
|
||||||
const el=$('roomOptions'); el.innerHTML='';
|
const el=$('room可选效果'); el.innerHTML='';
|
||||||
(state.options.rooms||[]).forEach((r,i)=>{
|
(state.options.rooms||[]).forEach((r,i)=>{
|
||||||
const div=document.createElement('div');
|
const div=document.createElement('div');
|
||||||
div.className='option';
|
div.className='option';
|
||||||
@ -265,8 +235,8 @@ function renderRooms(){
|
|||||||
if(i===0)div.click();
|
if(i===0)div.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function renderPatterns(){
|
function render铺设方式s(){
|
||||||
const el=$('patternOptions'); el.innerHTML=''; state.pattern=null;
|
const el=$('pattern可选效果'); el.innerHTML=''; state.pattern=null;
|
||||||
patternList().forEach((p,i)=>{
|
patternList().forEach((p,i)=>{
|
||||||
const div=document.createElement('div');
|
const div=document.createElement('div');
|
||||||
div.className='option';
|
div.className='option';
|
||||||
@ -277,7 +247,7 @@ function renderPatterns(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function renderFloors(){
|
function renderFloors(){
|
||||||
const el=$('floorOptions'); el.innerHTML=''; state.floor=null;
|
const el=$('floor可选效果'); el.innerHTML=''; state.floor=null;
|
||||||
const q=$('floorSearch').value.trim().toLowerCase();
|
const q=$('floorSearch').value.trim().toLowerCase();
|
||||||
const list=floorList().filter(f=>!q||cardText(f).toLowerCase().includes(q)).slice(0,200);
|
const list=floorList().filter(f=>!q||cardText(f).toLowerCase().includes(q)).slice(0,200);
|
||||||
if(list.length===0){el.innerHTML='<div class="small">没有匹配的地板款式</div>';checkReady();return}
|
if(list.length===0){el.innerHTML='<div class="small">没有匹配的地板款式</div>';checkReady();return}
|
||||||
@ -293,14 +263,15 @@ function renderFloors(){
|
|||||||
}
|
}
|
||||||
function setType(type){
|
function setType(type){
|
||||||
state.type=type;
|
state.type=type;
|
||||||
[...$('typeOptions').querySelectorAll('.option')].forEach(x=>x.classList.toggle('selected',x.dataset.type===type));
|
[...$('type可选效果').querySelectorAll('.option')].forEach(x=>x.classList.toggle('selected',x.dataset.type===type));
|
||||||
$('floorTitle').textContent=type==='tile'?'选择瓷砖款式':type==='vinyl'?'选择乙烯基地板款式':type==='special'?'特殊测试地板 (含AI描述)':'选择木地板款式';
|
$('floorTitle').textContent=type==='tile'?'选择Tile / Stone款式':type==='vinyl'?'选择Luxury Vinyl款式':type==='special'?'AI Test地板 (含AI描述)':'选择Hardwood款式';
|
||||||
const groutDisabled=type!=='tile'&&type!=='special';
|
const groutDisabled=type!=='tile'&&type!=='vinyl'&&type!=='special';
|
||||||
$('groutSelect').disabled=groutDisabled;
|
$('grout').disabled=groutDisabled;
|
||||||
if(groutDisabled)$('groutSelect').value='';
|
$('groutToggle').classList.toggle('disabled',groutDisabled);
|
||||||
renderPatterns(); renderFloors(); checkReady();
|
if(groutDisabled){$('grout').checked=false;$('groutToggle').classList.remove('selected')}
|
||||||
|
render铺设方式s(); renderFloors(); checkReady();
|
||||||
}
|
}
|
||||||
async function loadOptions(){
|
async function load可选效果(){
|
||||||
setProgress('加载选项','正在读取数据库中的地板款式...', '8%');
|
setProgress('加载选项','正在读取数据库中的地板款式...', '8%');
|
||||||
const res=await fetch('/floor/options');
|
const res=await fetch('/floor/options');
|
||||||
const text=await res.text();
|
const text=await res.text();
|
||||||
@ -400,12 +371,16 @@ $('fileInput').onchange=()=>{if($('fileInput').files[0])handleFile($('fileInput'
|
|||||||
$('uploadZone').ondragover=e=>{e.preventDefault();$('uploadZone').classList.add('dragover')};
|
$('uploadZone').ondragover=e=>{e.preventDefault();$('uploadZone').classList.add('dragover')};
|
||||||
$('uploadZone').ondragleave=()=>$('uploadZone').classList.remove('dragover');
|
$('uploadZone').ondragleave=()=>$('uploadZone').classList.remove('dragover');
|
||||||
$('uploadZone').ondrop=e=>{e.preventDefault();$('uploadZone').classList.remove('dragover');if(e.dataTransfer.files[0])handleFile(e.dataTransfer.files[0])};
|
$('uploadZone').ondrop=e=>{e.preventDefault();$('uploadZone').classList.remove('dragover');if(e.dataTransfer.files[0])handleFile(e.dataTransfer.files[0])};
|
||||||
$('typeOptions').onclick=e=>{const opt=e.target.closest('.option');if(opt)setType(opt.dataset.type)};
|
$('type可选效果').onclick=e=>{const opt=e.target.closest('.option');if(opt)setType(opt.dataset.type)};
|
||||||
$('floorSearch').oninput=renderFloors;
|
$('floorSearch').oninput=renderFloors;
|
||||||
|
$('grout').onchange=()=>$('groutToggle').classList.toggle('selected',$('grout').checked);
|
||||||
|
$('baseboard').onchange=()=>$('baseboardToggle').classList.toggle('selected',$('baseboard').checked);
|
||||||
|
$('groutToggle').onclick=e=>{if(e.target.tagName!=='INPUT'&&!$('grout').disabled){$('grout').checked=!$('grout').checked;$('grout').onchange()}};
|
||||||
|
$('baseboardToggle').onclick=e=>{if(e.target.tagName!=='INPUT'){$('baseboard').checked=!$('baseboard').checked;$('baseboard').onchange()}};
|
||||||
|
|
||||||
$('generateBtn').onclick=async()=>{
|
$('generateBtn').onclick=async()=>{
|
||||||
if(!state.file||!state.floor||!state.pattern)return;
|
if(!state.file||!state.floor||!state.pattern)return;
|
||||||
clearInterval(state.timer); clearError();
|
clearInterval(state.timer); clear错误();
|
||||||
$('generateBtn').classList.add('loading');
|
$('generateBtn').classList.add('loading');
|
||||||
$('generateBtn').disabled=true;
|
$('generateBtn').disabled=true;
|
||||||
$('compare').style.display='none';
|
$('compare').style.display='none';
|
||||||
@ -423,9 +398,8 @@ $('generateBtn').onclick=async()=>{
|
|||||||
fd.append('floor_id',floorId);
|
fd.append('floor_id',floorId);
|
||||||
fd.append('pattern_code',patternCode);
|
fd.append('pattern_code',patternCode);
|
||||||
fd.append('room_code',roomCode);
|
fd.append('room_code',roomCode);
|
||||||
fd.append('grout_lines',$('groutSelect').value||'');
|
fd.append('grout_lines',$('grout').checked?'true':'false');
|
||||||
fd.append('white_baseboard',$('baseboardSelect').value||'');
|
fd.append('white_baseboard',$('baseboard').checked?'true':'false');
|
||||||
fd.append('material_source',state.materialSource||'image');
|
|
||||||
fd.append('material_source',state.materialSource||'image');
|
fd.append('material_source',state.materialSource||'image');
|
||||||
const submitted=`提交参数:\nfloor_id: ${floorId}\npattern_code: ${patternCode}\nroom_code: ${roomCode}\ngrout_lines: ${$('grout').checked}\nwhite_baseboard: ${$('baseboard').checked}`;
|
const submitted=`提交参数:\nfloor_id: ${floorId}\npattern_code: ${patternCode}\nroom_code: ${roomCode}\ngrout_lines: ${$('grout').checked}\nwhite_baseboard: ${$('baseboard').checked}`;
|
||||||
setProgress('提交任务',submitted, '10%');
|
setProgress('提交任务',submitted, '10%');
|
||||||
@ -440,14 +414,14 @@ $('generateBtn').onclick=async()=>{
|
|||||||
clearInterval(state.timer); state.timer=null;
|
clearInterval(state.timer); state.timer=null;
|
||||||
$('generateBtn').classList.remove('loading');
|
$('generateBtn').classList.remove('loading');
|
||||||
$('generateBtn').disabled=false;
|
$('generateBtn').disabled=false;
|
||||||
showError(err.message, state.lastStatus ? JSON.stringify(state.lastStatus,null,2) : '');
|
show错误(err.message, state.lastStatus ? JSON.stringify(state.lastStatus,null,2) : '');
|
||||||
}),2500);
|
}),2500);
|
||||||
}catch(err){
|
}catch(err){
|
||||||
if(state.liveInterval){clearInterval(state.liveInterval);$('liveTimer').style.display='none'}
|
if(state.liveInterval){clearInterval(state.liveInterval);$('liveTimer').style.display='none'}
|
||||||
$('generateBtn').classList.remove('loading');
|
$('generateBtn').classList.remove('loading');
|
||||||
$('generateBtn').disabled=false;
|
$('generateBtn').disabled=false;
|
||||||
$('statusBox').style.display='none';
|
$('statusBox').style.display='none';
|
||||||
showError(err.message,'');
|
show错误(err.message,'');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -460,7 +434,7 @@ window.addEventListener('touchmove',e=>{if(dragging)updateCompare(e.touches[0].c
|
|||||||
window.addEventListener('touchend',()=>dragging=false);
|
window.addEventListener('touchend',()=>dragging=false);
|
||||||
window.addEventListener('resize',()=>{if($('compare').style.display==='block')$('beforeImg').style.width=$('compare').offsetWidth+'px'});
|
window.addEventListener('resize',()=>{if($('compare').style.display==='block')$('beforeImg').style.width=$('compare').offsetWidth+'px'});
|
||||||
|
|
||||||
loadOptions().catch(err=>showError('选项加载失败',err.message));
|
load可选效果().catch(err=>show错误('选项加载失败',err.message));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import (
|
|||||||
"floorvisualizer/internal/repository"
|
"floorvisualizer/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Floor options ---
|
// 閳光偓閳光偓 Floor options 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
type FloorOption struct {
|
type FloorOption struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@ -50,8 +50,8 @@ type RoomOption struct {
|
|||||||
var woodPatterns = []PatternOption{
|
var woodPatterns = []PatternOption{
|
||||||
{Code: 1, Name: "直铺(工字拼)Straight Lay", Category: "wood", Pattern: "planks running from bottom-left toward top-right of the image, joints staggered randomly, at least 6 inch offset between rows"},
|
{Code: 1, Name: "直铺(工字拼)Straight Lay", Category: "wood", Pattern: "planks running from bottom-left toward top-right of the image, joints staggered randomly, at least 6 inch offset between rows"},
|
||||||
{Code: 2, Name: "横铺 Horizontal Lay", Category: "wood", Pattern: "planks running horizontally left to right, parallel to the bottom edge of the image, joints staggered randomly, at least 6 inch offset"},
|
{Code: 2, Name: "横铺 Horizontal Lay", Category: "wood", Pattern: "planks running horizontally left to right, parallel to the bottom edge of the image, joints staggered randomly, at least 6 inch offset"},
|
||||||
{Code: 3, Name: "人字拼 Herringbone", Category: "wood", Pattern: "ALL planks must be IDENTICAL width - no piece may appear narrower. Planks at 90° forming continuous zigzag V-lines from bottom-left to top-right of the image. At far distance, planks remain distinct with visible seams - NOT merging into a solid mass."},
|
{Code: 3, Name: "人字铺 Herringbone", Category: "wood", Pattern: "ALL planks must be IDENTICAL width - no piece may appear narrower. Planks at 90 degrees forming continuous zigzag V-lines from bottom-left to top-right of the image. At far distance, planks remain distinct with visible seams - NOT merging into a solid mass."},
|
||||||
{Code: 4, Name: "鱼骨拼 Chevron", Category: "wood", Pattern: "planks with 45° mitered ends forming continuous V-points aligned from bottom-left to top-right of the image"},
|
{Code: 4, Name: "鱼骨铺 Chevron", Category: "wood", Pattern: "planks with 45 degree mitered ends forming continuous V-points aligned from bottom-left to top-right of the image"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var tilePatterns = []PatternOption{
|
var tilePatterns = []PatternOption{
|
||||||
@ -59,7 +59,7 @@ var tilePatterns = []PatternOption{
|
|||||||
{Code: 6, Name: "工字铺(1/2错缝)Running Bond", Category: "tile", Pattern: "rows parallel to bottom edge of image, each row offset 50% from previous, horizontal grout lines continuous, vertical grout lines staggered"},
|
{Code: 6, Name: "工字铺(1/2错缝)Running Bond", Category: "tile", Pattern: "rows parallel to bottom edge of image, each row offset 50% from previous, horizontal grout lines continuous, vertical grout lines staggered"},
|
||||||
{Code: 7, Name: "三七错铺 1/3 Offset", Category: "tile", Pattern: "rows parallel to bottom edge of image, each row offset exactly 33% of tile length, every third row aligns"},
|
{Code: 7, Name: "三七错铺 1/3 Offset", Category: "tile", Pattern: "rows parallel to bottom edge of image, each row offset exactly 33% of tile length, every third row aligns"},
|
||||||
{Code: 8, Name: "六边形 Hexagonal", Category: "tile", Pattern: "six-sided tiles interlocked in honeycomb mesh, flat edges horizontal (parallel to bottom edge of image), grout follows hexagonal edges"},
|
{Code: 8, Name: "六边形 Hexagonal", Category: "tile", Pattern: "six-sided tiles interlocked in honeycomb mesh, flat edges horizontal (parallel to bottom edge of image), grout follows hexagonal edges"},
|
||||||
{Code: 9, Name: "斜铺(60度对角)Diagonal", Category: "tile", Pattern: "tiles rotated 60° to the image frame, grout lines run diagonally at 60 and 150 degrees to the bottom edge, no lines parallel to image edges"},
|
{Code: 9, Name: "斜铺(60度对角)Diagonal", Category: "tile", Pattern: "tiles rotated 60 degrees to the image frame, grout lines run diagonally at 60 and 150 degrees to the bottom edge, no lines parallel to image edges"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomOptions = []RoomOption{
|
var roomOptions = []RoomOption{
|
||||||
@ -113,7 +113,7 @@ func loadFloorOptions(db *sql.DB) ([]FloorOption, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
type key struct{ brand, style string }
|
type key struct{ brand, style string }
|
||||||
seen := map[key]int{} // key → index in out slice
|
seen := map[key]int{} // key 閳?index in out slice
|
||||||
var out []FloorOption
|
var out []FloorOption
|
||||||
for _, p := range prods {
|
for _, p := range prods {
|
||||||
k := key{p.Brand, p.StyleName}
|
k := key{p.Brand, p.StyleName}
|
||||||
@ -199,7 +199,7 @@ func findFloorBySKU(db *sql.DB, sku string) *FloorOption {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Public accessors for worker ---
|
// 閳光偓閳光偓 Public accessors for worker 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
func FindFloorBySKUPublic(db *sql.DB, sku string) *FloorOption { return findFloorBySKU(db, sku) }
|
func FindFloorBySKUPublic(db *sql.DB, sku string) *FloorOption { return findFloorBySKU(db, sku) }
|
||||||
func FindPatternByCodePublic(code string) *PatternOption {
|
func FindPatternByCodePublic(code string) *PatternOption {
|
||||||
@ -218,18 +218,9 @@ func BuildFloorPromptPublic(f FloorOption, p PatternOption, room *RoomOption) st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildCombinedFloorPrompt builds a single prompt that identifies the floor area AND replaces it.
|
// BuildCombinedFloorPrompt builds a single prompt that identifies the floor area AND replaces it.
|
||||||
// No separate mask generation step — the AI does both in one pass.
|
// No separate mask generation step 閳?the AI does both in one pass.
|
||||||
var baseboardColorNames = map[string]string{
|
func BuildCombinedFloorPrompt(f FloorOption, p PatternOption, room *RoomOption, groutLines, whiteBaseboard bool) string {
|
||||||
"white": "白色", "floor": "与地板同色", "cream": "米白",
|
return buildCompactCombinedFloorPrompt(f, p, room, groutLines, whiteBaseboard)
|
||||||
"lightgray": "浅灰", "darkgray": "深灰", "wood": "木色(深棕)",
|
|
||||||
}
|
|
||||||
var groutColorNames = map[string]string{
|
|
||||||
"white": "白色", "tile": "与瓷砖同色", "lightgray": "浅灰色",
|
|
||||||
"gray": "灰色", "beige": "米色", "darkgray": "深灰色",
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildCombinedFloorPrompt(f FloorOption, p PatternOption, room *RoomOption, groutColor, baseboardColor string) string {
|
|
||||||
return buildCompactCombinedFloorPrompt(f, p, room, groutColor, baseboardColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPatternConstraint returns installation rules specific to each pattern code.
|
// buildPatternConstraint returns installation rules specific to each pattern code.
|
||||||
@ -258,7 +249,7 @@ func buildPatternConstraint(code int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Floor Generate Handler (Redis queue) ---
|
// 閳光偓閳光偓 Floor Generate Handler (Redis queue) 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
func SetQueue(enqueue func(ctx context.Context, jobID string, payload map[string]string) error) {
|
func SetQueue(enqueue func(ctx context.Context, jobID string, payload map[string]string) error) {
|
||||||
generateQueue = enqueue
|
generateQueue = enqueue
|
||||||
@ -346,9 +337,9 @@ func FloorGenerate(client *openrouter.Client, db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Progress ---
|
// 閳光偓閳光偓 Progress 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
// --- Floor Status Query ---
|
// 閳光偓閳光偓 Floor Status Query 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
func FloorStatus(w http.ResponseWriter, r *http.Request) {
|
func FloorStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
jobID := r.URL.Query().Get("job_id")
|
jobID := r.URL.Query().Get("job_id")
|
||||||
@ -368,7 +359,7 @@ func FloorStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, 200, js)
|
writeJSON(w, 200, js)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Options ---
|
// 閳光偓閳光偓 Options 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
func FloorOptions(db *sql.DB) http.HandlerFunc {
|
func FloorOptions(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -400,7 +391,7 @@ func FloorOptions(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// materialIndex caches SKU → material description from MaterialAssets JSON files.
|
// materialIndex caches SKU 閳?material description from MaterialAssets JSON files.
|
||||||
var materialIndex map[string]string
|
var materialIndex map[string]string
|
||||||
|
|
||||||
// LoadMaterialIndex loads the material description index from a JSON file.
|
// LoadMaterialIndex loads the material description index from a JSON file.
|
||||||
@ -450,13 +441,13 @@ func loadSpecialFloors(db *sql.DB) []FloorOption {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f.FloorDescription = r.Description
|
f.FloorDescription = r.Description
|
||||||
// Keep f.ImageURL from DB (canonical smallest) — do NOT override with local path
|
// Keep f.ImageURL from DB (canonical smallest) 閳?do NOT override with local path
|
||||||
out = append(out, *f)
|
out = append(out, *f)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Prompts ---
|
// 閳光偓閳光偓 Prompts 閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓閳光偓
|
||||||
|
|
||||||
func buildMaskPrompt(room *RoomOption) string {
|
func buildMaskPrompt(room *RoomOption) string {
|
||||||
base := "你是精确的地面分割工具。请为这张室内照片生成纯黑白遮罩图。\n\n" +
|
base := "你是精确的地面分割工具。请为这张室内照片生成纯黑白遮罩图。\n\n" +
|
||||||
@ -481,26 +472,18 @@ func buildMaterialRules(floor FloorOption) string {
|
|||||||
return "地面必须保持同一个 SKU 的颜色、纹理、木纹或板纹和光泽。保留原图已有光照和阴影;不要新增反光、亮斑或阴影。板缝透视一致,纹理自然但不能明显重复。"
|
return "地面必须保持同一个 SKU 的颜色、纹理、木纹或板纹和光泽。保留原图已有光照和阴影;不要新增反光、亮斑或阴影。板缝透视一致,纹理自然但不能明显重复。"
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCompactCombinedFloorPrompt(f FloorOption, p PatternOption, room *RoomOption, groutColor, baseboardColor string) string {
|
func buildCompactCombinedFloorPrompt(f FloorOption, p PatternOption, room *RoomOption, groutLines, whiteBaseboard bool) string {
|
||||||
roomName := "自动识别"
|
roomName := "自动识别"
|
||||||
if room != nil && room.Code != 0 {
|
if room != nil && room.Code != 0 {
|
||||||
roomName = room.Name
|
roomName = room.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
var priority []string
|
var priority []string
|
||||||
if baseboardColor != "" {
|
if whiteBaseboard {
|
||||||
colorName := baseboardColorNames[baseboardColor]
|
priority = append(priority, "最高优先级:必须生成白色踢脚线。\n踢脚线属于可编辑区域,不属于墙面保护区域。\n沿所有可见墙地交界处生成连续白色踢脚线,高约 4-6 英寸。\n踢脚线应为真实白色木质或漆面条带,有轻微厚度和接触阴影。\n如果没有明显、连续的白色踢脚线,结果视为失败。")
|
||||||
if colorName == "" {
|
|
||||||
colorName = baseboardColor
|
|
||||||
}
|
|
||||||
priority = append(priority, fmt.Sprintf("最高优先级:必须生成踢脚线,颜色为%s。\n踢脚线属于可编辑区域,不属于墙面保护区域。\n沿所有可见墙地交界处生成连续踢脚线,高约 4-6 英寸。\n踢脚线应为真实木质或漆面条带,有轻微厚度和接触阴影。\n如果没有明显、连续的踢脚线,结果视为失败。", colorName))
|
|
||||||
}
|
}
|
||||||
if groutColor != "" {
|
if groutLines {
|
||||||
colorName := groutColorNames[groutColor]
|
priority = append(priority, "必须生成清晰、等距、连续的瓷砖缝。")
|
||||||
if colorName == "" {
|
|
||||||
colorName = groutColor
|
|
||||||
}
|
|
||||||
priority = append(priority, fmt.Sprintf("必须生成清晰、等距、连续的瓷砖缝,颜色为%s。", colorName))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityText := ""
|
priorityText := ""
|
||||||
@ -509,8 +492,8 @@ func buildCompactCombinedFloorPrompt(f FloorOption, p PatternOption, room *RoomO
|
|||||||
}
|
}
|
||||||
|
|
||||||
editScope := "只替换地面"
|
editScope := "只替换地面"
|
||||||
if baseboardColor != "" {
|
if whiteBaseboard {
|
||||||
editScope = "只替换地面和踢脚线"
|
editScope = "只替换地面和白色踢脚线"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s你是室内照片编辑工具,不是重新生成图片。\n房间类型:%s。\n\n任务:\n%s。墙面、家具、桌椅、门窗、天花板保持原图不变。\n保持原图尺寸、构图、相机角度、光照、阴影、白平衡不变。\n保持原图房间空间尺度:如果原图是小房间,生成后仍必须是同一间小房间;不要拉远视角,不要扩大房间深度,不要让瓷砖尺寸呈现大空间远视效果。\n\n%s\n\n输出:\n只返回完成后的图片,不要文字、不要 JSON。",
|
return fmt.Sprintf("%s你是室内照片编辑工具,不是重新生成图片。\n房间类型:%s。\n\n任务:\n%s。墙面、家具、桌椅、门窗、天花板保持原图不变。\n保持原图尺寸、构图、相机角度、光照、阴影、白平衡不变。\n保持原图房间空间尺度:如果原图是小房间,生成后仍必须是同一间小房间;不要拉远视角,不要扩大房间深度,不要让瓷砖尺寸呈现大空间远视效果。\n\n%s\n\n输出:\n只返回完成后的图片,不要文字、不要 JSON。",
|
||||||
@ -585,6 +568,10 @@ func buildFloorPrompt(floor FloorOption, pattern PatternOption, room *RoomOption
|
|||||||
return buildCompactFloorPrompt(floor, pattern, room)
|
return buildCompactFloorPrompt(floor, pattern, room)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildProductNameGuard(floor FloorOption) string {
|
||||||
|
return compactProductNameGuard(floor)
|
||||||
|
}
|
||||||
|
|
||||||
func validateContent(client *openrouter.Client, ctx context.Context, imagePath string) (bool, int) {
|
func validateContent(client *openrouter.Client, ctx context.Context, imagePath string) (bool, int) {
|
||||||
imgData, err := os.ReadFile(imagePath)
|
imgData, err := os.ReadFile(imagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"floorvisualizer/internal/logger"
|
"floorvisualizer/internal/logger"
|
||||||
@ -10,7 +9,7 @@ import (
|
|||||||
|
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
statusCode int
|
statusCode int
|
||||||
wroteHeader bool
|
wroteHeader bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,10 +36,6 @@ func Logging(next http.Handler) http.Handler {
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||||
next.ServeHTTP(rw, r)
|
next.ServeHTTP(rw, r)
|
||||||
// Skip logging for polling endpoints to keep logs clean
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/floor/status") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("%s %s %d %s", r.Method, r.URL.Path, rw.statusCode, time.Since(start))
|
logger.Info("%s %s %d %s", r.Method, r.URL.Path, rw.statusCode, time.Since(start))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,8 +69,8 @@ func processJob(ctx context.Context, rdb *redis.Client, client *openrouter.Clien
|
|||||||
floor := handler.FindFloorBySKUPublic(db, floorID)
|
floor := handler.FindFloorBySKUPublic(db, floorID)
|
||||||
pattern := handler.FindPatternByCodePublic(patternCode)
|
pattern := handler.FindPatternByCodePublic(patternCode)
|
||||||
room := handler.FindRoomByCodePublic(roomCode)
|
room := handler.FindRoomByCodePublic(roomCode)
|
||||||
groutColor := strings.ToLower(strings.TrimSpace(payload["grout_lines"]))
|
groutLines := payloadBool(payload, "grout_lines")
|
||||||
baseboardColor := strings.ToLower(strings.TrimSpace(payload["white_baseboard"]))
|
whiteBaseboard := payloadBool(payload, "white_baseboard")
|
||||||
materialSource := payload["material_source"] // "image", "json", or "both"
|
materialSource := payload["material_source"] // "image", "json", or "both"
|
||||||
if materialSource == "" {
|
if materialSource == "" {
|
||||||
materialSource = "image"
|
materialSource = "image"
|
||||||
@ -137,7 +137,7 @@ func processJob(ctx context.Context, rdb *redis.Client, client *openrouter.Clien
|
|||||||
imageSize := floorImageSize()
|
imageSize := floorImageSize()
|
||||||
logger.Info("[%s] TIMER generate request material=%s image_size=%s input=%s output=%s ref=%s",
|
logger.Info("[%s] TIMER generate request material=%s image_size=%s input=%s output=%s ref=%s",
|
||||||
jobID, materialSource, imageSize, workPath, outputPath, refImagePath)
|
jobID, materialSource, imageSize, workPath, outputPath, refImagePath)
|
||||||
floorPrompt := handler.BuildCombinedFloorPrompt(*floor, *pattern, room, groutColor, baseboardColor)
|
floorPrompt := handler.BuildCombinedFloorPrompt(*floor, *pattern, room, groutLines, whiteBaseboard)
|
||||||
_, err = client.GenerateImageToFile(jobCtx, openrouter.ImageGenerationRequest{
|
_, err = client.GenerateImageToFile(jobCtx, openrouter.ImageGenerationRequest{
|
||||||
Prompt: floorPrompt, OutputPath: outputPath, InputImagePath: workPath,
|
Prompt: floorPrompt, OutputPath: outputPath, InputImagePath: workPath,
|
||||||
ImageSize: imageSize, ReferenceImagePath: refImagePath,
|
ImageSize: imageSize, ReferenceImagePath: refImagePath,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user