working manual refresh
parent
930df2e288
commit
69b2fff6d1
53
src/Scene.js
53
src/Scene.js
|
@ -235,7 +235,7 @@ export class Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
boolOp(m1, m2, op) {
|
boolOp(m1, m2, op, refresh = false) {
|
||||||
let bspA = CSG.fromMesh(m1)
|
let bspA = CSG.fromMesh(m1)
|
||||||
let bspB = CSG.fromMesh(m2)
|
let bspB = CSG.fromMesh(m2)
|
||||||
m1.visible = false
|
m1.visible = false
|
||||||
|
@ -275,18 +275,51 @@ export class Scene {
|
||||||
|
|
||||||
mesh.add(vertices)
|
mesh.add(vertices)
|
||||||
|
|
||||||
sc.obj3d.add(mesh)
|
if (!refresh) {
|
||||||
|
sc.obj3d.add(mesh)
|
||||||
|
|
||||||
this.store.dispatch({
|
this.store.dispatch({
|
||||||
type: 'set-entry-visibility', obj: {
|
type: 'set-entry-visibility', obj: {
|
||||||
[m1.name]: false,
|
[m1.name]: false,
|
||||||
[m2.name]: false,
|
[m2.name]: false,
|
||||||
[mesh.name]: true,
|
[mesh.name]: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.store.dispatch({
|
||||||
|
type: 'rx-boolean', mesh, deps: [m1.name, m2.name]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return mesh
|
||||||
|
}
|
||||||
|
|
||||||
return mesh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshNode(id) {
|
||||||
|
let curId
|
||||||
|
let que = [id]
|
||||||
|
let idx = 0
|
||||||
|
|
||||||
|
const { byId, tree } = this.store.getState().treeEntries
|
||||||
|
while (idx < que.length) {
|
||||||
|
curId = que[idx++]
|
||||||
|
|
||||||
|
const info = byId[curId].userData.featureInfo
|
||||||
|
let newNode
|
||||||
|
if (info.length == 2) {
|
||||||
|
newNode = this.extrude(byId[info[0]], info[1], true)
|
||||||
|
} else if (info.length == 3) {
|
||||||
|
newNode = this.boolOp(byId[info[0]], byId[info[1]], info[2], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
byId[curId].geometry.copy(newNode.geometry)
|
||||||
|
|
||||||
|
for (let k in tree[curId]) {
|
||||||
|
que.push(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -310,12 +310,36 @@ class Sketch {
|
||||||
|
|
||||||
for (let j = 0; j < link.length; j++) {
|
for (let j = 0; j < link.length; j++) {
|
||||||
const obj = this.obj3d.children[i + j]
|
const obj = this.obj3d.children[i + j]
|
||||||
obj.geometry.dispose()
|
// obj.geometry.dispose()
|
||||||
obj.material.dispose()
|
// obj.material.dispose()
|
||||||
|
|
||||||
for (let c_id of obj.userData.constraints.slice()) { // i hate js
|
obj.traverse((ob) => {
|
||||||
|
if (ob.geometry) ob.geometry.dispose()
|
||||||
|
if (ob.material) ob.material.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
// collect all coincident constraints to be reconnected
|
||||||
|
// after deleting this point
|
||||||
|
let arr = []
|
||||||
|
let cons
|
||||||
|
for (let c_id of obj.userData.constraints.slice()) {
|
||||||
|
// i hate js, slice is important because deleteContraints mutates constraints array
|
||||||
|
cons = this.constraints.get(c_id)
|
||||||
|
if (cons[0] == 'points_coincident') {
|
||||||
|
arr.push(cons[2][0] == obj.name ?
|
||||||
|
cons[2][1] : cons[2][0]
|
||||||
|
)
|
||||||
|
}
|
||||||
this.deleteConstraints(c_id)
|
this.deleteConstraints(c_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length - 1; i++) {
|
||||||
|
setCoincident.call(this,[
|
||||||
|
this.obj3d.children[this.objIdx.get(arr[i])],
|
||||||
|
this.obj3d.children[this.objIdx.get(arr[i+1])]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
obj.userData.constraints = []
|
obj.userData.constraints = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { color } from './shared'
|
import { color } from './shared'
|
||||||
|
|
||||||
export async function setCoincident() {
|
export async function setCoincident(sel) {
|
||||||
let selection = await this.awaitSelection({ point: 2 }, { point: 1, line: 1 })
|
let selection
|
||||||
if (selection == null) return;
|
if (sel === undefined) {
|
||||||
|
selection = await this.awaitSelection({ point: 2 }, { point: 1, line: 1 })
|
||||||
|
if (selection == null) return;
|
||||||
|
} else {
|
||||||
|
selection = sel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (selection.every(e => e.userData.type == 'point')) {
|
if (selection.every(e => e.userData.type == 'point')) {
|
||||||
this.constraints.set(++this.c_id,
|
this.constraints.set(++this.c_id,
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as THREE from '../node_modules/three/src/Three';
|
import * as THREE from '../node_modules/three/src/Three';
|
||||||
import { color } from './shared'
|
import { color } from './shared'
|
||||||
export function extrude(sketch, depth) {
|
export function extrude(sketch, depth, refresh=false) {
|
||||||
|
|
||||||
let constraints = sketch.constraints;
|
let constraints = sketch.constraints;
|
||||||
let linkedObjs = sketch.linkedObjs;
|
let linkedObjs = sketch.linkedObjs;
|
||||||
|
@ -10,7 +10,7 @@ export function extrude(sketch, depth) {
|
||||||
let v2s = []
|
let v2s = []
|
||||||
|
|
||||||
function findPair(node) {
|
function findPair(node) {
|
||||||
console.log(node.name,'xx')
|
// console.log(node.name, 'xx')
|
||||||
if (node.userData.construction) return;
|
if (node.userData.construction) return;
|
||||||
visited.add(node)
|
visited.add(node)
|
||||||
let linkedObj = linkedObjs.get(node.userData.l_id)
|
let linkedObj = linkedObjs.get(node.userData.l_id)
|
||||||
|
@ -43,7 +43,7 @@ export function extrude(sketch, depth) {
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
if (d == children[2]) {
|
if (d == children[2]) {
|
||||||
console.log('pair found')
|
// console.log('pair found')
|
||||||
};
|
};
|
||||||
findTouching(d)
|
findTouching(d)
|
||||||
|
|
||||||
|
@ -51,19 +51,19 @@ export function extrude(sketch, depth) {
|
||||||
|
|
||||||
|
|
||||||
function findTouching(node) {
|
function findTouching(node) {
|
||||||
console.log(node.name,'yy')
|
// console.log(node.name, 'yy')
|
||||||
for (let t of node.userData.constraints) {
|
for (let t of node.userData.constraints) {
|
||||||
console.log(constraints.get(t)[2],node.name )
|
// console.log(constraints.get(t)[2], node.name)
|
||||||
if (constraints.get(t)[0] != 'points_coincident') continue
|
if (constraints.get(t)[0] != 'points_coincident') continue
|
||||||
for (let c of constraints.get(t)[2]) {
|
for (let c of constraints.get(t)[2]) {
|
||||||
if (c == -1) continue;
|
if (c == -1) continue;
|
||||||
const d = children[objIdx.get(c)]
|
const d = children[objIdx.get(c)]
|
||||||
if (d == node) continue;
|
if (d == node) continue;
|
||||||
if (d == children[2]) {
|
if (d == children[2]) {
|
||||||
console.log('loop found')
|
// console.log('loop found')
|
||||||
} else {
|
} else {
|
||||||
// if (!visited.has(d)) {
|
// if (!visited.has(d)) {
|
||||||
findPair(d)
|
findPair(d)
|
||||||
// }
|
// }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ export function extrude(sketch, depth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
findPair(children[2]) //??? need fixing
|
findPair(children[sketch.geomStartIdx + 1]) // ?? possibly allow user select search start point
|
||||||
|
|
||||||
const shape = new THREE.Shape(v2s);
|
const shape = new THREE.Shape(v2s);
|
||||||
// const extrudeSettings = { depth: Math.abs(depth), bevelEnabled: false };
|
// const extrudeSettings = { depth: Math.abs(depth), bevelEnabled: false };
|
||||||
|
@ -88,27 +88,9 @@ export function extrude(sketch, depth) {
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(geometry, material)
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
|
|
||||||
// const material = new THREE.MeshPhongMaterial({
|
|
||||||
// color: color.mesh,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const wireframe = new THREE.WireframeGeometry( geometry );
|
|
||||||
// const mesh = new THREE.LineSegments( wireframe );
|
|
||||||
// mesh.material.depthTest = true;
|
|
||||||
// mesh.material.opacity = 0.8;
|
|
||||||
// mesh.material.transparent = true;
|
|
||||||
|
|
||||||
// const edges = new THREE.EdgesGeometry( geometry, 15 );
|
|
||||||
// edges.type = 'BufferGeometry'
|
|
||||||
// edges.parameters = undefined
|
|
||||||
|
|
||||||
// const mesh = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0x000000 } ) );
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mesh.name = 'e' + this.mid++
|
mesh.name = 'e' + this.mid++
|
||||||
// mesh.name = 'm' + sketch.obj3d.name.slice(1)
|
|
||||||
// mesh.name = 'e' + sketch.obj3d.name.slice(1)
|
|
||||||
mesh.userData.type = 'mesh'
|
mesh.userData.type = 'mesh'
|
||||||
mesh.userData.featureInfo = [sketch.obj3d.name, depth]
|
mesh.userData.featureInfo = [sketch.obj3d.name, depth]
|
||||||
mesh.layers.enable(1)
|
mesh.layers.enable(1)
|
||||||
|
@ -119,12 +101,6 @@ export function extrude(sketch, depth) {
|
||||||
|
|
||||||
mesh.add(vertices)
|
mesh.add(vertices)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mesh.matrixAutoUpdate = false;
|
mesh.matrixAutoUpdate = false;
|
||||||
mesh.matrix.multiply(sketch.obj3d.matrix)
|
mesh.matrix.multiply(sketch.obj3d.matrix)
|
||||||
|
|
||||||
|
@ -134,25 +110,22 @@ export function extrude(sketch, depth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.obj3d.add(mesh)
|
if (!refresh) {
|
||||||
|
this.obj3d.add(mesh)
|
||||||
|
|
||||||
this.store.dispatch({ type: 'rx-extrusion', mesh, sketchId: sketch.obj3d.name })
|
this.store.dispatch({ type: 'rx-extrusion', mesh, sketchId: sketch.obj3d.name })
|
||||||
|
|
||||||
// sketch.userData
|
if (this.activeSketch == sketch) {
|
||||||
if (this.activeSketch == sketch) {
|
sketch.deactivate()
|
||||||
this.activeSketch = null
|
}
|
||||||
sketch.deactivate()
|
this.render()
|
||||||
|
} else {
|
||||||
|
return mesh
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.clearSelection()
|
|
||||||
this.render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function flipBufferGeometryNormals(geometry) {
|
export function flipBufferGeometryNormals(geometry) {
|
||||||
//https://stackoverflow.com/a/54496265
|
//https://stackoverflow.com/a/54496265
|
||||||
const tempXYZ = [0, 0, 0];
|
const tempXYZ = [0, 0, 0];
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
|
|
||||||
export class DepTree {
|
export class DepTree {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
if (obj) {
|
this.order = { ...obj.order }
|
||||||
this.order = { ...obj.order }
|
this.byId = { ...obj.byId }
|
||||||
this.byId = { ...obj.byId }
|
this.allIds = [...obj.allIds]
|
||||||
this.allIds = [...obj.allIds]
|
|
||||||
|
|
||||||
this.tree = {}
|
this.tree = {}
|
||||||
for (let k in obj.tree) {
|
for (let k in obj.tree) {
|
||||||
this.tree[k] = { ...obj.tree[k] }
|
this.tree[k] = { ...obj.tree[k] }
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.tree = {}
|
|
||||||
this.order = {}
|
|
||||||
this.allIds = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addParent(id) {
|
addParent(id) {
|
||||||
|
@ -71,7 +66,7 @@ export class DepTree {
|
||||||
|
|
||||||
const deletedObj = sc.obj3d.children.splice(spliceIdx + 1, 1)[0] // first 1 elements are non geom
|
const deletedObj = sc.obj3d.children.splice(spliceIdx + 1, 1)[0] // first 1 elements are non geom
|
||||||
|
|
||||||
deletedObj.traverse((obj)=>{
|
deletedObj.traverse((obj) => {
|
||||||
if (obj.geometry) obj.geometry.dispose()
|
if (obj.geometry) obj.geometry.dispose()
|
||||||
if (obj.material) obj.material.dispose()
|
if (obj.material) obj.material.dispose()
|
||||||
})
|
})
|
||||||
|
@ -95,6 +90,9 @@ export class DepTree {
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,7 +101,7 @@ export class DepTree {
|
||||||
// const dt = new DepTree()
|
// const dt = new DepTree()
|
||||||
// dt.addParent('r1')
|
// dt.addParent('r1')
|
||||||
// dt.addParent('r2')
|
// dt.addParent('r2')
|
||||||
// dt.addChild('r3', 'r1', 'r2')
|
// dt.addChild('r3', 'r1', 'r2')s
|
||||||
// dt.addParent('r4')
|
// dt.addParent('r4')
|
||||||
// dt.addChild('r5', 'r4', 'r3')
|
// dt.addChild('r5', 'r4', 'r3')
|
||||||
// dt.addChild('r6', 'r1', 'r5')
|
// dt.addChild('r6', 'r1', 'r5')
|
||||||
|
@ -128,3 +126,6 @@ export class DepTree {
|
||||||
// allIds: [ 'r1', 'r2', 'r3', 'r4', 'r8' ]
|
// allIds: [ 'r1', 'r2', 'r3', 'r4', 'r8' ]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,21 +22,28 @@ export const Dialog = ({ dialog, setDialog }) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const extrude = () => {
|
const extrude = () => {
|
||||||
|
let sketch
|
||||||
if (sc.activeSketch) {
|
if (sc.activeSketch) {
|
||||||
sc.extrude(sc.activeSketch, ref.current.value)
|
sketch = sc.activeSketch
|
||||||
setDialog(null)
|
|
||||||
} else if (sc.selected.length === 1 && sc.selected[0].userData.type == 'sketch') {
|
} else if (sc.selected.length === 1 && sc.selected[0].userData.type == 'sketch') {
|
||||||
sc.extrude(treeEntriesById[sc.selected[0].name], ref.current.value)
|
sketch = treeEntriesById[sc.selected[0].name]
|
||||||
setDialog(null)
|
|
||||||
} else {
|
} else {
|
||||||
console.log('invalid selection')
|
console.log('invalid selection')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDialog(null)
|
||||||
|
sc.extrude(sketch, ref.current.value)
|
||||||
|
|
||||||
|
sc.render()
|
||||||
|
forceUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const [_, forceUpdate] = useReducer(x => x + 1, 0);
|
const [_, forceUpdate] = useReducer(x => x + 1, 0);
|
||||||
|
|
||||||
return <div className='dialog w-40 h-10 flex items-center bg-gray-700'>
|
return <div className='dialog w-40 h-10 flex items-center bg-gray-700'>
|
||||||
|
{/* return <div className='w-40 h-full flex items-center bg-gray-700'> */}
|
||||||
<input className='w-1/2' type="number" {...useNumField(1)} step="0.1" ref={ref} />
|
<input className='w-1/2' type="number" {...useNumField(1)} step="0.1" ref={ref} />
|
||||||
<Icon.Flip className="btn w-auto h-full p-2"
|
<Icon.Flip className="btn w-auto h-full p-2"
|
||||||
onClick={() => ref.current.value *= -1}
|
onClick={() => ref.current.value *= -1}
|
||||||
|
|
|
@ -17,8 +17,7 @@ export const NavBar = ({ setDialog }) => {
|
||||||
const boolOp = (code) => {
|
const boolOp = (code) => {
|
||||||
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return
|
if (sc.selected.length != 2 || !sc.selected.every(e => e.userData.type == 'mesh')) return
|
||||||
const [m1, m2] = sc.selected
|
const [m1, m2] = sc.selected
|
||||||
const mesh = sc.boolOp(m1, m2, code)
|
sc.boolOp(m1, m2, code)
|
||||||
dispatch({ type: 'rx-boolean', mesh, deps: [m1.name, m2.name] })
|
|
||||||
sc.render()
|
sc.render()
|
||||||
forceUpdate()
|
forceUpdate()
|
||||||
}
|
}
|
||||||
|
@ -65,8 +64,7 @@ export const NavBar = ({ setDialog }) => {
|
||||||
|
|
||||||
|
|
||||||
const btnz2 = [
|
const btnz2 = [
|
||||||
[FaEdit, addSketch, 'Sketch [s]']
|
[FaEdit, addSketch, 'Sketch [s]'],
|
||||||
,
|
|
||||||
[Icon.Extrude, extrude, 'Extrude [e]'],
|
[Icon.Extrude, extrude, 'Extrude [e]'],
|
||||||
[Icon.Union, () => boolOp('u'), 'Union'],
|
[Icon.Union, () => boolOp('u'), 'Union'],
|
||||||
[Icon.Subtract, () => boolOp('s'), 'Subtract'],
|
[Icon.Subtract, () => boolOp('s'), 'Subtract'],
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function treeEntries(state = defaultState, action) {
|
||||||
},
|
},
|
||||||
allIds: { $push: [action.mesh.name] },
|
allIds: { $push: [action.mesh.name] },
|
||||||
tree: {
|
tree: {
|
||||||
[action.sketchId]: { [action.mesh.name]: { $set: true} },
|
[action.sketchId]: { [action.mesh.name]: { $set: true } },
|
||||||
[action.mesh.name]: { $set: {} }
|
[action.mesh.name]: { $set: {} }
|
||||||
},
|
},
|
||||||
order: { [action.mesh.name]: { $set: state.allIds.length } },
|
order: { [action.mesh.name]: { $set: state.allIds.length } },
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
import React, { useReducer, useState } from 'react';
|
import React, { useReducer, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { MdVisibilityOff, MdVisibility, MdDelete } from 'react-icons/md'
|
import { MdVisibilityOff, MdVisibility, MdDelete, MdRefresh } from 'react-icons/md'
|
||||||
|
|
||||||
import { FaCube, FaEdit } from 'react-icons/fa'
|
import { FaCube, FaEdit } from 'react-icons/fa'
|
||||||
|
|
||||||
|
import { DepTree } from './depTree.mjs'
|
||||||
|
|
||||||
export const Tree = () => {
|
export const Tree = () => {
|
||||||
const treeEntries = useSelector(state => state.treeEntries)
|
const treeEntries = useSelector(state => state.treeEntries)
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ const treeIcons = {
|
||||||
|
|
||||||
const TreeEntry = ({ entId }) => {
|
const TreeEntry = ({ entId }) => {
|
||||||
|
|
||||||
|
const state = useSelector(state => state.treeEntries)
|
||||||
const treeEntries = useSelector(state => state.treeEntries.byId)
|
const treeEntries = useSelector(state => state.treeEntries.byId)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
@ -110,6 +113,14 @@ const TreeEntry = ({ entId }) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<MdRefresh className='btn-green h-full w-auto p-1.5'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
sc.refreshNode(entId)
|
||||||
|
sc.render()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
visible ?
|
visible ?
|
||||||
|
|
|
@ -166,7 +166,7 @@ function setHover(obj, state, meshHover = true) {
|
||||||
break;
|
break;
|
||||||
case 'mesh':
|
case 'mesh':
|
||||||
if (meshHover) {
|
if (meshHover) {
|
||||||
// obj.material.emissive.set(colObj.emissive)
|
obj.material.emissive.set(colObj.emissive)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
5
todo.txt
5
todo.txt
|
@ -30,16 +30,15 @@ tangent // done to the best of my ability
|
||||||
|
|
||||||
extrude dialogue / done
|
extrude dialogue / done
|
||||||
better default ent names / done
|
better default ent names / done
|
||||||
|
loopfind especially arc, // fixed for single looop, good enough, maybe stretch goal of selecting search start pt
|
||||||
|
dim tag delete //resolved
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-unable to delete arc
|
-unable to delete arc
|
||||||
hover not clearing sometimes in sketch
|
hover not clearing sometimes in sketch
|
||||||
dim tags are not clearing
|
|
||||||
|
|
||||||
|
|
||||||
auto update extrude
|
auto update extrude
|
||||||
loopfind especially arc
|
|
||||||
file save, stl export
|
file save, stl export
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue