Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front
This commit is contained in:
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
3
src/assets/icons/dc/brush_sb.svg
Normal file
3
src/assets/icons/dc/brush_sb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.157122 13.4725L0.526449 10.3737C0.550847 10.1699 0.642783 9.98015 0.787096 9.83413L9.86461 0.654119C10.1812 0.334063 10.6118 0.151974 11.062 0.149342C11.5123 0.146814 11.9457 0.324063 12.2659 0.640637L13.8892 2.24581C14.2094 2.56248 14.3915 2.99386 14.394 3.44416C14.3964 3.89432 14.2192 4.32698 13.9027 4.64714L4.8252 13.8272C4.68081 13.9731 4.49215 14.0672 4.28861 14.0938L1.19415 14.4979C1.05466 14.5161 0.912174 14.5022 0.778872 14.4573C0.645754 14.4124 0.525008 14.3374 0.425099 14.2386C0.325191 14.1398 0.248858 14.0199 0.202421 13.8873C0.167635 13.7878 0.14946 13.6829 0.149902 13.578L0.157122 13.4725ZM11.752 5.29496L13.1397 3.89164C13.2557 3.77408 13.3206 3.61539 13.3198 3.45019C13.3189 3.28482 13.2522 3.12618 13.1347 3.00982L11.5104 1.40368C11.3928 1.28748 11.2334 1.22262 11.068 1.22354C10.9028 1.22458 10.7449 1.29127 10.6286 1.40863L9.24097 2.81195L11.752 5.29496ZM1.84678 13.3302L4.09809 13.0363L10.9965 6.05996L8.4845 3.57598L1.58606 10.5523L1.31747 12.8068L1.24565 13.4088L1.84678 13.3302Z" fill="#0D0D0D" stroke="black" stroke-width="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
src/assets/icons/dc/erase_sb.svg
Normal file
3
src/assets/icons/dc/erase_sb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.26 0.555138L15.4449 4.74003C15.6209 4.91603 15.7605 5.12497 15.8557 5.35492C15.951 5.58488 16 5.83134 16 6.08024C16 6.32914 15.951 6.5756 15.8557 6.80556C15.7605 7.03551 15.6209 7.24445 15.4449 7.42045L8.13225 14.7322L13.2682 14.7331C13.421 14.7329 13.5687 14.7881 13.6839 14.8884C13.7992 14.9888 13.8741 15.1275 13.8949 15.2789L13.9 15.3648C13.9 15.5175 13.8447 15.665 13.7443 15.7801C13.644 15.8951 13.5054 15.97 13.3541 15.9907L13.2682 15.9966L6.18385 15.9975C5.91791 16.0124 5.65181 15.9709 5.403 15.8758C5.15418 15.7807 4.92826 15.6342 4.74003 15.4457L0.555138 11.26C0.379138 11.084 0.239526 10.875 0.144276 10.6451C0.0490248 10.4151 0 10.1687 0 9.91976C0 9.67086 0.0490248 9.4244 0.144276 9.19444C0.239526 8.96449 0.379138 8.75555 0.555138 8.57955L8.57955 0.555138C8.75555 0.379138 8.96449 0.239527 9.19444 0.144276C9.4244 0.049025 9.67086 0 9.91976 0C10.1687 0 10.4151 0.049025 10.6451 0.144276C10.875 0.239527 11.084 0.379138 11.26 0.555138ZM2.70065 8.21986L1.44805 9.4733C1.32974 9.59176 1.26328 9.75234 1.26328 9.91976C1.26328 10.0872 1.32974 10.2478 1.44805 10.3662L5.63378 14.5519C5.75677 14.6749 5.9185 14.7373 6.08024 14.7373L6.10467 14.7331L6.13668 14.7347C6.28414 14.7216 6.42224 14.6569 6.5267 14.5519L7.7793 13.2993L2.69981 8.21986H2.70065ZM9.4733 1.44805L3.59356 7.3261L8.67305 12.4056L14.552 6.52754C14.6107 6.46887 14.6572 6.3992 14.689 6.32253C14.7208 6.24585 14.7371 6.16366 14.7371 6.08066C14.7371 5.99766 14.7208 5.91547 14.689 5.8388C14.6572 5.76212 14.6107 5.69245 14.552 5.63378L10.3662 1.44805C10.2478 1.32974 10.0872 1.26328 9.91976 1.26328C9.75234 1.26328 9.59176 1.32974 9.4733 1.44805Z" fill="#212121"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 962 B |
@@ -1,23 +1,25 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="ai-selectbox-panel">
|
||||
<div>
|
||||
<span class="icon"><svg-icon name="dc-add" size="16" /></span>
|
||||
<span class="label">Add</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="icon"><svg-icon name="dc-remove" size="16" /></span>
|
||||
<span class="label">Remove</span>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.type"
|
||||
:class="{ active: item.name === props.currentTool }"
|
||||
>
|
||||
<span class="icon"><svg-icon :name="item.name" size="16" /></span>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</div>
|
||||
<button>创建</button>
|
||||
</div>
|
||||
</transition>
|
||||
<brush-control-panel :currentTool="show ? 'draw' : ''" style="top: 14rem" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, computed, watch } from 'vue'
|
||||
import depthSlider from './tools/depth-slider.vue'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
import { OperationType, AI_SELECTBOX_TYPE } from '../tools/layerHelper'
|
||||
|
||||
const props = defineProps({
|
||||
currentTool: { required: true, type: [String, null] }
|
||||
})
|
||||
@@ -25,6 +27,28 @@
|
||||
const toolManager = inject('toolManager') as any
|
||||
const showTools = [OperationType.SELECTBOX]
|
||||
const show = computed(() => showTools.includes(props.currentTool))
|
||||
const list = ref([
|
||||
{
|
||||
type: AI_SELECTBOX_TYPE.ADD,
|
||||
name: 'dc-add_sb',
|
||||
label: 'Add'
|
||||
},
|
||||
{
|
||||
type: AI_SELECTBOX_TYPE.REMOVE,
|
||||
name: 'dc-remove_sb',
|
||||
label: 'Remove'
|
||||
},
|
||||
{
|
||||
type: AI_SELECTBOX_TYPE.DRAW,
|
||||
name: 'dc-brush_sb',
|
||||
label: 'Brush'
|
||||
},
|
||||
{
|
||||
type: AI_SELECTBOX_TYPE.ERASER,
|
||||
name: 'dc-erase_sb',
|
||||
label: 'Erase'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// 淡入淡出动画
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
watch(brushState, (value) => {
|
||||
if (value) updateBrushState()
|
||||
})
|
||||
watch(
|
||||
() => props.currentTool,
|
||||
(value) => updateBrushState()
|
||||
)
|
||||
const brushSize = ref(40)
|
||||
const brushOpacity = ref(100)
|
||||
const brushColor = ref('#000000')
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
const isShow = computed(() => isRepeat.value)
|
||||
|
||||
const updateActiveObject = () => {
|
||||
const obj = layers.value.find((v: any) => v.info.id === activeID.value)
|
||||
activeObject.value = obj ? JSON.parse(JSON.stringify(obj)) : null
|
||||
const layer = layerManager.getActiveLayer()
|
||||
activeObject.value = layer ? JSON.parse(JSON.stringify(layer)) : null
|
||||
}
|
||||
watch(layers, () => updateActiveObject())
|
||||
watch(activeID, () => updateActiveObject())
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
<template>
|
||||
<div class="layer-item" @click="onClickLayer">
|
||||
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
||||
<div class="thumb">
|
||||
<img v-if="layer.thumbnail" :src="layer.thumbnail" />
|
||||
</div>
|
||||
<div class="name">
|
||||
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
||||
{{ layer.info.name || '未命名图层' }}
|
||||
<div class="layer-item">
|
||||
<div class="item" @click="onClickLayer">
|
||||
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
||||
<div class="thumb">
|
||||
<img v-if="layer.thumbnail" :src="layer.thumbnail" />
|
||||
</div>
|
||||
<div class="name">
|
||||
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
||||
{{ layer.info.name || '未命名图层' }}
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
ref="nameInputRef"
|
||||
:value="layer.info.name"
|
||||
@blur="onChangeName"
|
||||
@keyup.enter="onChangeName"
|
||||
/>
|
||||
</div>
|
||||
<div class="icons">
|
||||
<span @click.stop="onClickLock">
|
||||
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
|
||||
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
|
||||
</span>
|
||||
<span @click.stop="onClickShowHide"
|
||||
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
||||
/></span>
|
||||
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
|
||||
<span
|
||||
v-if="isGroup"
|
||||
@click.stop="onShowGroup"
|
||||
class="show-group"
|
||||
:class="{ active: layer.info.showChildren }"
|
||||
>
|
||||
<svg-icon name="dc-down_arrow" size="11" />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
ref="nameInputRef"
|
||||
:value="layer.info.name"
|
||||
@blur="onChangeName"
|
||||
@keyup.enter="onChangeName"
|
||||
/>
|
||||
</div>
|
||||
<div class="icons">
|
||||
<span @click.stop="onClickLock">
|
||||
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
|
||||
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
|
||||
</span>
|
||||
<span @click.stop="onClickShowHide"
|
||||
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
||||
/></span>
|
||||
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
|
||||
<!-- <span><svg-icon name="dc-down_arrow" size="11" /></span> -->
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +52,10 @@
|
||||
layer: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const nameInputRef = ref(null)
|
||||
@@ -72,82 +86,98 @@
|
||||
const info = props.layer.info
|
||||
layerManager.setLayerLockById(info.id, !info.lock)
|
||||
}
|
||||
const onShowGroup = () => {
|
||||
props.layer.info.showChildren = !props.layer.info.showChildren
|
||||
// layerManager.setLayerGroupVisibleById(props.layer.info.id, !props.layer.visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layer-item {
|
||||
width: 100%;
|
||||
height: 9.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 1.4rem;
|
||||
background-color: #fafafa;
|
||||
gap: 1rem;
|
||||
border-bottom: 0.2rem solid #ededed;
|
||||
&:last-child {
|
||||
&:last-child > .item {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:not([draging='true']):hover {
|
||||
&:not([draging='true']) > .item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
&.active {
|
||||
&.active > .item {
|
||||
background-color: #ededed !important;
|
||||
}
|
||||
&.drag {
|
||||
opacity: 0;
|
||||
}
|
||||
&.ghost,
|
||||
&.chosen {
|
||||
&.ghost > .item,
|
||||
&.chosen > .item {
|
||||
box-shadow: inset 0 0 5px #aaa;
|
||||
background-color: #ededed !important;
|
||||
}
|
||||
> .drag {
|
||||
padding: 0.3rem;
|
||||
cursor: move;
|
||||
}
|
||||
> .thumb {
|
||||
width: 5.6rem;
|
||||
height: 5.6rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 0.2rem solid #ebebeb;
|
||||
background-color: #fff;
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
> .name {
|
||||
flex: 1;
|
||||
font-size: 1.4rem;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
cursor: pointer;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
> input {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
> .icons {
|
||||
> .item {
|
||||
width: 100%;
|
||||
height: 9.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
> span {
|
||||
cursor: pointer;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
padding: 0 1.4rem;
|
||||
background-color: #fafafa;
|
||||
gap: 1rem;
|
||||
border-bottom: 0.2rem solid #ededed;
|
||||
|
||||
> .drag {
|
||||
padding: 0.3rem;
|
||||
cursor: move;
|
||||
}
|
||||
> .thumb {
|
||||
width: 5.6rem;
|
||||
height: 5.6rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 0.2rem solid #ebebeb;
|
||||
background-color: #fff;
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
> .name {
|
||||
flex: 1;
|
||||
font-size: 1.4rem;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
cursor: pointer;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
> input {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
> .icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
> span {
|
||||
cursor: pointer;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
> .show-group {
|
||||
> .c-svg {
|
||||
transition: transform 0.2s;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&.active > .c-svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,16 @@
|
||||
<div class="content">
|
||||
<VueDraggable
|
||||
:model-value="list"
|
||||
@start="handleDragStart"
|
||||
@end="handleDragEnd"
|
||||
@start="(e) => handleDragStart(e)"
|
||||
@end="(e) => handleDragEnd(e)"
|
||||
@add="(e) => handleAdd(e)"
|
||||
class="sortable-layers"
|
||||
:data-container-type="'root'"
|
||||
:data-parent-id="null"
|
||||
:animation="250"
|
||||
:disabled="false"
|
||||
handle=".drag"
|
||||
ghost-class="ghost"
|
||||
chosen-class="chosen"
|
||||
drag-class="drag"
|
||||
v-bind="config"
|
||||
:group="{
|
||||
name: 'groupName',
|
||||
name: 'group',
|
||||
pull: true,
|
||||
put: true
|
||||
}"
|
||||
:swap-threshold="0.5"
|
||||
:empty-insert-threshold="5"
|
||||
:force-fallback="false"
|
||||
:fallback-tolerance="3"
|
||||
:scroll-sensitivity="100"
|
||||
:scroll-speed="10"
|
||||
>
|
||||
<layer-item
|
||||
v-for="layer in list"
|
||||
@@ -41,7 +29,34 @@
|
||||
:layer="layer"
|
||||
:draging="draging"
|
||||
:class="{ active: layer.info.id === layerManager.activeID.value }"
|
||||
/>
|
||||
:is-group="layer.type === 'group'"
|
||||
>
|
||||
<VueDraggable
|
||||
v-if="layer.type === 'group'"
|
||||
v-show="layer.info.showChildren"
|
||||
:model-value="layer.children"
|
||||
@start="(e) => handleDragStart(e, layer)"
|
||||
@end="(e) => handleDragEnd(e, layer)"
|
||||
@add="(e) => handleAdd(e, layer)"
|
||||
class="sortable-layers-child"
|
||||
v-bind="config"
|
||||
:group="{
|
||||
name: 'child_' + layer.info.id,
|
||||
pull: true,
|
||||
put: true
|
||||
}"
|
||||
>
|
||||
<layer-item
|
||||
v-for="child in layer.children"
|
||||
:key="child.info.id"
|
||||
:layer="child"
|
||||
:draging="draging"
|
||||
:class="{
|
||||
active: child.info.id === layerManager.activeID.value
|
||||
}"
|
||||
/>
|
||||
</VueDraggable>
|
||||
</layer-item>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,14 +68,65 @@
|
||||
import layerItem from './layer-item.vue'
|
||||
const draging = ref(false)
|
||||
const layerManager = inject('layerManager') as any
|
||||
const canvasManager = inject('canvasManager') as any
|
||||
const list = computed(() => layerManager.layers.value)
|
||||
const handleDragStart = () => {
|
||||
draging.value = true
|
||||
const config = ref({
|
||||
'data-container-type': 'root',
|
||||
'data-parent-id': 'null',
|
||||
animation: 250,
|
||||
disabled: false,
|
||||
handle: '.drag',
|
||||
'ghost-class': 'ghost',
|
||||
'chosen-class': 'chosen',
|
||||
'drag-class': 'drag',
|
||||
'swap-threshold': 0.5,
|
||||
'empty-insert-threshold': 5,
|
||||
'force-fallback': false,
|
||||
'fallback-tolerance': 3,
|
||||
'scroll-sensitivity': 100,
|
||||
'scroll-speed': 10
|
||||
})
|
||||
const startParent = ref(null)
|
||||
const clearData = () => {
|
||||
startParent.value = null
|
||||
}
|
||||
const handleDragEnd = (event) => {
|
||||
const handleDragStart = (event, parent?) => {
|
||||
draging.value = true
|
||||
startParent.value = parent
|
||||
}
|
||||
const moveElementInPlace = (arr, oldIndex, newIndex) => {
|
||||
if (oldIndex === newIndex) return arr
|
||||
const movedItem = arr[oldIndex]
|
||||
// 移除元素
|
||||
arr.splice(oldIndex, 1)
|
||||
// 插入到新位置
|
||||
arr.splice(newIndex, 0, movedItem)
|
||||
return arr
|
||||
}
|
||||
const handleDragEnd = (event, parent?) => {
|
||||
draging.value = false
|
||||
const { from, to, oldIndex, newIndex, data } = event
|
||||
layerManager.dragSort(data.info.id, newIndex)
|
||||
if (from !== to) return
|
||||
const arr = parent ? parent.children : layerManager.layers.value
|
||||
moveElementInPlace(arr, oldIndex, newIndex)
|
||||
clearData()
|
||||
layerManager.sortLayers(true)
|
||||
}
|
||||
const handleAdd = (event, parent?) => {
|
||||
const { from, to, oldIndex, newIndex, data } = event
|
||||
if (data.type === 'group') return
|
||||
console.log('跨级拖动', startParent.value, oldIndex, parent, newIndex)
|
||||
const oldArr = startParent.value ? startParent.value.children : layerManager.layers.value
|
||||
const arr = parent ? parent.children : layerManager.layers.value
|
||||
const layer = oldArr.splice(oldIndex, 1)[0]
|
||||
if (parent) {
|
||||
layer.info.parentId = parent.info.id
|
||||
} else {
|
||||
delete layer.info.parentId
|
||||
}
|
||||
arr.splice(newIndex, 0, layer)
|
||||
clearData()
|
||||
layerManager.sortLayers(true)
|
||||
}
|
||||
const addLayer = () => {
|
||||
layerManager.createEmptyLayer(true, true)
|
||||
@@ -116,4 +182,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortable-layers-child {
|
||||
border-left: 2.5rem solid #e8e8e8;
|
||||
border-bottom: 0.2rem solid #ededed;
|
||||
// min-height: 5rem;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { createId } from '../../tools/tools'
|
||||
import { OperationType, AI_SELECTBOX_TYPE } from '../tools/layerHelper'
|
||||
import { getObjectAlphaToCanvas, traceImageContour } from '../tools/canvasMethod'
|
||||
|
||||
/** 智能框选工具管理器 */
|
||||
export class AISelectboxToolManager {
|
||||
@@ -7,6 +8,7 @@ export class AISelectboxToolManager {
|
||||
canvasManager: any
|
||||
stateManager: any
|
||||
layerManager: any
|
||||
toolManager: any
|
||||
|
||||
isDragging: boolean = false
|
||||
startX: number = 0
|
||||
@@ -16,7 +18,16 @@ export class AISelectboxToolManager {
|
||||
this.canvasManager = options.canvasManager
|
||||
this.stateManager = options.stateManager
|
||||
this.layerManager = options.layerManager
|
||||
this.toolManager = options.toolManager
|
||||
}
|
||||
/** 处理切换工具 */
|
||||
handleToolChange(oldTool: string, newTool: string) {
|
||||
if (newTool === OperationType.SELECTBOX) {
|
||||
// 切换到智能框选工具
|
||||
} else {
|
||||
// 切换到普通框选工具
|
||||
|
||||
}
|
||||
}
|
||||
mouseDownEvent(e) {
|
||||
this.isDragging = true
|
||||
@@ -63,7 +74,7 @@ export class AISelectboxToolManager {
|
||||
this.canvasManager.canvas.remove(this.demoObject)
|
||||
this.canvasManager.canvas.renderAll()
|
||||
|
||||
this.createSelectbox()
|
||||
// this.createSelectbox()
|
||||
}
|
||||
|
||||
|
||||
@@ -81,15 +92,11 @@ export class AISelectboxToolManager {
|
||||
stroke: "rgba(255, 77, 71, 1)",
|
||||
strokeWidth: 1.5,
|
||||
strokeDashArray: [4, 4],
|
||||
fill: "rgba(255, 186, 186, 0.5)",
|
||||
fill: "transparent",
|
||||
strokeUniform: true, // 保持描边宽度不随缩放改变
|
||||
// strokeLineCap: "round",// 折线端点样式
|
||||
// strokeLineJoin: "bevel", // 折线连接样式
|
||||
// selectable: false,
|
||||
// evented: false,
|
||||
excludeFromExport: true,
|
||||
hoverCursor: "default",
|
||||
moveCursor: "default",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
absolutePositioned: true,
|
||||
};
|
||||
async createSelectbox() {
|
||||
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
|
||||
@@ -112,200 +119,31 @@ export class AISelectboxToolManager {
|
||||
}).join(" L ");
|
||||
const path = new fabric.Path(`M ${str} z`);
|
||||
path.set({
|
||||
left: left + minX * scaleX,
|
||||
top: top + minY * scaleY,
|
||||
left: left + minX,
|
||||
top: top + minY,
|
||||
scaleX: scaleX,
|
||||
scaleY: scaleY,
|
||||
...this.selectionStyle,
|
||||
});
|
||||
const rect1 = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#f00',
|
||||
info: {
|
||||
id: createId("rect"),
|
||||
name: '矩形图层',
|
||||
}
|
||||
})
|
||||
const rect2 = new fabric.Rect({
|
||||
left: 200,
|
||||
top: 200,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#ff0',
|
||||
info: {
|
||||
id: createId("rect"),
|
||||
name: '矩形图层',
|
||||
}
|
||||
})
|
||||
this.layerManager.createGroupLayer({
|
||||
child: [rect1, rect2],
|
||||
})
|
||||
// this.canvasManager.canvas.add(path)
|
||||
// this.canvasManager.canvas.renderAll()
|
||||
|
||||
const group = await this.layerManager.createGroupLayer({
|
||||
clipPath: path,
|
||||
}, false, false)
|
||||
const rect = await this.layerManager.createRectLayer({
|
||||
width: path.width,
|
||||
height: path.height,
|
||||
left: left + minX,
|
||||
top: top + minY,
|
||||
fill: "rgba(255, 186, 186, 0.5)",
|
||||
info: { parentId: group.info.id },
|
||||
}, false, true)
|
||||
await this.canvasManager.updateSubLayerClipPath()
|
||||
await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false)
|
||||
await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail)
|
||||
this.stateManager.recordState()
|
||||
this.toolManager.setTool(OperationType.SELECT)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
dispose() { }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取对象黑白通道画布
|
||||
* @param {fabric.Object} object - 要处理的 fabric 对象
|
||||
* @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明
|
||||
* @param {number} diff - 差值,默认 25
|
||||
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
|
||||
* @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData
|
||||
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
|
||||
*/
|
||||
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
|
||||
const image = object.getElement();
|
||||
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
|
||||
console.warn("对象不是图片");
|
||||
return null;
|
||||
}
|
||||
const { width, height } = image;
|
||||
if (!width || !height) {
|
||||
console.warn("对象没有元素");
|
||||
return null;
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
const data = ctx.getImageData(0, 0, width, height);
|
||||
for (let i = 0; i < data.data.length; i += 4) {
|
||||
const r = data.data[i + 0];
|
||||
const g = data.data[i + 1];
|
||||
const b = data.data[i + 2];
|
||||
const a = data.data[i + 3];
|
||||
const revR = revData?.data[i + 0] || 0;
|
||||
const revG = revData?.data[i + 1] || 0;
|
||||
const revB = revData?.data[i + 2] || 0;
|
||||
const revA = revData?.data[i + 3] || 0;
|
||||
let isHave = false;
|
||||
if (r || g || b || a) {
|
||||
if (revR > diff || revG > diff || revB > diff || revA > diff) {
|
||||
isHave = false;
|
||||
} else {
|
||||
isHave = true;
|
||||
}
|
||||
}
|
||||
if (isMerge && (revR || revG || revB || revA)) isHave = true;
|
||||
if (isHave) {
|
||||
data.data[i + 0] = rgba.r;
|
||||
data.data[i + 1] = rgba.g;
|
||||
data.data[i + 2] = rgba.b;
|
||||
data.data[i + 3] = rgba.a;
|
||||
} else {
|
||||
data.data[i + 0] = 0;
|
||||
data.data[i + 1] = 0;
|
||||
data.data[i + 2] = 0;
|
||||
data.data[i + 3] = 0;
|
||||
}
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图片边界跟踪算法(透明底)
|
||||
* @param {HTMLCanvasElement} canvas - canvas元素
|
||||
* @param {Number} scale - 缩放比例
|
||||
* @returns {Array} 边界点数组 [{x, y}, ...]
|
||||
*/
|
||||
export function traceImageContour(canvas) {
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
// 查找起始点(第一个不透明像素)
|
||||
let startX = -1;
|
||||
let startY = -1;
|
||||
|
||||
outer: for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
startX = x;
|
||||
startY = y;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startX === -1) return []; // 没有不透明像素
|
||||
|
||||
// Moore-Neighbor边界跟踪算法
|
||||
const contour = [];
|
||||
const visited = new Set();
|
||||
const directions = [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[-1, 1],
|
||||
];
|
||||
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
let backtrackDir = 4; // 起始方向:右
|
||||
|
||||
do {
|
||||
const pointKey = `${currentX},${currentY}`;
|
||||
if (!visited.has(pointKey)) {
|
||||
contour.push({ x: currentX, y: currentY });
|
||||
visited.add(pointKey);
|
||||
}
|
||||
|
||||
// 从右方向开始顺时针查找
|
||||
let found = false;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const dir = (backtrackDir + i) % 8;
|
||||
const dx = directions[dir][0];
|
||||
const dy = directions[dir][1];
|
||||
const checkX = currentX + dx;
|
||||
const checkY = currentY + dy;
|
||||
|
||||
if (
|
||||
checkX >= 0 &&
|
||||
checkX < width &&
|
||||
checkY >= 0 &&
|
||||
checkY < height
|
||||
) {
|
||||
const index = (checkY * width + checkX) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
currentX = checkX;
|
||||
currentY = checkY;
|
||||
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
} while (
|
||||
!(currentX === startX && currentY === startY) &&
|
||||
visited.size < width * height
|
||||
);
|
||||
|
||||
return contour;
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import { AnimationManager } from './AnimationManager'
|
||||
import { detectDeviceType } from '../tools/index'
|
||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
import { cloneObjects } from '../tools/canvasMethod'
|
||||
import { createId } from '../../tools/tools'
|
||||
import md5 from 'md5'
|
||||
|
||||
// 自定义画布转对象属性
|
||||
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail"];
|
||||
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail", "absolutePositioned"];
|
||||
fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject
|
||||
fabric.Object.prototype.toObject = function () {
|
||||
const args = [...arguments]
|
||||
@@ -129,25 +130,28 @@ export class CanvasManager {
|
||||
|
||||
/** 测试-开始 */
|
||||
// this.stateManager.setIsRecord(false)
|
||||
// const rect = await this.layerManager.createRectLayer({ left: 200 })
|
||||
// await this.layerManager.createStarLayer({ left: 400 })
|
||||
// await this.layerManager.createArrowLayer({ left: 600 })
|
||||
// const groupObject = await this.layerManager.createGroupLayer()
|
||||
// const parentId = groupObject.info.id
|
||||
// const rect = await this.layerManager.createRectLayer({ left: 200, info: { parentId } })
|
||||
// const star = await this.layerManager.createStarLayer({ left: 400, info: { parentId } })
|
||||
// const arrow = await this.layerManager.createArrowLayer({ left: 600, info: { parentId } })
|
||||
// await this.layerManager.createGroupLayer()
|
||||
// this.layerManager.setActiveID(rect.info.id)
|
||||
// this.stateManager.setIsRecord(true)
|
||||
/** 测试-结束 */
|
||||
|
||||
this.resetZoom(false, true)// 画布居中
|
||||
|
||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||
this.layerManager.updateLayers()
|
||||
this.stateManager.recordState()
|
||||
// this.stateManager.toolManager.setTool(OperationType.RECTANGLE)
|
||||
}
|
||||
/** 画布添加对象 */
|
||||
async add(obj: any, isRecord = true) {
|
||||
this.canvas.add(obj)
|
||||
const id = obj?.info?.id || ""
|
||||
if (id) {
|
||||
this.layerManager.updateLayers()
|
||||
await this.layerManager.updateLayers(!!obj.info.parentId)
|
||||
this.renderAll()
|
||||
await this.layerManager.updateLayerThumbnailsById(id)
|
||||
}
|
||||
@@ -162,6 +166,35 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新子图层裁剪区域 */
|
||||
async updateSubLayerClipPath() {
|
||||
const objects = this.getObjects().filter((v: any) => v.type !== "group" && !!v.info?.id);
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
let object = objects[i]
|
||||
if (object.clipPath) object.set({ clipPath: null })
|
||||
let group = this.getObjectById(object.info.parentId)
|
||||
if (!group) continue
|
||||
let path = group.clipPath
|
||||
if (!path) continue
|
||||
let clipPath = await cloneObjects([path]).then((v) => v[0])
|
||||
clipPath.set({
|
||||
absolutePositioned: true,
|
||||
})
|
||||
object.set({ clipPath })
|
||||
}
|
||||
this.renderAll()
|
||||
}
|
||||
/** 排序画布对象 */
|
||||
async sortObjectByIds(ids: string[], isRecord?: boolean) {
|
||||
ids.forEach((id, index) => {
|
||||
this.canvas.moveTo(this.getObjectById(id), index)
|
||||
})
|
||||
await this.updateSubLayerClipPath()
|
||||
this.renderAll()
|
||||
if (isRecord) this.stateManager.recordState()
|
||||
}
|
||||
|
||||
|
||||
/** 设置画布事件 */
|
||||
setupCanvasEvents() {
|
||||
// 创建画布事件管理器
|
||||
@@ -177,8 +210,18 @@ export class CanvasManager {
|
||||
}
|
||||
/** 设置激活对象 */
|
||||
setActiveObjectById(id: string) {
|
||||
this.discardActiveObject()
|
||||
const obj = this.getObjectById(id)
|
||||
if (obj && obj.evented) this.canvas.setActiveObject(obj)
|
||||
if (!obj) return
|
||||
if (obj.type === "group") {
|
||||
const objects = [];
|
||||
this.getObjects().forEach((item: any) => {
|
||||
if (item?.info?.parentId === id) objects.push(item)
|
||||
})
|
||||
if (objects.length > 0) this.canvas.setActiveObject(new fabric.ActiveSelection(objects, { canvas: this.canvas }));
|
||||
} else {
|
||||
if (obj.evented) this.canvas.setActiveObject(obj)
|
||||
}
|
||||
this.renderAll()
|
||||
}
|
||||
resetZoom(animated = true, adaptive = true) {
|
||||
@@ -222,11 +265,11 @@ export class CanvasManager {
|
||||
renderAll() {
|
||||
this.canvas.renderAll()
|
||||
}
|
||||
deleteObjectById(id: string) {
|
||||
deleteObjectById(id: string, isUpdate = true) {
|
||||
const object = this.getObjectById(id)
|
||||
if (object) {
|
||||
this.canvas.remove(object)
|
||||
this.layerManager.updateLayers()
|
||||
if (isUpdate) this.layerManager.updateLayers()
|
||||
this.renderAll()
|
||||
}
|
||||
}
|
||||
@@ -235,13 +278,6 @@ export class CanvasManager {
|
||||
this.canvas.discardActiveObject()
|
||||
this.renderAll()
|
||||
}
|
||||
// 拖拽排序
|
||||
dragSort(id, newIndex) {
|
||||
this.canvas.moveTo(this.getObjectById(id), newIndex)
|
||||
this.layerManager.updateLayers()
|
||||
this.renderAll()
|
||||
this.stateManager.recordState()
|
||||
}
|
||||
|
||||
/** 画笔事件 */
|
||||
setupBrushEvents() {
|
||||
@@ -254,13 +290,13 @@ export class CanvasManager {
|
||||
};
|
||||
}
|
||||
/** 处理绘制图像 */
|
||||
handleDrawImage(fabricImage: fabric.Object) {
|
||||
async handleDrawImage(fabricImage: fabric.Object) {
|
||||
const activeID = this.stateManager.layerManager.activeID.value
|
||||
const activeLayer = this.getObjectById(activeID)
|
||||
if (activeLayer) {
|
||||
if (activeLayer && activeLayer.fill?.repeat !== "repeat") {
|
||||
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
|
||||
} else {
|
||||
const emptyLayer = this.layerManager.createEmptyLayer(false);
|
||||
const emptyLayer = await this.layerManager.createEmptyLayer(false);
|
||||
this.layerManager.setActiveID(emptyLayer.info.id, false)
|
||||
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ref } from 'vue'
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { createId } from '../../tools/tools'
|
||||
import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
import { OperationType, BlendMode } from '../tools/layerHelper'
|
||||
import { getArrowPath, cloneObjects, getStarArr } from '../tools/canvasMethod'
|
||||
|
||||
export class LayerManager {
|
||||
@@ -18,7 +18,12 @@ export class LayerManager {
|
||||
}
|
||||
onMounted() { }
|
||||
setActiveID(id: string, isActive = true) {
|
||||
this.activeID.value = id
|
||||
const layer = this.getLayerById(id)
|
||||
if (layer?.type === "group") {
|
||||
this.activeID.value = ""
|
||||
} else {
|
||||
this.activeID.value = id
|
||||
}
|
||||
if (isActive) {
|
||||
this.canvasManager.setActiveObjectById(id)
|
||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||
@@ -28,7 +33,18 @@ export class LayerManager {
|
||||
return this.getLayerById(this.activeID.value)
|
||||
}
|
||||
getLayerById(id) {
|
||||
return this.layers.value.find((item: any) => item.info.id === id)
|
||||
function call(arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let v = arr[i]
|
||||
if (v.info.id === id) return v
|
||||
if (v.children) {
|
||||
let layer = call(v.children)
|
||||
if (layer) return layer
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return call(this.layers.value)
|
||||
}
|
||||
setLayerNameById(id, name: string) {
|
||||
const layer = this.getLayerById(id)
|
||||
@@ -82,6 +98,10 @@ export class LayerManager {
|
||||
|
||||
/** 删除指定图层 */
|
||||
deleteLayerById(id, isActive = true) {
|
||||
const layer = this.getLayerById(id)
|
||||
if (layer.children) {
|
||||
layer.children.forEach(v => this.canvasManager.deleteObjectById(v.info.id, false))
|
||||
}
|
||||
this.canvasManager.deleteObjectById(id)
|
||||
if (id === this.activeID.value && isActive) {
|
||||
this.setActiveID(this.layers.value[0]?.info?.id || "")
|
||||
@@ -106,17 +126,38 @@ export class LayerManager {
|
||||
this.setActiveID(newObject.info.id)
|
||||
})
|
||||
}
|
||||
// 拖拽排序
|
||||
dragSort(id, newIndex) {
|
||||
const index = Math.abs(this.layers.value.length - newIndex - 1)
|
||||
this.canvasManager.dragSort(id, index)
|
||||
/** 根据layers排序图层 */
|
||||
async sortLayers(isRecord?: boolean) {
|
||||
const ids = [];
|
||||
call(this.layers.value)
|
||||
await this.canvasManager.sortObjectByIds(ids.reverse(), isRecord)
|
||||
function call(arr) {
|
||||
arr.forEach(v => {
|
||||
ids.push(v.info.id)
|
||||
if (v.children) call(v.children)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图层列表
|
||||
updateLayers() {
|
||||
this.layers.value = this.canvasManager.getObjects()
|
||||
.filter((v: any) => !!v?.info?.id)
|
||||
.reverse()
|
||||
.map(v => v.toObject())
|
||||
async updateLayers(isSort = false) {
|
||||
const objects = this.canvasManager.getObjects().map(v => v.toObject()).filter(v => !!v.info?.id).reverse()
|
||||
objects.forEach(v => {
|
||||
if (v.type === "group") {
|
||||
if (!v.children) v.children = []
|
||||
return;
|
||||
}
|
||||
const parentId = v.info?.parentId
|
||||
if (!parentId) return
|
||||
objects.forEach((obj: any) => {
|
||||
if (obj.info?.id !== parentId) return
|
||||
if (!obj.children) obj.children = []
|
||||
obj.children.push(v)
|
||||
})
|
||||
})
|
||||
const layers = objects.filter(v => !v.info?.parentId)
|
||||
this.layers.value = layers
|
||||
if (isSort) await this.sortLayers()
|
||||
}
|
||||
|
||||
/** 设置图层位置-不设置默认居中 */
|
||||
@@ -136,7 +177,7 @@ export class LayerManager {
|
||||
}
|
||||
}
|
||||
/** 创建空图层 */
|
||||
createEmptyLayer(isRecord = true, isActive = false) {
|
||||
async createEmptyLayer(isRecord = true, isActive = false) {
|
||||
const emptyObject = new fabric.Rect({
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -147,37 +188,28 @@ export class LayerManager {
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(emptyObject)
|
||||
this.canvasManager.add(emptyObject, isRecord)
|
||||
await this.canvasManager.add(emptyObject, isRecord)
|
||||
if (isActive) this.setActiveID(emptyObject.info.id, false)
|
||||
return emptyObject
|
||||
}
|
||||
/** 创建组图层 */
|
||||
createGroupLayer(options?: any, isRecord = true, isActive = false) {
|
||||
const child = options?.child || []
|
||||
delete options.child
|
||||
const groupObject = new fabric.Group(child, {
|
||||
// subTargetCheck: true, // 关键:检测子对象
|
||||
// interactive: true, // 启用交互
|
||||
// hasControls: true,
|
||||
// hasBorders: true,
|
||||
|
||||
// // 子对象样式
|
||||
// cornerColor: 'blue',
|
||||
// cornerSize: 8,
|
||||
// borderColor: 'green',
|
||||
|
||||
// // 允许子对象独立变换
|
||||
// lockScalingX: false,
|
||||
// lockScalingY: false,
|
||||
// lockRotation: false,
|
||||
async createGroupLayer(options?: any, isRecord = true, isActive = false) {
|
||||
const children = options?.children || []
|
||||
delete options.children
|
||||
const groupObject = new fabric.Group(children, {
|
||||
...(options || {}),
|
||||
hasControls: false, // 不显示控制点
|
||||
hasBorders: false, // 不显示边框
|
||||
selectable: false, // 不可选中(可选)
|
||||
info: {
|
||||
id: createId("group"),
|
||||
name: '组图层',
|
||||
name: '智能选区组',
|
||||
showChildren: true,
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
// this.setLayerPosition(groupObject)
|
||||
this.canvasManager.add(groupObject, isRecord)
|
||||
this.setLayerPosition(groupObject, options)
|
||||
await this.canvasManager.add(groupObject, isRecord)
|
||||
if (isActive) this.setActiveID(groupObject.info.id, false)
|
||||
return groupObject
|
||||
}
|
||||
@@ -199,7 +231,7 @@ export class LayerManager {
|
||||
return textObject
|
||||
}
|
||||
/** 创建矩形图层 */
|
||||
async createRectLayer(options?: any, isActive = false) {
|
||||
async createRectLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const rectObject = new fabric.Rect({
|
||||
width: 100,
|
||||
height: 100,
|
||||
@@ -213,12 +245,12 @@ export class LayerManager {
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(rectObject, options)
|
||||
await this.canvasManager.add(rectObject)
|
||||
await this.canvasManager.add(rectObject, isRecord)
|
||||
if (isActive) this.setActiveID(rectObject.info.id)
|
||||
return rectObject
|
||||
}
|
||||
/** 创建直线图层 */
|
||||
async createLineLayer(options?: any, isActive = false) {
|
||||
async createLineLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
|
||||
delete options.x1
|
||||
delete options.y1
|
||||
@@ -235,14 +267,13 @@ export class LayerManager {
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(lineObject, options)
|
||||
await this.canvasManager.add(lineObject)
|
||||
await this.canvasManager.add(lineObject, isRecord)
|
||||
if (isActive) this.setActiveID(lineObject.info.id)
|
||||
return lineObject
|
||||
}
|
||||
/** 创建椭圆图层 */
|
||||
async createEllipseLayer(options?: any, isActive = false) {
|
||||
async createEllipseLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const ellipseObject = new fabric.Ellipse({
|
||||
radius: 50,
|
||||
fill: '#000',
|
||||
strokeWidth: 0,
|
||||
...(options || {}),
|
||||
@@ -258,7 +289,7 @@ export class LayerManager {
|
||||
return ellipseObject
|
||||
}
|
||||
/** 创建三角形图层 */
|
||||
async createTriangleLayer(options?: any, isActive = false) {
|
||||
async createTriangleLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const triangleObject = new fabric.Triangle({
|
||||
width: 100,
|
||||
height: 100,
|
||||
@@ -272,12 +303,12 @@ export class LayerManager {
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(triangleObject, options)
|
||||
await this.canvasManager.add(triangleObject)
|
||||
await this.canvasManager.add(triangleObject, isRecord)
|
||||
if (isActive) this.setActiveID(triangleObject.info.id)
|
||||
return triangleObject
|
||||
}
|
||||
/** 创建五角星图层 */
|
||||
async createStarLayer(options?: any, isActive = false) {
|
||||
async createStarLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const width = options?.width || 100
|
||||
const height = options?.height || 100
|
||||
delete options.points
|
||||
@@ -292,12 +323,12 @@ export class LayerManager {
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(starObject, options)
|
||||
await this.canvasManager.add(starObject)
|
||||
await this.canvasManager.add(starObject, isRecord)
|
||||
if (isActive) this.setActiveID(starObject.info.id)
|
||||
return starObject
|
||||
}
|
||||
/** 创建箭头图层 */
|
||||
async createArrowLayer(options?: any, isActive = false) {
|
||||
async createArrowLayer(options?: any, isRecord = true, isActive = true) {
|
||||
const width = options?.width || 100
|
||||
const height = options?.height || 10
|
||||
delete options.width
|
||||
@@ -316,7 +347,7 @@ export class LayerManager {
|
||||
}
|
||||
});
|
||||
this.setLayerPosition(arrowObject, options)
|
||||
this.canvasManager.add(arrowObject)
|
||||
await this.canvasManager.add(arrowObject, isRecord)
|
||||
if (isActive) this.setActiveID(arrowObject.info.id)
|
||||
return arrowObject
|
||||
}
|
||||
@@ -324,7 +355,7 @@ export class LayerManager {
|
||||
|
||||
|
||||
/** 创建图片图层 */
|
||||
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) {
|
||||
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true, isActive = true) {
|
||||
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
|
||||
|
||||
const imageObject = await new Promise((resolve) => {
|
||||
@@ -350,7 +381,7 @@ export class LayerManager {
|
||||
}) as fabric.Object
|
||||
this.setLayerPosition(imageObject, options)
|
||||
await this.canvasManager.add(imageObject, isRecord)
|
||||
this.setActiveID(imageObject.info.id)
|
||||
if (isActive) this.setActiveID(imageObject.info.id)
|
||||
return imageObject
|
||||
}
|
||||
|
||||
@@ -363,6 +394,7 @@ export class LayerManager {
|
||||
left: info.left,
|
||||
top: info.top,
|
||||
info: {
|
||||
...(targetLayer?.info || {}),
|
||||
id: createId("image"),
|
||||
name: targetLayer?.info?.name || "合并图层",
|
||||
}
|
||||
@@ -370,12 +402,17 @@ export class LayerManager {
|
||||
resolve(img)
|
||||
}, { crossOrigin: 'anonymous' })
|
||||
})
|
||||
// console.log(mergedImage)
|
||||
const index = this.canvasManager.getObjects().indexOf(targetLayer);
|
||||
this.deleteLayerById(targetLayer.info.id, false)
|
||||
this.setActiveID(mergedImage.info.id, false)
|
||||
|
||||
const nid = mergedImage.info.id
|
||||
await this.canvasManager.add(mergedImage, false);
|
||||
this.setActiveID(nid, false)
|
||||
this.canvasManager.canvas.moveTo(mergedImage, index);
|
||||
|
||||
// this.stateManager.objectManager.setBlendMode(nid, BlendMode.MULTIPLY)
|
||||
// this.stateManager.objectManager.setFillRepeat(nid, false)
|
||||
|
||||
this.canvasManager.renderAll()
|
||||
this.updateLayers()
|
||||
this.stateManager.recordState()
|
||||
@@ -391,12 +428,12 @@ export class LayerManager {
|
||||
})
|
||||
}
|
||||
/** 更新图层缩略图 */
|
||||
async updateLayerThumbnailsById(id: string) {
|
||||
async updateLayerThumbnailsById(id: string, thumbnail?: string, isUpdate = true) {
|
||||
const object = this.canvasManager.getObjectById(id);
|
||||
if (!object) return;
|
||||
const url = await exportObjectToThumbnail(object);
|
||||
const url = thumbnail || await exportObjectToThumbnail(object);
|
||||
object.thumbnail = url
|
||||
this.updateLayers()
|
||||
if (isUpdate) this.updateLayers()
|
||||
}
|
||||
|
||||
dispose() { }
|
||||
|
||||
@@ -87,7 +87,7 @@ export class ObjectManager {
|
||||
}
|
||||
|
||||
/** 设置平铺状态 */
|
||||
setFillRepeat(id: string) {
|
||||
setFillRepeat(id: string, isRecord = true) {
|
||||
const object = this.canvasManager.getObjectById(id)
|
||||
if (!object) return console.warn('设置平铺状态失败,对象不存在ID:', id)
|
||||
if (object.type !== 'image') return console.warn('设置平铺状态失败,对象不是图片类型:', id)
|
||||
@@ -133,7 +133,7 @@ export class ObjectManager {
|
||||
});
|
||||
rect.set("fill", pattern)
|
||||
this.canvasManager.canvas.remove(object)
|
||||
this.canvasManager.add(rect)
|
||||
this.canvasManager.add(rect, isRecord)
|
||||
}
|
||||
/** 获取填充对象 */
|
||||
getFillRepeatObject(id: string) {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class ShapeToolManager {
|
||||
upRectangle(object) {
|
||||
if (object.width === 0) object.width = 100
|
||||
if (object.height === 0) object.height = 100
|
||||
this.layerManager.createRectLayer(object, true)
|
||||
this.layerManager.createRectLayer(object)
|
||||
}
|
||||
|
||||
/** 绘制直线 */
|
||||
@@ -151,7 +151,7 @@ export class ShapeToolManager {
|
||||
})
|
||||
}
|
||||
upLine(object) {
|
||||
this.layerManager.createLineLayer(object, true)
|
||||
this.layerManager.createLineLayer(object)
|
||||
}
|
||||
|
||||
/** 绘制椭圆 */
|
||||
@@ -170,7 +170,7 @@ export class ShapeToolManager {
|
||||
upEllipse(object) {
|
||||
if (object.rx === 0) object.rx = 50
|
||||
if (object.ry === 0) object.ry = 50
|
||||
this.layerManager.createEllipseLayer(object, true)
|
||||
this.layerManager.createEllipseLayer(object)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ export class ShapeToolManager {
|
||||
upTriangle(object) {
|
||||
if (object.width === 0) object.width = 100
|
||||
if (object.height === 0) object.height = 100
|
||||
this.layerManager.createTriangleLayer(object, true)
|
||||
this.layerManager.createTriangleLayer(object)
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export class ShapeToolManager {
|
||||
upStar(object) {
|
||||
if (object.width === 0) object.width = 100
|
||||
if (object.height === 0) object.height = 100
|
||||
this.layerManager.createStarLayer(object, true)
|
||||
this.layerManager.createStarLayer(object)
|
||||
}
|
||||
|
||||
/** 绘制箭头 */
|
||||
@@ -249,7 +249,7 @@ export class ShapeToolManager {
|
||||
top: this.startY,
|
||||
}, true)
|
||||
} else {
|
||||
this.layerManager.createArrowLayer(object, true)
|
||||
this.layerManager.createArrowLayer(object)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export class StateManager {
|
||||
brushManager: any
|
||||
keyEventManager: any
|
||||
objectManager: any
|
||||
aiSelectboxToolManager: any
|
||||
// 设置管理器
|
||||
setManager(options) {
|
||||
options.eventManager && (this.eventManager = options.eventManager)
|
||||
@@ -47,6 +48,7 @@ export class StateManager {
|
||||
options.brushManager && (this.brushManager = options.brushManager)
|
||||
options.keyEventManager && (this.keyEventManager = options.keyEventManager)
|
||||
options.objectManager && (this.objectManager = options.objectManager)
|
||||
options.aiSelectboxToolManager && (this.aiSelectboxToolManager = options.aiSelectboxToolManager)
|
||||
}
|
||||
constructor(options) {
|
||||
this.mxHistory = ref(50)
|
||||
@@ -71,6 +73,7 @@ export class StateManager {
|
||||
/** 记录状态 */
|
||||
recordState() {
|
||||
if (this.running.value) return
|
||||
console.log("recordState")
|
||||
this.running.value = true
|
||||
if (this.historyIndex.value < this.historyList.value.length - 1) {
|
||||
this.historyList.value.splice(this.historyIndex.value + 1)
|
||||
|
||||
@@ -101,6 +101,7 @@ export class ToolManager {
|
||||
setTool(value: string) {
|
||||
const tool = this.tools.find((t) => t.name === value)
|
||||
if (!tool) return console.warn(`工具${tool}不存在`)
|
||||
const oldTool = this.currentTool.value
|
||||
this.currentTool.value = tool.name
|
||||
this.canvasManager.canvas.defaultCursor = tool.cursor
|
||||
this.setCanvasEvented(!!tool.selection)
|
||||
@@ -110,6 +111,7 @@ export class ToolManager {
|
||||
|
||||
if (tool.setup) tool.setup()
|
||||
|
||||
this.stateManager?.aiSelectboxToolManager?.handleToolChange?.(oldTool, tool.name)
|
||||
setTimeout(() => {
|
||||
this.canvasManager.renderAll()
|
||||
});
|
||||
@@ -142,13 +144,16 @@ export class ToolManager {
|
||||
const brushStore = this.brushManager?.brushStore
|
||||
if (brushStore) {
|
||||
// 同步基本属性
|
||||
this.brushManager.setBrushSize(brushStore.state.size);
|
||||
this.brushManager.setBrushColor(brushStore.state.color);
|
||||
this.brushManager.setBrushOpacity(brushStore.state.opacity);
|
||||
// this.brushManager.setBrushSize(brushStore.state.size);
|
||||
// this.brushManager.setBrushColor(brushStore.state.color);
|
||||
// this.brushManager.setBrushOpacity(brushStore.state.opacity);
|
||||
|
||||
// 同步笔刷类型 - 修复方法名,使用正确的setBrushType方法
|
||||
this.brushManager.setBrushType("pencil");
|
||||
}
|
||||
this.brushManager.setBrushSize(5);
|
||||
this.brushManager.setBrushColor("#000");
|
||||
this.brushManager.setBrushOpacity(1);
|
||||
|
||||
// 更新应用到画布
|
||||
this.brushManager.updateBrush();
|
||||
@@ -168,6 +173,7 @@ export class ToolManager {
|
||||
this.brushManager.createEraser();
|
||||
}
|
||||
|
||||
this.brushManager.setBrushSize(5);
|
||||
this.stateManager.layerManager.setActiveObjectErasable()
|
||||
// 启用笔刷指示器
|
||||
this._enableBrushIndicator();
|
||||
|
||||
@@ -34,6 +34,7 @@ export class CanvasEventManager {
|
||||
}
|
||||
this.shapeToolManager = new ShapeToolManager(managers)
|
||||
this.aiSelectboxToolManager = new AISelectboxToolManager(managers)
|
||||
this.stateManager.setManager({ aiSelectboxToolManager: this.aiSelectboxToolManager })
|
||||
|
||||
// 初始化所有事件
|
||||
this.initEvents();
|
||||
@@ -59,9 +60,8 @@ export class CanvasEventManager {
|
||||
// 共享事件
|
||||
this.setupSelectionEvents();
|
||||
this.setupObjectEvents();
|
||||
// this.setupDoubleClickEvents();
|
||||
this.setupDoubleClickEvents();
|
||||
|
||||
// this.setupHandlePathCreated();
|
||||
}
|
||||
|
||||
setupZoomEvents() {
|
||||
@@ -730,8 +730,10 @@ export class CanvasEventManager {
|
||||
});
|
||||
this.canvas.on("object:modified", async (e) => {
|
||||
// updateLayers(e);
|
||||
const id = e.target?.info?.id;
|
||||
const target = e.target;
|
||||
const id = target?.info?.id;
|
||||
if (id) await this.layerManager.updateLayerThumbnailsById(id)
|
||||
if (target.type === "group") await this.canvasManager.updateSubLayerClipPath()
|
||||
this.stateManager.recordState();
|
||||
});
|
||||
this.canvas.on("object:removed", (e) => {
|
||||
@@ -752,113 +754,6 @@ export class CanvasEventManager {
|
||||
});
|
||||
}
|
||||
|
||||
setupLongPress(callback) {
|
||||
this.canvas.on("mouse:down", (opt) => {
|
||||
if (!opt.target) return;
|
||||
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
callback(opt);
|
||||
}, this.longPressThreshold);
|
||||
});
|
||||
|
||||
this.canvas.on("mouse:up", () => {
|
||||
clearTimeout(this.longPressTimer);
|
||||
});
|
||||
|
||||
this.canvas.on("mouse:move", () => {
|
||||
clearTimeout(this.longPressTimer);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置路径创建事件
|
||||
setupHandlePathCreated() {
|
||||
// 在 CanvasEventManager 的构造函数或初始化方法中
|
||||
// this.canvas.on("path:created", this._handlePathCreated.bind(this));
|
||||
}
|
||||
|
||||
_handlePathCreated(e) {
|
||||
// // 获取新创建的路径对象
|
||||
// const path = e.path;
|
||||
// // 设置路径的ID和其他属性
|
||||
// path.id = generateId(); // 生成唯一ID
|
||||
// // 获取当前活动图层
|
||||
// const activeLayer = this.layerManager.getActiveLayer();
|
||||
// // 将路径对象绑定到当前活动图层
|
||||
// if (activeLayer) {
|
||||
// // 设置路径的图层ID
|
||||
// path.layerId = activeLayer.id;
|
||||
// // 更新图层对象列表
|
||||
// if (!activeLayer.fabricObjects) activeLayer.fabricObjects = [];
|
||||
// activeLayer.fabricObjects.push(path);
|
||||
// // 更新图层缩略图
|
||||
// if (this.thumbnailManager) {
|
||||
// this.thumbnailManager.generateLayerThumbnail(activeLayer.id);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并图层中的对象为组以提高性能
|
||||
* @param {Object} options 合并选项
|
||||
* @param {fabric.Image} options.fabricImage 新的图像对象
|
||||
* @param {Object} options.activeLayer 当前活动图层
|
||||
* @private
|
||||
*/
|
||||
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer, options }) {
|
||||
// 确保有命令管理器
|
||||
if (!this.layerManager || !this.layerManager.commandManager) {
|
||||
console.warn("合并对象失败:没有命令管理器");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保有活动图层
|
||||
if (!activeLayer) {
|
||||
console.warn("合并对象失败:没有活动图层");
|
||||
return;
|
||||
}
|
||||
// 验证是否需要合并
|
||||
const hasExistingObjects =
|
||||
Array.isArray(activeLayer.fabricObjects) &&
|
||||
activeLayer.fabricObjects.length > 0;
|
||||
const hasNewImage = !!fabricImage;
|
||||
|
||||
if (!hasExistingObjects && !hasNewImage) {
|
||||
// console.log("没有对象需要合并");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果只有一个新图像且图层为空,直接添加到图层
|
||||
if (hasNewImage && !hasExistingObjects) {
|
||||
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id, options);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行高保真合并操作
|
||||
try {
|
||||
// console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
|
||||
|
||||
const command = await this.layerManager.LayerObjectsToGroup(
|
||||
activeLayer,
|
||||
fabricImage
|
||||
);
|
||||
|
||||
// 设置命令的撤销状态
|
||||
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
|
||||
|
||||
this.layerManager?.commandManager?.execute?.(command, {
|
||||
name: `合并图层 ${activeLayer.name} 中的对象为组`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("合并图层对象时发生错误:", error);
|
||||
|
||||
// 降级处理:如果合并失败,至少保证新图像能添加到图层
|
||||
if (fabricImage && this.layerManager) {
|
||||
// console.log("执行降级处理:直接添加图像到图层");
|
||||
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedLayer(opt) {
|
||||
const selected = opt.selected[0];
|
||||
if (selected && opt.selected.length === 1) {
|
||||
@@ -866,31 +761,6 @@ export class CanvasEventManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图层缩略图
|
||||
updateLayerThumbnail(layerId) {
|
||||
if (!this.thumbnailManager || !layerId || !this.layers) return;
|
||||
|
||||
const layer = this.layers.value.find((l) => l.id === layerId);
|
||||
if (layer) {
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新子元素组合缩略图
|
||||
updateLayerChidrenThumbnail(layerId, fabricObject) {
|
||||
if (!this.thumbnailManager || !fabricObject || !this.layers) return;
|
||||
|
||||
// 查找对应的图层(现在元素就是图层)
|
||||
const layer = this.layers.value.find(
|
||||
(l) => l.fabricObjects && l.fabricObjects?.[0]?.id === layerId
|
||||
);
|
||||
|
||||
if (layer) {
|
||||
// 生成图层缩略图
|
||||
this.thumbnailManager.generateLayerThumbnail(layer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 精确检测设备类型,区分 PC、Mac、平板和移动设备
|
||||
|
||||
@@ -93,3 +93,157 @@ export function angleBetweenPointsDegrees(x1, y1, x2, y2) {
|
||||
|
||||
return deg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取对象黑白通道画布
|
||||
* @param {fabric.Object} object - 要处理的 fabric 对象
|
||||
* @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明
|
||||
* @param {number} diff - 差值,默认 25
|
||||
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
|
||||
* @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData
|
||||
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
|
||||
*/
|
||||
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
|
||||
const image = object.getElement();
|
||||
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
|
||||
console.warn("对象不是图片");
|
||||
return null;
|
||||
}
|
||||
const { width, height } = image;
|
||||
if (!width || !height) {
|
||||
console.warn("对象没有元素");
|
||||
return null;
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
const data = ctx.getImageData(0, 0, width, height);
|
||||
for (let i = 0; i < data.data.length; i += 4) {
|
||||
const r = data.data[i + 0];
|
||||
const g = data.data[i + 1];
|
||||
const b = data.data[i + 2];
|
||||
const a = data.data[i + 3];
|
||||
const revR = revData?.data[i + 0] || 0;
|
||||
const revG = revData?.data[i + 1] || 0;
|
||||
const revB = revData?.data[i + 2] || 0;
|
||||
const revA = revData?.data[i + 3] || 0;
|
||||
let isHave = false;
|
||||
if (r || g || b || a) {
|
||||
if (revR > diff || revG > diff || revB > diff || revA > diff) {
|
||||
isHave = false;
|
||||
} else {
|
||||
isHave = true;
|
||||
}
|
||||
}
|
||||
if (isMerge && (revR || revG || revB || revA)) isHave = true;
|
||||
if (isHave) {
|
||||
data.data[i + 0] = rgba.r;
|
||||
data.data[i + 1] = rgba.g;
|
||||
data.data[i + 2] = rgba.b;
|
||||
data.data[i + 3] = rgba.a;
|
||||
} else {
|
||||
data.data[i + 0] = 0;
|
||||
data.data[i + 1] = 0;
|
||||
data.data[i + 2] = 0;
|
||||
data.data[i + 3] = 0;
|
||||
}
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图片边界跟踪算法(透明底)
|
||||
* @param {HTMLCanvasElement} canvas - canvas元素
|
||||
* @param {Number} scale - 缩放比例
|
||||
* @returns {Array} 边界点数组 [{x, y}, ...]
|
||||
*/
|
||||
export function traceImageContour(canvas) {
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
// 查找起始点(第一个不透明像素)
|
||||
let startX = -1;
|
||||
let startY = -1;
|
||||
|
||||
outer: for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
startX = x;
|
||||
startY = y;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startX === -1) return []; // 没有不透明像素
|
||||
|
||||
// Moore-Neighbor边界跟踪算法
|
||||
const contour = [];
|
||||
const visited = new Set();
|
||||
const directions = [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[-1, 1],
|
||||
];
|
||||
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
let backtrackDir = 4; // 起始方向:右
|
||||
|
||||
do {
|
||||
const pointKey = `${currentX},${currentY}`;
|
||||
if (!visited.has(pointKey)) {
|
||||
contour.push({ x: currentX, y: currentY });
|
||||
visited.add(pointKey);
|
||||
}
|
||||
|
||||
// 从右方向开始顺时针查找
|
||||
let found = false;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const dir = (backtrackDir + i) % 8;
|
||||
const dx = directions[dir][0];
|
||||
const dy = directions[dir][1];
|
||||
const checkX = currentX + dx;
|
||||
const checkY = currentY + dy;
|
||||
|
||||
if (
|
||||
checkX >= 0 &&
|
||||
checkX < width &&
|
||||
checkY >= 0 &&
|
||||
checkY < height
|
||||
) {
|
||||
const index = (checkY * width + checkX) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
currentX = checkX;
|
||||
currentY = checkY;
|
||||
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
} while (
|
||||
!(currentX === startX && currentY === startY) &&
|
||||
visited.size < width * height
|
||||
);
|
||||
|
||||
return contour;
|
||||
}
|
||||
@@ -29,6 +29,12 @@ export async function exportObjectsToImage(objects = [], isDetails = false) {
|
||||
left: obj.left - boundingBox.left,
|
||||
top: obj.top - boundingBox.top,
|
||||
})
|
||||
if (obj.clipPath && obj.clipPath.absolutePositioned) {
|
||||
obj.clipPath.set({
|
||||
left: obj.clipPath.left - boundingBox.left,
|
||||
top: obj.clipPath.top - boundingBox.top,
|
||||
})
|
||||
}
|
||||
staticCanvas.add(obj)
|
||||
})
|
||||
// 导出图片
|
||||
|
||||
@@ -68,3 +68,11 @@ export const BlendMode = {
|
||||
DESTINATION_IN: "destination-in", // 目标内
|
||||
DESTINATION_OUT: "destination-out", // 目标外
|
||||
};
|
||||
|
||||
/** 智能框选工具类型枚举 */
|
||||
export const AI_SELECTBOX_TYPE = {
|
||||
ADD: "add", // 添加模式
|
||||
REMOVE: "remove", // 删除模式
|
||||
DRAW: "draw", // 绘画模式
|
||||
ERASER: "eraser", // 橡皮擦模式
|
||||
}
|
||||
|
||||
@@ -11,15 +11,12 @@
|
||||
<div class="item" @mousedown="(e) => stateManager.setActiveNodeID(node.id)">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
|
||||
<svg-icon name="add" size="14" size-unit="px" />
|
||||
</div>
|
||||
<div class="mask" v-show="mask"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { NODE_DATATYPE, NODE_DATATIER, NODE_TYPE } from '../tools/index.d'
|
||||
import { NODE_DATATYPE, NODE_TYPE } from '../tools/index.d'
|
||||
import { computed, ref, inject } from 'vue'
|
||||
const handles = ref({
|
||||
[NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }],
|
||||
@@ -48,39 +45,6 @@
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const stateManager = inject('stateManager') as any
|
||||
const nodes = computed(() => props.stateManager.nodes.value)
|
||||
const isSubord = computed(() => nodes.value.some((v) => v.data.superiorID === props.node.id))
|
||||
const tier = computed(() => Number(props.node?.data?.tier || 0))
|
||||
//只有3d模型才有三级菜单,目前三级菜单内容少直接禁用按钮
|
||||
const isAdd3d = computed(() => (tier.value === 2 && props.node?.data?.superiorNodeType === NODE_DATATYPE.TO_3D_MODEL) || props.node?.data?.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL)
|
||||
const isReturned = computed(() => {
|
||||
return (
|
||||
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
|
||||
props.node.data.data.imageProcessTasks[0].status == 'RETURNED'
|
||||
)
|
||||
})
|
||||
const isAdd = computed(
|
||||
() =>
|
||||
!isSubord.value &&
|
||||
NODE_DATATYPE.RESULT_IMAGE === props.node.data.type &&
|
||||
!(tier.value === NODE_DATATIER.TO_3VIEW) &&
|
||||
isReturned.value &&
|
||||
isAdd3d.value
|
||||
)
|
||||
const onAdd = () => {
|
||||
const tier_ = tier.value + 1
|
||||
// 从data中获取originalImage
|
||||
let nodeData = props.node?.data?.data.imageProcessTasks.filter(
|
||||
(v) => v.taskId === props.node?.data?.data.selectTaskId
|
||||
)
|
||||
const originalImage = nodeData[0]?.url
|
||||
if (!originalImage) console.log('originalImage 找不到原始图片')
|
||||
props.stateManager.nodeManager.createCardsSelect({
|
||||
data: { tier: tier_, superiorID: props.node.id, originalImage }
|
||||
})
|
||||
stateManager.setActiveNodeID('')
|
||||
}
|
||||
const posCenter = computed(() => {
|
||||
const arr = [NODE_DATATYPE.RESULT_IMAGE]
|
||||
return arr.includes(props.node?.data?.type)
|
||||
@@ -106,25 +70,6 @@
|
||||
> .item {
|
||||
position: relative;
|
||||
}
|
||||
> .add {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid #fff;
|
||||
top: var(--top);
|
||||
right: -16px;
|
||||
transform: translateY(-50%);
|
||||
background-color: #ed8936;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 25px;
|
||||
box-shadow: 0 8px 20px 0 #71809633;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
}
|
||||
&.center {
|
||||
--top: 50%;
|
||||
}
|
||||
|
||||
@@ -69,25 +69,26 @@
|
||||
const onClickItem = (v) => {
|
||||
const id = props.node.id
|
||||
if (!id) return
|
||||
const superiorID = props.node.data.superiorID
|
||||
stateManager.deleteNode(id)
|
||||
console.log(props.node)
|
||||
if(v.secondaryMenu){
|
||||
nodeManager.createCardsSelect({
|
||||
data: {
|
||||
tier: props.node.data?.tier,
|
||||
superiorID: props.node.data?.superiorID,
|
||||
originalImage: props.node.data?.originalImage,
|
||||
secondaryMenu: v.secondaryMenu
|
||||
superiorID,
|
||||
isActive: props.node.data?.isActive,
|
||||
secondaryMenu: v.secondaryMenu,
|
||||
createIndexPosition: props.node.data.createIndexPosition,
|
||||
}
|
||||
})
|
||||
}else{
|
||||
const superiorID = props.node.data.superiorID
|
||||
nodeManager.createCardNode({
|
||||
data: {
|
||||
tier: v.tier,
|
||||
type: v.type,
|
||||
superiorID,
|
||||
originalImage: props.node.data?.originalImage,
|
||||
isActive: props.node.data?.isActive,
|
||||
createIndexPosition: props.node.data.createIndexPosition,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,24 +9,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, inject, useAttrs } from 'vue'
|
||||
import { reactive, inject, useAttrs, computed } from 'vue'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import { getCurrentTime } from '../../../../tools/tools.ts'
|
||||
import { NODE_DATATIER } from '../../../tools/index.d'
|
||||
const attrs = useAttrs()
|
||||
const data = reactive({
|
||||
url: attrs.node?.data?.originalImage,
|
||||
})
|
||||
const stateManager = inject('stateManager') as any
|
||||
const nodeManager = inject('nodeManager') as any
|
||||
const eventManager = inject('eventManager') as any
|
||||
const data = reactive({
|
||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||
})
|
||||
const getApiData = ()=>{
|
||||
return {
|
||||
}
|
||||
}
|
||||
const opCanvas = ()=>{
|
||||
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs?.node?.data?.superiorID || null)
|
||||
if (!superiorNodeUrl) console.log('superiorNodeUrl 找不到原始图片')
|
||||
const data = {
|
||||
url:attrs?.node?.data?.originalImage,
|
||||
url:superiorNodeUrl,
|
||||
canvasId: attrs?.node?.data?.canvasId || null,
|
||||
sketchId: stateManager.sketchId.value,
|
||||
onWorkbench:(options)=>{
|
||||
|
||||
@@ -42,12 +42,15 @@
|
||||
import ColorPalette from './color-palette.vue'
|
||||
import To3View from './to-3view.vue'
|
||||
import To3DModel from './to-3d-model.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi } from '@/api/flow-canvas'
|
||||
|
||||
// import ToVideo from './to-video.vue'
|
||||
// import AddPrint from './add-print.vue'
|
||||
// import ToCAD from './to-cad.vue'
|
||||
const { t } = useI18n()
|
||||
const attrs = useAttrs()
|
||||
const componentRef = ref(null)
|
||||
const components = [
|
||||
@@ -143,14 +146,13 @@
|
||||
|
||||
const onGenerateClick = async () => {
|
||||
const data = componentRef.value?.getApiData?.() || {}
|
||||
const subordNode = stateManager.getSubordNodeById(attrs.node.id)
|
||||
const subordNodes = stateManager.getSubordNodes(attrs.node.id)
|
||||
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs.node.data.superiorID)
|
||||
if(!superiorNodeUrl)return console.log('superiorNodeUrl 找不到原始图片')
|
||||
emit('update-data', componentRef.value?.data)
|
||||
if(!attrs.node?.data?.originalImage)console.log('originalImage 找不到原始图片')
|
||||
|
||||
const apiData = {
|
||||
sketchId: props.sketchId,
|
||||
imageUrl: attrs.node?.data?.originalImage,
|
||||
imageUrl: superiorNodeUrl,
|
||||
...data,
|
||||
}
|
||||
const taskList = await currentComponent.value.api(apiData).then((rv)=>{
|
||||
@@ -178,8 +180,9 @@
|
||||
})
|
||||
}
|
||||
//删除功能卡片
|
||||
const onDeleteClick = ()=>{
|
||||
stateManager.deleteNode(attrs.node.id,{isElMessageBox:true})
|
||||
const onDeleteClick = async ()=>{
|
||||
console.log(stateManager.nodes)
|
||||
stateManager.getSubordinateAllNodes(attrs.node.id,{ isElMessageBox: true })
|
||||
}
|
||||
const setDate = () => {
|
||||
for (const key in props.data) {
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, useAttrs } from 'vue'
|
||||
import { reactive, inject, useAttrs, computed } from 'vue'
|
||||
import uploadFile from '../../tools/upload-file.vue'
|
||||
const attrs = useAttrs()
|
||||
|
||||
const stateManager = inject('stateManager') as any
|
||||
const data = reactive({
|
||||
url: attrs.node?.data?.originalImage,
|
||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||
})
|
||||
const getApiData = ()=>{
|
||||
return {
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
const attrs = useAttrs()
|
||||
const stateManager = inject('stateManager') as any
|
||||
const data = reactive({
|
||||
url: attrs.node.data.originalImage,
|
||||
})
|
||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||
})
|
||||
const getApiData = ()=>{
|
||||
let glbUrl = null
|
||||
const superiorNode = stateManager.nodes.value.filter((item:any)=>item.id === attrs.node?.data?.superiorID)[0]
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
|
||||
<svg-icon name="add" size="14" size-unit="px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,7 +64,7 @@
|
||||
import { downloadImage } from '../../../tools/tools'
|
||||
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue'
|
||||
import HighlightAdmin from '@/components/highlightAdmin.vue'
|
||||
import { NODE_DATATYPE } from '../../tools/index.d'
|
||||
import { NODE_DATATIER, NODE_DATATYPE } from '../../tools/index.d'
|
||||
const openImagePreview = inject('openImagePreview') as (url: string) => void
|
||||
const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void
|
||||
const props = defineProps({
|
||||
@@ -229,6 +232,38 @@
|
||||
eventManager.removeEvents()
|
||||
myEvent.emit('openDepthCanvas', data)
|
||||
}
|
||||
|
||||
const isSubord = computed(() => props.node.id == stateManager.activeNodeID.value)
|
||||
const tier = computed(() => Number(props.node?.data?.tier || 0))
|
||||
//只有3d模型才有三级菜单,目前三级菜单内容少直接禁用按钮
|
||||
const isAdd3d = computed(() => (tier.value === 2 && props.node?.data?.superiorNodeType === NODE_DATATYPE.TO_3D_MODEL) || props.node?.data?.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL)
|
||||
const isReturned = computed(() => {
|
||||
return (
|
||||
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
|
||||
props.node.data.data.imageProcessTasks[0].status == 'RETURNED'
|
||||
)
|
||||
})
|
||||
const isAdd = computed(
|
||||
() =>
|
||||
NODE_DATATYPE.RESULT_IMAGE === props.node.data.type &&
|
||||
!(tier.value === NODE_DATATIER.TO_3VIEW) &&
|
||||
isReturned.value &&
|
||||
isAdd3d.value &&
|
||||
isSubord.value
|
||||
)
|
||||
const onAdd = () => {
|
||||
const tier_ = tier.value + 1
|
||||
const subordNodes = stateManager.getSubordNodes(props.node.id)
|
||||
stateManager.nodeManager.createCardsSelect({
|
||||
data: {
|
||||
tier: tier_,
|
||||
superiorID: props.node.id,
|
||||
createIndexPosition: subordNodes.length + 1,
|
||||
isActive: subordNodes.length == 0
|
||||
}
|
||||
})
|
||||
stateManager.setActiveNodeID('')
|
||||
}
|
||||
document.addEventListener('mousedown', hideMenu)
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousedown', hideMenu)
|
||||
@@ -250,6 +285,25 @@
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid #fff;
|
||||
top: var(--top);
|
||||
right: -16px;
|
||||
transform: translateY(-50%);
|
||||
background-color: #ed8936;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 25px;
|
||||
box-shadow: 0 8px 20px 0 #71809633;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
}
|
||||
.result-image {
|
||||
width: 244px;
|
||||
border-radius: 16px;
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
/** 点击节点 */
|
||||
const clickNode = (event) => {
|
||||
let node = event.node
|
||||
stateManager.showNodeConnections(node.id)
|
||||
// stateManager.showNodeConnections(node.id)
|
||||
}
|
||||
/** 删除节点 */
|
||||
const deleteNode = (id) => {
|
||||
|
||||
@@ -94,7 +94,7 @@ export class EventManager {
|
||||
handleDelete(event: any, activeNodeID: string) {
|
||||
event.preventDefault()
|
||||
if (!activeNodeID) return console.warn('没有选中节点')
|
||||
this.stateManager.deleteNode(activeNodeID, { isElMessageBox: true })
|
||||
this.stateManager.getSubordinateAllNodes(activeNodeID, { isElMessageBox: true })
|
||||
}
|
||||
/** 处理键盘事件 */
|
||||
_handleKeyDown: any
|
||||
|
||||
@@ -10,7 +10,6 @@ interface NodeData {
|
||||
superiorNodeType?: string// 上级节点类型
|
||||
disableDelete?: boolean// 是否禁用删除
|
||||
disableCopy?: boolean// 是否禁用复制
|
||||
originalImage?: string// 要进行生成的图片
|
||||
createIndexPosition?: number// 创建索引位置
|
||||
isActive?: boolean// 是否激活
|
||||
}
|
||||
@@ -35,7 +34,7 @@ export class NodeManager {
|
||||
|
||||
/** 删除节点 */
|
||||
deleteNode(id: string) {
|
||||
this.stateManager.deleteNode(id)
|
||||
this.stateManager.getSubordinateAllNodes(id, { isElMessageBox: true })
|
||||
}
|
||||
/** 添加节点 */
|
||||
addNode(node: any) {
|
||||
@@ -45,27 +44,35 @@ export class NodeManager {
|
||||
/** 创建节点 */
|
||||
createNode(options: NodeOptions) {
|
||||
const superiorID = options?.data?.superiorID
|
||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
||||
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
||||
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
|
||||
const currentNode = superiorGenerateNodes.find((node) => {
|
||||
return node.data.createIndexPosition === options?.data?.createIndexPosition
|
||||
})
|
||||
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
||||
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
||||
}, superiorGenerateNodes[0])
|
||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
||||
console.log(snode)
|
||||
const id = options.id || createId()
|
||||
const positionX = options.positionX || 0
|
||||
const positionY = options.positionY || 0
|
||||
const position = options.position ||
|
||||
(
|
||||
endGenerateNode?
|
||||
currentNode ?
|
||||
currentNode.position :
|
||||
endGenerateNode ?
|
||||
{
|
||||
x: endGenerateNode.position.x + positionX,
|
||||
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
||||
} :
|
||||
!snode ?
|
||||
{ x: positionX, y: positionY } :
|
||||
} : snode ?
|
||||
{
|
||||
x: snode.position.x + snode.dimensions.width + this.nodesep + positionX,
|
||||
y: snode.position.y + positionY
|
||||
} :
|
||||
{
|
||||
x: positionX,
|
||||
y: positionY
|
||||
}
|
||||
)
|
||||
const data = options?.data || {}
|
||||
@@ -103,6 +110,7 @@ export class NodeManager {
|
||||
data: {
|
||||
tier: NODE_DATATIER.CARDS_SELECT,
|
||||
type: NODE_DATATYPE.CARDS_SELECT,
|
||||
createIndexPosition: options?.data?.createIndexPosition || 1,
|
||||
...(options?.data || {}),
|
||||
},
|
||||
}
|
||||
@@ -113,7 +121,7 @@ export class NodeManager {
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.CARD,
|
||||
data: {
|
||||
data: {
|
||||
...(options?.data || {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export class StateManager {
|
||||
source: source,
|
||||
target: target,
|
||||
selectable: false,
|
||||
visible: (node.data.type !== NODE_DATATYPE.RESULT_IMAGE || node.data.isActive),
|
||||
visible: (node.data.isActive),
|
||||
type: 'default'
|
||||
})
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export class StateManager {
|
||||
|
||||
}
|
||||
/** 设置激活节点 */
|
||||
setActiveNodeID(id: string) { this.activeNodeID.value = id }
|
||||
setActiveNodeID(id: string) { this.activeNodeID.value = id;this.showNodeConnections(id) }
|
||||
/** 添加节点 */
|
||||
addNode(node: NodesItem) {
|
||||
this.nodes.value.push(node);
|
||||
@@ -119,37 +119,69 @@ export class StateManager {
|
||||
this.exportFlow()
|
||||
}
|
||||
/** 删除节点 */
|
||||
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) {
|
||||
const node = this.getNodeById(id)
|
||||
if (!node) return console.warn(`没有找到指定id:${id}`)
|
||||
if (node.data.disableDelete) return console.warn('该节点禁用删除')
|
||||
let deletePromise: any = true
|
||||
if (isElMessageBox) {
|
||||
deletePromise = await new Promise<void>((resolve, reject) => {
|
||||
ElMessageBox.confirm(
|
||||
t('flowCanvas.deleteCardConfirm'),
|
||||
'',
|
||||
{
|
||||
confirmButtonText: t('flowCanvas.confirm'),
|
||||
cancelButtonText: t('flowCanvas.cancel'),
|
||||
}
|
||||
).then(() => {
|
||||
resolve(true)
|
||||
}).catch(() => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
if (!deletePromise) return console.log('删除操作被取消')
|
||||
async deleteNode(id: string) {
|
||||
|
||||
this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id)
|
||||
this.recordState()
|
||||
this.exportFlow()
|
||||
}
|
||||
/** 获取节点 */
|
||||
getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
|
||||
/** 获取下级节点 */
|
||||
getSubordNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) }
|
||||
getLastNode() { console.log(this.nodes.value); return this.nodes.value[this.nodes.value.length - 1] }
|
||||
|
||||
/** 获取上级生成节点的图片 */
|
||||
getSuperiorNodeImage(superiorID: string) {
|
||||
const superiorNode = this.getNodeById(superiorID)
|
||||
if(!superiorNode){
|
||||
ElMessage.error(t('flowCanvas.cannotFindSuperiorImage'))
|
||||
return null
|
||||
}
|
||||
const superiorNodeUrl = superiorNode.data.data.imageProcessTasks.filter((item)=>{
|
||||
return item.taskId == superiorNode.data.data.selectTaskId
|
||||
})[0]?.url
|
||||
return superiorNodeUrl
|
||||
}
|
||||
/** 获取下级所有子级节点 */
|
||||
async getSubordinateAllNodes(id: string,{ isElMessageBox } = { isElMessageBox: false }) {
|
||||
const node = this.getNodeById(id)
|
||||
if (!node) return console.warn(`没有找到指定id:${id}`)
|
||||
if (node.data.disableDelete) return ElMessage.error(t('flowCanvas.initialNodeProhibited'))
|
||||
|
||||
const result = [node]
|
||||
const findChildren = (parentId: string) => {
|
||||
const children = this.nodes.value.filter(item => item.data.superiorID === parentId)
|
||||
children.forEach(child => {
|
||||
if(child.data.type !== NODE_DATATYPE.RESULT_IMAGE){
|
||||
result.push(child)
|
||||
}
|
||||
findChildren(child.id)
|
||||
})
|
||||
}
|
||||
let deletePromise: any = true
|
||||
if (isElMessageBox && result.length > 1) {
|
||||
deletePromise = await new Promise<void>((resolve, reject) => {
|
||||
ElMessageBox.confirm(
|
||||
t('flowCanvas.deleteSubordinateCard'),
|
||||
'',
|
||||
{
|
||||
confirmButtonText: t('flowCanvas.confirm'),
|
||||
cancelButtonText: t('flowCanvas.cancel'),
|
||||
}
|
||||
).then(() => {resolve(true)
|
||||
}).catch(() => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
if(!deletePromise) return console.log('删除操作被取消')
|
||||
|
||||
this.deleteNode(id)
|
||||
result.forEach(item => {
|
||||
this.deleteNode(item.id)
|
||||
})
|
||||
this.recordState()
|
||||
this.exportFlow()
|
||||
}
|
||||
/** 设置工具 */
|
||||
setTool(tool: string) { this.tool.value = tool }
|
||||
/** 设置光标 */
|
||||
@@ -222,9 +254,9 @@ export class StateManager {
|
||||
|
||||
/** 显示指定子节点和父节点连接线,隐藏父节点和其他子节点链接线, */
|
||||
showNodeConnections(id: string) {
|
||||
if(!id) return
|
||||
const node = this.getNodeById(id)
|
||||
if(node.data.component != NODE_DATATYPE.RESULT_IMAGE && node.data.superiorID) return
|
||||
let edges_ = JSON.parse(JSON.stringify(this.edges.value))
|
||||
if(!node?.data?.superiorID) return
|
||||
this.nodes.value.forEach((nodeItem) => {
|
||||
if(node.data.superiorID === nodeItem.data.superiorID && nodeItem.id == id) {
|
||||
nodeItem.data.isActive = true
|
||||
|
||||
@@ -15,7 +15,8 @@ import { computed } from "vue";
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "",
|
||||
// required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
@@ -30,7 +31,7 @@ const props = defineProps({
|
||||
default: 'rem',
|
||||
}
|
||||
});
|
||||
const iconName = computed(() => `#icon-${props.name}`);
|
||||
const iconName = computed(() => `#icon-${props?.name}`);
|
||||
const svgClass = computed(() => {
|
||||
if (props.name) return `svg-icon icon-${props.name}`;
|
||||
return "svg-icon";
|
||||
|
||||
@@ -173,21 +173,17 @@ export default {
|
||||
deleteHint:'Once deleted, you won’t be able to view this conversation again.',
|
||||
restoreChat:'Restore chat?',
|
||||
restoreHint:'Once deleted, you won’t be able to view this conversation again.',
|
||||
cancel: 'cancel',
|
||||
Cancel: 'Cancel',
|
||||
Confirm: 'Confirm',
|
||||
export: 'Export',
|
||||
},
|
||||
//generateSketch
|
||||
generateSketch: {
|
||||
restore: 'Restore',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit'
|
||||
},
|
||||
flowCanvas: {
|
||||
deleteCardConfirm: 'Are you sure you want to delete this function card?',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
confirmLeave: 'Are you sure you want to leave? You may have unsaved changes.',
|
||||
cannotFindSuperiorImage: 'Cannot find the superior image',
|
||||
deleteSubordinateCard: 'After deletion, all the function cards will also be deleted.',
|
||||
initialNodeProhibited: 'Initial node is prohibited from being deleted.',
|
||||
},
|
||||
assistant: {
|
||||
inputPlaceholder: 'Ask anything',
|
||||
|
||||
@@ -168,9 +168,9 @@ export default {
|
||||
deleteHint: '删除后将无法恢复该对话。',
|
||||
restoreChat: '恢复对话?',
|
||||
restoreHint: '恢复后将显示该对话。',
|
||||
cancel: '取消',
|
||||
Cancel: '取消',
|
||||
Confirm: '确认',
|
||||
export: '导出'
|
||||
export: '导出',
|
||||
},
|
||||
//generateSketch
|
||||
generateSketch: {
|
||||
@@ -179,10 +179,11 @@ export default {
|
||||
edit: '编辑'
|
||||
},
|
||||
flowCanvas: {
|
||||
deleteCardConfirm: '确定要删除该功能卡片吗?',
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
confirmLeave: '您可能有未保存的更改,确定要离开吗?',
|
||||
cannotFindSuperiorImage: '找不到上级图片',
|
||||
initialNodeProhibited: 'Initial node is prohibited from being deleted.',
|
||||
},
|
||||
assistant: {
|
||||
inputPlaceholder: '请输入'
|
||||
|
||||
@@ -58,6 +58,9 @@ const setVersionsList = (res)=>{
|
||||
}
|
||||
traverseArray(res,'',(item,i,father)=>{
|
||||
item.versionId = father?`${father.versionId}-${i+1}`:'1'
|
||||
if(item.id == projectStore.state.nodeId){
|
||||
selectItem.value = {...item}
|
||||
}
|
||||
})
|
||||
versionsList.value = res
|
||||
}
|
||||
@@ -110,10 +113,6 @@ const versionDelete = (versionDetail)=>{
|
||||
treeKey.value++
|
||||
}
|
||||
|
||||
watch(()=>projectStore.state.nodeId,(newVal,oldVal)=>{
|
||||
if(!newVal || newVal === selectItem?.value?.id)return
|
||||
selectItem.value = {id:newVal}
|
||||
})
|
||||
|
||||
let data = reactive({})
|
||||
// onMounted(() => {setVersionsList('')})
|
||||
@@ -126,7 +125,6 @@ const {} = toRefs(data)
|
||||
<el-drawer
|
||||
v-model="versionTreeData.drawer"
|
||||
:close-on-press-escape="false"
|
||||
:close-on-click-modal="false"
|
||||
:size="treeState ? '73.5rem' : '73.5rem'"
|
||||
body-class="versionTreeBody"
|
||||
:with-header="false"
|
||||
|
||||
@@ -7,6 +7,8 @@ import InputNode from './InputNode.vue'//主
|
||||
import SecondaryNode from './secondaryNode.vue'//分支
|
||||
import { useLayout } from '@/utils/treeDiagram'
|
||||
import dialogVue from "../../components/dialog.vue";
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const props = defineProps({
|
||||
selectItem: {
|
||||
type: Object,
|
||||
@@ -22,7 +24,7 @@ const emit = defineEmits([
|
||||
'versionRestore',
|
||||
'versionDelete',
|
||||
])
|
||||
|
||||
const {t:$t} = useI18n()
|
||||
const dialogDeleteRef = ref()
|
||||
const dialogRestoreRef = ref()
|
||||
|
||||
@@ -89,9 +91,8 @@ watch(()=>props.treeList.length, (newVal, oldVal) => {
|
||||
watch(()=>props.selectItem.versionId, (newVal, oldVal) => {
|
||||
})
|
||||
|
||||
const versionRestore = ()=>{
|
||||
emit('versionRestore')
|
||||
// dialogRestoreRef.value?.open()
|
||||
const versionRestore = async ()=>{
|
||||
dialogDeleteRef.value?.open()
|
||||
}
|
||||
|
||||
const versionDelete = ()=>{
|
||||
@@ -139,24 +140,24 @@ defineExpose({push})
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- <dialogVue
|
||||
<dialogVue
|
||||
:textData="{
|
||||
title: $t('VersionTree.deleteChat'),
|
||||
text: $t('VersionTree.deleteHint'),
|
||||
submitText: $t('VersionTree.delete'),
|
||||
cancelText: $t('VersionTree.cancel'),
|
||||
cancelText: $t('VersionTree.Cancel'),
|
||||
}"
|
||||
:styleData="{
|
||||
width: '40.6rem'
|
||||
}"
|
||||
:callBack="()=>emit('versionDelete')"
|
||||
:callBack="()=>emit('versionRestore')"
|
||||
ref="dialogDeleteRef" />
|
||||
<dialogVue
|
||||
<!-- <dialogVue
|
||||
:textData="{
|
||||
title: $t('VersionTree.restoreChat'),
|
||||
text: $t('VersionTree.restoreHint'),
|
||||
submitText: $t('VersionTree.confirm'),
|
||||
cancelText: $t('VersionTree.cancel'),
|
||||
cancelText: $t('VersionTree.Cancel'),
|
||||
}"
|
||||
:styleData="{
|
||||
width: '40.6rem'
|
||||
@@ -225,10 +226,9 @@ defineExpose({push})
|
||||
margin-bottom: 2rem;
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
&.active{
|
||||
background-color: #f5f5f5;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #f5f5f5;
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<span class="label" v-show="!item.edit">{{ item.name }}</span>
|
||||
<el-popover
|
||||
placement="right"
|
||||
trigger="click"
|
||||
trigger="contextmenu"
|
||||
width="10rem"
|
||||
popper-style="
|
||||
padding: .6rem 0.7rem;
|
||||
@@ -54,7 +54,9 @@
|
||||
v-model:visible="item.visible"
|
||||
>
|
||||
<template #reference>
|
||||
<span @click.stop class="icon"><svg-icon name="more" size="16" /></span>
|
||||
<span @click.stop="item.visible = !item.visible" class="icon">
|
||||
<svg-icon name="more" size="16" />
|
||||
</span>
|
||||
</template>
|
||||
<div class="history-item-menu">
|
||||
<div class="rename" @click="onRenameHistoryItem(item)">
|
||||
|
||||
Reference in New Issue
Block a user