drag drop update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
(() => {
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const MIN_DRAW_SIZE = 4;
|
||||
|
||||
function initEditor() {
|
||||
const editor = document.getElementById('device-type-shape-editor');
|
||||
@@ -38,7 +39,13 @@
|
||||
font_size: editor.querySelector('[data-field="font_size"]')
|
||||
};
|
||||
|
||||
let draggedTemplate = null;
|
||||
let activeToolType = null;
|
||||
let drawState = {
|
||||
active: false,
|
||||
shapeId: null,
|
||||
startX: 0,
|
||||
startY: 0
|
||||
};
|
||||
let dragState = {
|
||||
active: false,
|
||||
shapeId: null,
|
||||
@@ -48,63 +55,21 @@
|
||||
let selectedShapeId = null;
|
||||
let shapes = normalizeShapeList(readJson(hiddenInput.value));
|
||||
|
||||
bindToolbarDragEvents(editor);
|
||||
bindCanvasDropEvents(svg);
|
||||
bindToolbarEvents(editor);
|
||||
bindCanvasPointerEvents(svg);
|
||||
bindOverlayEvents(overlay);
|
||||
render();
|
||||
|
||||
function bindToolbarDragEvents(root) {
|
||||
function bindToolbarEvents(root) {
|
||||
const tools = root.querySelectorAll('.shape-tool[data-shape-template]');
|
||||
tools.forEach((tool) => {
|
||||
tool.addEventListener('dragstart', (event) => {
|
||||
draggedTemplate = String(tool.dataset.shapeTemplate || '').trim();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', draggedTemplate);
|
||||
}
|
||||
tool.addEventListener('click', () => {
|
||||
const toolType = String(tool.dataset.shapeTemplate || '').trim();
|
||||
activeToolType = activeToolType === toolType ? null : toolType;
|
||||
drawState.active = false;
|
||||
drawState.shapeId = null;
|
||||
renderToolState();
|
||||
});
|
||||
|
||||
tool.addEventListener('dragend', () => {
|
||||
draggedTemplate = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindCanvasDropEvents(canvas) {
|
||||
const canvasWrap = canvas.closest('.shape-editor-canvas');
|
||||
if (!canvasWrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasWrap.addEventListener('dragover', (event) => {
|
||||
if (!draggedTemplate) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
canvasWrap.classList.add('drag-over');
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
});
|
||||
|
||||
canvasWrap.addEventListener('dragleave', () => {
|
||||
canvasWrap.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
canvasWrap.addEventListener('drop', (event) => {
|
||||
if (!draggedTemplate) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
canvasWrap.classList.remove('drag-over');
|
||||
|
||||
const point = toSvgPoint(event, canvas);
|
||||
const shape = createDefaultShape(draggedTemplate, point.x, point.y);
|
||||
shapes.push(shape);
|
||||
selectedShapeId = shape.id;
|
||||
persist();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,6 +79,34 @@
|
||||
if (!(target instanceof SVGElement)) {
|
||||
return;
|
||||
}
|
||||
const point = toSvgPoint(event, canvas);
|
||||
|
||||
if (activeToolType) {
|
||||
const shape = createDefaultShape(activeToolType, point.x, point.y);
|
||||
shapes.push(shape);
|
||||
selectedShapeId = shape.id;
|
||||
|
||||
if (activeToolType === 'text') {
|
||||
drawState.active = false;
|
||||
drawState.shapeId = null;
|
||||
persist();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
drawState = {
|
||||
active: true,
|
||||
shapeId: shape.id,
|
||||
startX: Math.round(point.x),
|
||||
startY: Math.round(point.y)
|
||||
};
|
||||
|
||||
const shapeElement = target.closest('[data-shape-id]') || canvas;
|
||||
shapeElement.setPointerCapture(event.pointerId);
|
||||
persist();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
const shapeElement = target.closest('[data-shape-id]');
|
||||
if (!shapeElement) {
|
||||
@@ -131,7 +124,6 @@
|
||||
selectedShapeId = shape.id;
|
||||
renderOverlay();
|
||||
|
||||
const point = toSvgPoint(event, canvas);
|
||||
const anchor = getShapeAnchor(shape);
|
||||
dragState = {
|
||||
active: true,
|
||||
@@ -144,6 +136,18 @@
|
||||
});
|
||||
|
||||
canvas.addEventListener('pointermove', (event) => {
|
||||
if (drawState.active && drawState.shapeId) {
|
||||
const shape = findShape(drawState.shapeId);
|
||||
if (!shape) {
|
||||
return;
|
||||
}
|
||||
const point = toSvgPoint(event, canvas);
|
||||
updateShapeFromDiagonal(shape, drawState.startX, drawState.startY, point.x, point.y);
|
||||
persist();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dragState.active || !dragState.shapeId) {
|
||||
return;
|
||||
}
|
||||
@@ -162,13 +166,31 @@
|
||||
});
|
||||
|
||||
canvas.addEventListener('pointerup', () => {
|
||||
if (drawState.active && drawState.shapeId) {
|
||||
const shape = findShape(drawState.shapeId);
|
||||
if (shape && isBelowMinDrawSize(shape)) {
|
||||
shapes = shapes.filter((candidate) => candidate.id !== shape.id);
|
||||
if (selectedShapeId === shape.id) {
|
||||
selectedShapeId = null;
|
||||
}
|
||||
}
|
||||
drawState.active = false;
|
||||
drawState.shapeId = null;
|
||||
}
|
||||
|
||||
dragState.active = false;
|
||||
dragState.shapeId = null;
|
||||
persist();
|
||||
render();
|
||||
});
|
||||
|
||||
canvas.addEventListener('pointercancel', () => {
|
||||
drawState.active = false;
|
||||
drawState.shapeId = null;
|
||||
dragState.active = false;
|
||||
dragState.shapeId = null;
|
||||
persist();
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,10 +247,19 @@
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderToolState();
|
||||
renderCanvas();
|
||||
renderOverlay();
|
||||
}
|
||||
|
||||
function renderToolState() {
|
||||
editor.querySelectorAll('.shape-tool[data-shape-template]').forEach((tool) => {
|
||||
const type = String(tool.dataset.shapeTemplate || '').trim();
|
||||
tool.classList.toggle('is-active', activeToolType === type);
|
||||
});
|
||||
svg.classList.toggle('shape-tool-active', !!activeToolType);
|
||||
}
|
||||
|
||||
function renderCanvas() {
|
||||
svg.querySelectorAll('.shape-object').forEach((el) => el.remove());
|
||||
|
||||
@@ -299,6 +330,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateShapeFromDiagonal(shape, x1, y1, x2, y2) {
|
||||
const left = Math.min(x1, x2);
|
||||
const top = Math.min(y1, y2);
|
||||
const width = Math.abs(x2 - x1);
|
||||
const height = Math.abs(y2 - y1);
|
||||
|
||||
if (shape.type === 'rect') {
|
||||
shape.x = Math.round(left);
|
||||
shape.y = Math.round(top);
|
||||
shape.width = Math.round(width);
|
||||
shape.height = Math.round(height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shape.type === 'circle') {
|
||||
const radius = Math.round(Math.min(width, height) / 2);
|
||||
shape.radius = radius;
|
||||
shape.x = Math.round(left + radius);
|
||||
shape.y = Math.round(top + radius);
|
||||
}
|
||||
}
|
||||
|
||||
function isBelowMinDrawSize(shape) {
|
||||
if (shape.type === 'rect') {
|
||||
return (shape.width ?? 0) < MIN_DRAW_SIZE || (shape.height ?? 0) < MIN_DRAW_SIZE;
|
||||
}
|
||||
if (shape.type === 'circle') {
|
||||
return (shape.radius ?? 0) < (MIN_DRAW_SIZE / 2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createSvgShapeElement(shape) {
|
||||
const fill = normalizeColor(shape.fill, '#cccccc');
|
||||
const stroke = normalizeColor(shape.stroke, '#333333');
|
||||
@@ -381,7 +444,7 @@
|
||||
y: Math.round(y),
|
||||
width: 0,
|
||||
height: 0,
|
||||
radius: 26,
|
||||
radius: 1,
|
||||
text: '',
|
||||
fontSize: 16,
|
||||
fill: '#cfe6ff',
|
||||
@@ -416,8 +479,8 @@
|
||||
type: 'rect',
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
width: 120,
|
||||
height: 60,
|
||||
width: 1,
|
||||
height: 1,
|
||||
radius: 0,
|
||||
text: '',
|
||||
fontSize: 16,
|
||||
|
||||
471
app/assets/js/floor-svg-editor.js
Normal file
471
app/assets/js/floor-svg-editor.js
Normal file
@@ -0,0 +1,471 @@
|
||||
(() => {
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const VIEWBOX_WIDTH = 2000;
|
||||
const VIEWBOX_HEIGHT = 1000;
|
||||
const SNAP_TOLERANCE = 12;
|
||||
|
||||
function initFloorSvgEditor() {
|
||||
const editor = document.getElementById('floor-svg-editor');
|
||||
const svg = document.getElementById('floor-svg-canvas');
|
||||
const hiddenInput = document.getElementById('floor-svg-content');
|
||||
if (!editor || !svg || !hiddenInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controls = {
|
||||
startPolyline: document.getElementById('floor-start-polyline'),
|
||||
finishPolyline: document.getElementById('floor-finish-polyline'),
|
||||
deletePolyline: document.getElementById('floor-delete-polyline'),
|
||||
clearDrawing: document.getElementById('floor-clear-drawing'),
|
||||
lock45: document.getElementById('floor-lock-45'),
|
||||
snapGuides: document.getElementById('floor-snap-guides'),
|
||||
addGuide: document.getElementById('floor-add-guide'),
|
||||
guideOrientation: document.getElementById('floor-guide-orientation'),
|
||||
guidePosition: document.getElementById('floor-guide-position'),
|
||||
guideList: document.getElementById('floor-guide-list')
|
||||
};
|
||||
|
||||
const state = {
|
||||
polylines: [],
|
||||
guides: [],
|
||||
selectedPolylineId: null,
|
||||
activePolylineId: null,
|
||||
draggingVertex: null
|
||||
};
|
||||
|
||||
loadFromExistingSvg(hiddenInput.value, state);
|
||||
bindControlEvents(controls, state, svg, hiddenInput);
|
||||
bindCanvasEvents(svg, controls, state, hiddenInput);
|
||||
render(svg, controls, state, hiddenInput);
|
||||
}
|
||||
|
||||
function bindControlEvents(controls, state, svg, hiddenInput) {
|
||||
controls.startPolyline.addEventListener('click', () => {
|
||||
const id = createId('poly');
|
||||
state.polylines.push({
|
||||
id,
|
||||
points: []
|
||||
});
|
||||
state.activePolylineId = id;
|
||||
state.selectedPolylineId = id;
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
controls.finishPolyline.addEventListener('click', () => {
|
||||
finishActivePolyline(state);
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
controls.deletePolyline.addEventListener('click', () => {
|
||||
if (!state.selectedPolylineId) {
|
||||
return;
|
||||
}
|
||||
state.polylines = state.polylines.filter((line) => line.id !== state.selectedPolylineId);
|
||||
if (state.activePolylineId === state.selectedPolylineId) {
|
||||
state.activePolylineId = null;
|
||||
}
|
||||
state.selectedPolylineId = null;
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
controls.clearDrawing.addEventListener('click', () => {
|
||||
state.polylines = [];
|
||||
state.guides = [];
|
||||
state.selectedPolylineId = null;
|
||||
state.activePolylineId = null;
|
||||
state.draggingVertex = null;
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
controls.addGuide.addEventListener('click', () => {
|
||||
const orientation = controls.guideOrientation.value === 'horizontal' ? 'horizontal' : 'vertical';
|
||||
const position = Number(controls.guidePosition.value);
|
||||
if (!Number.isFinite(position)) {
|
||||
return;
|
||||
}
|
||||
state.guides.push({
|
||||
id: createId('guide'),
|
||||
orientation,
|
||||
position: Math.round(position)
|
||||
});
|
||||
controls.guidePosition.value = '';
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
controls.guideList.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const id = target.getAttribute('data-remove-guide');
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
state.guides = state.guides.filter((guide) => guide.id !== id);
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
}
|
||||
|
||||
function bindCanvasEvents(svg, controls, state, hiddenInput) {
|
||||
svg.addEventListener('pointerdown', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof SVGElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vertex = target.closest('[data-vertex-index]');
|
||||
if (vertex) {
|
||||
const polylineId = vertex.getAttribute('data-polyline-id');
|
||||
const vertexIndex = Number(vertex.getAttribute('data-vertex-index'));
|
||||
if (polylineId && Number.isInteger(vertexIndex)) {
|
||||
state.selectedPolylineId = polylineId;
|
||||
state.draggingVertex = { polylineId, vertexIndex };
|
||||
vertex.setPointerCapture(event.pointerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const polylineEl = target.closest('[data-polyline-id]');
|
||||
if (polylineEl) {
|
||||
const polylineId = polylineEl.getAttribute('data-polyline-id');
|
||||
if (polylineId) {
|
||||
state.selectedPolylineId = polylineId;
|
||||
if (!state.activePolylineId) {
|
||||
state.activePolylineId = polylineId;
|
||||
}
|
||||
render(svg, controls, state, hiddenInput);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const point = toSvgPoint(svg, event);
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.activePolylineId) {
|
||||
const id = createId('poly');
|
||||
state.polylines.push({ id, points: [] });
|
||||
state.activePolylineId = id;
|
||||
state.selectedPolylineId = id;
|
||||
}
|
||||
|
||||
const activeLine = state.polylines.find((line) => line.id === state.activePolylineId);
|
||||
if (!activeLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextPoint = point;
|
||||
if (controls.lock45.checked && activeLine.points.length > 0) {
|
||||
nextPoint = lockTo45(activeLine.points[activeLine.points.length - 1], nextPoint);
|
||||
}
|
||||
if (controls.snapGuides.checked) {
|
||||
nextPoint = snapPointToGuides(nextPoint, state.guides, SNAP_TOLERANCE);
|
||||
}
|
||||
|
||||
activeLine.points.push({
|
||||
x: Math.round(nextPoint.x),
|
||||
y: Math.round(nextPoint.y)
|
||||
});
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
svg.addEventListener('pointermove', (event) => {
|
||||
if (!state.draggingVertex) {
|
||||
return;
|
||||
}
|
||||
const point = toSvgPoint(svg, event);
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = state.polylines.find((item) => item.id === state.draggingVertex.polylineId);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
const index = state.draggingVertex.vertexIndex;
|
||||
if (!line.points[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextPoint = point;
|
||||
if (controls.lock45.checked && index > 0 && line.points[index - 1]) {
|
||||
nextPoint = lockTo45(line.points[index - 1], nextPoint);
|
||||
}
|
||||
if (controls.snapGuides.checked) {
|
||||
nextPoint = snapPointToGuides(nextPoint, state.guides, SNAP_TOLERANCE);
|
||||
}
|
||||
|
||||
line.points[index] = {
|
||||
x: Math.round(nextPoint.x),
|
||||
y: Math.round(nextPoint.y)
|
||||
};
|
||||
render(svg, controls, state, hiddenInput);
|
||||
});
|
||||
|
||||
svg.addEventListener('pointerup', () => {
|
||||
state.draggingVertex = null;
|
||||
});
|
||||
svg.addEventListener('pointercancel', () => {
|
||||
state.draggingVertex = null;
|
||||
});
|
||||
}
|
||||
|
||||
function render(svg, controls, state, hiddenInput) {
|
||||
const selected = state.polylines.find((line) => line.id === state.selectedPolylineId) || null;
|
||||
|
||||
svg.innerHTML = '';
|
||||
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
||||
|
||||
const background = createSvgElement('rect');
|
||||
background.setAttribute('x', '0');
|
||||
background.setAttribute('y', '0');
|
||||
background.setAttribute('width', String(VIEWBOX_WIDTH));
|
||||
background.setAttribute('height', String(VIEWBOX_HEIGHT));
|
||||
background.setAttribute('fill', '#fafafa');
|
||||
background.setAttribute('stroke', '#e1e1e1');
|
||||
background.setAttribute('stroke-width', '1');
|
||||
svg.appendChild(background);
|
||||
|
||||
state.guides.forEach((guide) => {
|
||||
const line = createSvgElement('line');
|
||||
if (guide.orientation === 'horizontal') {
|
||||
line.setAttribute('x1', '0');
|
||||
line.setAttribute('y1', String(guide.position));
|
||||
line.setAttribute('x2', String(VIEWBOX_WIDTH));
|
||||
line.setAttribute('y2', String(guide.position));
|
||||
} else {
|
||||
line.setAttribute('x1', String(guide.position));
|
||||
line.setAttribute('y1', '0');
|
||||
line.setAttribute('x2', String(guide.position));
|
||||
line.setAttribute('y2', String(VIEWBOX_HEIGHT));
|
||||
}
|
||||
line.setAttribute('stroke', '#8f8f8f');
|
||||
line.setAttribute('stroke-width', '1');
|
||||
line.setAttribute('stroke-dasharray', '8 6');
|
||||
line.setAttribute('opacity', '0.8');
|
||||
line.setAttribute('data-guide-id', guide.id);
|
||||
svg.appendChild(line);
|
||||
});
|
||||
|
||||
state.polylines.forEach((polyline) => {
|
||||
const line = createSvgElement('polyline');
|
||||
line.setAttribute('fill', 'none');
|
||||
line.setAttribute('stroke', polyline.id === state.selectedPolylineId ? '#007bff' : '#1f2937');
|
||||
line.setAttribute('stroke-width', polyline.id === state.selectedPolylineId ? '5' : '3');
|
||||
line.setAttribute('points', polyline.points.map((point) => `${point.x},${point.y}`).join(' '));
|
||||
line.setAttribute('data-polyline-id', polyline.id);
|
||||
svg.appendChild(line);
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
selected.points.forEach((point, index) => {
|
||||
const vertex = createSvgElement('circle');
|
||||
vertex.setAttribute('cx', String(point.x));
|
||||
vertex.setAttribute('cy', String(point.y));
|
||||
vertex.setAttribute('r', '8');
|
||||
vertex.setAttribute('fill', '#ffffff');
|
||||
vertex.setAttribute('stroke', '#dc3545');
|
||||
vertex.setAttribute('stroke-width', '3');
|
||||
vertex.setAttribute('data-polyline-id', selected.id);
|
||||
vertex.setAttribute('data-vertex-index', String(index));
|
||||
svg.appendChild(vertex);
|
||||
});
|
||||
}
|
||||
|
||||
controls.guideList.innerHTML = '';
|
||||
state.guides.forEach((guide) => {
|
||||
const li = document.createElement('li');
|
||||
const label = guide.orientation === 'horizontal' ? 'Horizontal' : 'Vertikal';
|
||||
li.innerHTML = `
|
||||
<span>${label}: ${Math.round(guide.position)}</span>
|
||||
<button type="button" class="button button-danger" data-remove-guide="${guide.id}">X</button>
|
||||
`;
|
||||
controls.guideList.appendChild(li);
|
||||
});
|
||||
|
||||
hiddenInput.value = buildSvgMarkup(state.polylines, state.guides);
|
||||
}
|
||||
|
||||
function finishActivePolyline(state) {
|
||||
const active = state.polylines.find((line) => line.id === state.activePolylineId);
|
||||
if (active && active.points.length < 2) {
|
||||
state.polylines = state.polylines.filter((line) => line.id !== active.id);
|
||||
if (state.selectedPolylineId === active.id) {
|
||||
state.selectedPolylineId = null;
|
||||
}
|
||||
}
|
||||
state.activePolylineId = null;
|
||||
}
|
||||
|
||||
function loadFromExistingSvg(raw, state) {
|
||||
const content = String(raw || '').trim();
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(content, 'image/svg+xml');
|
||||
const root = doc.documentElement;
|
||||
if (!root || root.nodeName.toLowerCase() === 'parsererror') {
|
||||
return;
|
||||
}
|
||||
|
||||
root.querySelectorAll('line[data-guide="1"], line.floor-guide').forEach((line) => {
|
||||
const orientation = line.getAttribute('data-orientation') === 'horizontal' ? 'horizontal' : 'vertical';
|
||||
const position = orientation === 'horizontal'
|
||||
? Number(line.getAttribute('y1'))
|
||||
: Number(line.getAttribute('x1'));
|
||||
if (Number.isFinite(position)) {
|
||||
state.guides.push({
|
||||
id: createId('guide'),
|
||||
orientation,
|
||||
position: Math.round(position)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
root.querySelectorAll('polyline').forEach((polyline) => {
|
||||
const pointsAttr = polyline.getAttribute('points') || '';
|
||||
const points = parsePoints(pointsAttr);
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
state.polylines.push({
|
||||
id: createId('poly'),
|
||||
points
|
||||
});
|
||||
});
|
||||
|
||||
if (state.polylines.length > 0) {
|
||||
state.selectedPolylineId = state.polylines[0].id;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore invalid svg content
|
||||
}
|
||||
}
|
||||
|
||||
function parsePoints(pointsAttr) {
|
||||
return pointsAttr
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((pair) => {
|
||||
const [x, y] = pair.split(',').map((value) => Number(value));
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
return null;
|
||||
}
|
||||
return { x: Math.round(x), y: Math.round(y) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildSvgMarkup(polylines, guides) {
|
||||
const svg = createSvgElement('svg');
|
||||
svg.setAttribute('xmlns', SVG_NS);
|
||||
svg.setAttribute('viewBox', `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`);
|
||||
|
||||
const background = createSvgElement('rect');
|
||||
background.setAttribute('x', '0');
|
||||
background.setAttribute('y', '0');
|
||||
background.setAttribute('width', String(VIEWBOX_WIDTH));
|
||||
background.setAttribute('height', String(VIEWBOX_HEIGHT));
|
||||
background.setAttribute('fill', '#fafafa');
|
||||
background.setAttribute('stroke', '#e1e1e1');
|
||||
background.setAttribute('stroke-width', '1');
|
||||
svg.appendChild(background);
|
||||
|
||||
guides.forEach((guide) => {
|
||||
const line = createSvgElement('line');
|
||||
if (guide.orientation === 'horizontal') {
|
||||
line.setAttribute('x1', '0');
|
||||
line.setAttribute('y1', String(guide.position));
|
||||
line.setAttribute('x2', String(VIEWBOX_WIDTH));
|
||||
line.setAttribute('y2', String(guide.position));
|
||||
} else {
|
||||
line.setAttribute('x1', String(guide.position));
|
||||
line.setAttribute('y1', '0');
|
||||
line.setAttribute('x2', String(guide.position));
|
||||
line.setAttribute('y2', String(VIEWBOX_HEIGHT));
|
||||
}
|
||||
line.setAttribute('stroke', '#8f8f8f');
|
||||
line.setAttribute('stroke-width', '1');
|
||||
line.setAttribute('stroke-dasharray', '8 6');
|
||||
line.setAttribute('class', 'floor-guide');
|
||||
line.setAttribute('data-guide', '1');
|
||||
line.setAttribute('data-orientation', guide.orientation);
|
||||
svg.appendChild(line);
|
||||
});
|
||||
|
||||
polylines.forEach((polyline) => {
|
||||
if (polyline.points.length < 2) {
|
||||
return;
|
||||
}
|
||||
const line = createSvgElement('polyline');
|
||||
line.setAttribute('fill', 'none');
|
||||
line.setAttribute('stroke', '#1f2937');
|
||||
line.setAttribute('stroke-width', '3');
|
||||
line.setAttribute('class', 'floor-polyline');
|
||||
line.setAttribute('points', polyline.points.map((point) => `${point.x},${point.y}`).join(' '));
|
||||
svg.appendChild(line);
|
||||
});
|
||||
|
||||
return new XMLSerializer().serializeToString(svg);
|
||||
}
|
||||
|
||||
function lockTo45(origin, point) {
|
||||
const dx = point.x - origin.x;
|
||||
const dy = point.y - origin.y;
|
||||
const length = Math.sqrt((dx * dx) + (dy * dy));
|
||||
if (length === 0) {
|
||||
return { x: origin.x, y: origin.y };
|
||||
}
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const step = Math.PI / 4;
|
||||
const snapped = Math.round(angle / step) * step;
|
||||
return {
|
||||
x: origin.x + Math.cos(snapped) * length,
|
||||
y: origin.y + Math.sin(snapped) * length
|
||||
};
|
||||
}
|
||||
|
||||
function snapPointToGuides(point, guides, tolerance) {
|
||||
let next = { ...point };
|
||||
guides.forEach((guide) => {
|
||||
if (guide.orientation === 'vertical') {
|
||||
if (Math.abs(next.x - guide.position) <= tolerance) {
|
||||
next.x = guide.position;
|
||||
}
|
||||
} else if (Math.abs(next.y - guide.position) <= tolerance) {
|
||||
next.y = guide.position;
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function toSvgPoint(svg, event) {
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = event.clientX;
|
||||
pt.y = event.clientY;
|
||||
const ctm = svg.getScreenCTM();
|
||||
if (!ctm) {
|
||||
return null;
|
||||
}
|
||||
const transformed = pt.matrixTransform(ctm.inverse());
|
||||
return {
|
||||
x: transformed.x,
|
||||
y: transformed.y
|
||||
};
|
||||
}
|
||||
|
||||
function createSvgElement(name) {
|
||||
return document.createElementNS(SVG_NS, name);
|
||||
}
|
||||
|
||||
function createId(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initFloorSvgEditor);
|
||||
})();
|
||||
Reference in New Issue
Block a user