diff --git a/dist/__snapshots__/oscd-designer.spec.snap.js b/dist/__snapshots__/oscd-designer.spec.snap.js
index a614d36..35c4fdd 100644
--- a/dist/__snapshots__/oscd-designer.spec.snap.js
+++ b/dist/__snapshots__/oscd-designer.spec.snap.js
@@ -334,7 +334,7 @@ snapshots["Designer given conducting equipment connects equipment on connection
`;
/* end snapshot Designer given conducting equipment connects equipment on connection point and equipment click */
-snapshots["Designer given conducting equipment with established connectivity uniquely names new connectivity nodes"] =
+snapshots["Designer given conducting equipment connects equipment on connect menu item select"] =
`
-
-
@@ -431,11 +426,21 @@ snapshots["Designer given conducting equipment with established connectivity uni
>
+
+
+
+
@@ -464,11 +469,11 @@ snapshots["Designer given conducting equipment with established connectivity uni
>
@@ -481,11 +486,20 @@ snapshots["Designer given conducting equipment with established connectivity uni
>
+
+
@@ -496,6 +510,15 @@ snapshots["Designer given conducting equipment with established connectivity uni
name="NEW1"
type="NEW"
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
-/* end snapshot Designer given conducting equipment with established connectivity uniquely names new connectivity nodes */
+/* end snapshot Designer given conducting equipment connects equipment on connect menu item select */
-snapshots["Designer given conducting equipment with established connectivity connects equipment on connection point and connectivity node click"] =
+snapshots["Designer given conducting equipment with established connectivity uniquely names new connectivity nodes"] =
`
+
+
-
+
+
+
+
@@ -650,8 +758,8 @@ snapshots["Designer given conducting equipment with established connectivity con
>
`;
-/* end snapshot Designer given conducting equipment with established connectivity connects equipment on connection point and connectivity node click */
+/* end snapshot Designer given conducting equipment with established connectivity uniquely names new connectivity nodes */
-snapshots["Designer given conducting equipment with established connectivity avoids short circuit connections"] =
+snapshots["Designer given conducting equipment with established connectivity connects equipment on connection point and connectivity node click"] =
`
+
+
+
+
+
`;
-/* end snapshot Designer given conducting equipment with established connectivity avoids short circuit connections */
+/* end snapshot Designer given conducting equipment with established connectivity connects equipment on connection point and connectivity node click */
-snapshots["Designer given conducting equipment with established connectivity keeps connection paths simple"] =
+snapshots["Designer given conducting equipment with established connectivity avoids short circuit connections"] =
`
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+/* end snapshot Designer given conducting equipment with established connectivity avoids short circuit connections */
+
+snapshots["Designer given conducting equipment with established connectivity keeps connection paths simple"] =
+`
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2333,7 +2627,38 @@ snapshots["Designer given conducting equipment with established connectivity bet
`;
/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment removes superfluous connectivity nodes when disconnecting */
-snapshots["Designer given conducting equipment connects equipment on connect menu item select"] =
+snapshots["Designer given a voltage level allows placing a new bus bar"] =
+`
+
+
+
+
+
+
+`;
+/* end snapshot Designer given a voltage level allows placing a new bus bar */
+
+snapshots["Designer given conducting equipment with established connectivity between more than two pieces of equipment keeps internal connectivity nodes when moving containers"] =
`
+
+
+
+
+
+
+
+
+
+
+
+
+ esld:x="18.5"
+ esld:y="7.5"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment keeps internal connectivity nodes when moving containers */
+
+snapshots["Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar keeps the bus bar when moving containers"] =
+`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar keeps the bus bar when moving containers */
+
+snapshots["Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar resizes the bus bar on first menu item select"] =
+`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar resizes the bus bar on first menu item select */
+
+snapshots["Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar moves the bus bar on first menu item select"] =
+`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar moves the bus bar on first menu item select */
+
+snapshots["Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar removes the bus bar on third menu item select"] =
+`
+
+
+
+
+
+
+
+
+
+
@@ -2475,6 +3656,15 @@ snapshots["Designer given conducting equipment connects equipment on connect men
voltageLevelName="V2"
>
+
+
-
-
@@ -2509,15 +3690,6 @@ snapshots["Designer given conducting equipment connects equipment on connect men
name="NEW1"
type="NEW"
>
-
-
-
-
-
-
+
+
-
-
-
-
-
`;
-/* end snapshot Designer given conducting equipment connects equipment on connect menu item select */
+/* end snapshot Designer given conducting equipment with established connectivity between more than two pieces of equipment and a bus bar removes the bus bar on third menu item select */
diff --git a/icons.ts b/icons.ts
index 7295f23..a2857f4 100644
--- a/icons.ts
+++ b/icons.ts
@@ -1,12 +1,10 @@
-import { svg, TemplateResult } from 'lit';
+import { html, svg, TemplateResult } from 'lit';
export const resizePath = svg` `;
-export const movePath = svg` `;
+export const movePath = svg` `;
export const voltageLevelIcon = svg`
`;
-export function equipmentPath(equipmentType: string): TemplateResult<2> {
- if (equipmentType in equipmentPaths) return equipmentPaths[equipmentType]!;
+export function equipmentPath(equipmentType: string | null): TemplateResult<2> {
+ if (equipmentType && equipmentType in equipmentPaths)
+ return equipmentPaths[equipmentType]!;
return defaultEquipmentPath;
}
-export function equipmentGraphic(equipmentType: string): TemplateResult<2> {
- return svg`
-${equipmentPath(equipmentType)}
- `;
+export function equipmentGraphic(
+ equipmentType: string | null
+): TemplateResult<1> {
+ return html`
+ ${equipmentPath(equipmentType)}
+ `;
}
-export function equipmentIcon(equipmentType: string): TemplateResult<2> {
- return svg`
-${equipmentPath(equipmentType)}
- `;
+export function equipmentIcon(equipmentType: string): TemplateResult<1> {
+ return html`
+ ${equipmentPath(equipmentType)}
+ `;
}
function equipmentSymbol(equipmentType: string): TemplateResult<2> {
return svg`
-${equipmentPath(equipmentType)}
- `;
+ id="${equipmentType}"
+ viewBox="0 0 25 25"
+ width="1" height="1"
+ >
+ ${equipmentPath(equipmentType)}
+ `;
}
export const connectivityNodeMarker = svg` {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle =
- sldEditor.shadowRoot!.querySelectorAll('a.handle')[1];
+ sldEditor.shadowRoot!.querySelectorAll('.handle')[1];
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('resizing')
@@ -308,7 +308,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle =
- sldEditor.shadowRoot!.querySelector('a.handle')!;
+ sldEditor.shadowRoot!.querySelector('.handle')!;
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('placing')
@@ -325,7 +325,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle =
- sldEditor.shadowRoot!.querySelector('a.handle')!;
+ sldEditor.shadowRoot!.querySelector('.handle')!;
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('placing')
@@ -355,6 +355,27 @@ describe('Designer', () => {
expect(bay).to.have.attribute('w', '7');
expect(bay).to.have.attribute('h', '8');
});
+
+ it('allows placing a new bus bar', async () => {
+ element
+ .shadowRoot!.querySelector('[label="Add Bus Bar"]')
+ ?.click();
+ expect(element).property('placing').to.have.property('tagName', 'Bay');
+ const sldEditor =
+ element.shadowRoot!.querySelector('sld-editor')!;
+ await sendMouse({ type: 'click', position: [200, 200] });
+ expect(element).to.have.property('placing', undefined);
+ expect(element).property('resizing').to.have.property('tagName', 'Bay');
+ await sendMouse({ type: 'click', position: [400, 400] });
+ expect(sldEditor).to.have.property('resizing', undefined);
+ const bus = element.doc.querySelector('Bay');
+ expect(bus).to.exist;
+ expect(bus).to.have.attribute('x', '5');
+ expect(bus).to.have.attribute('y', '3');
+ expect(bus).to.have.attribute('smth:w', '1');
+ expect(bus).to.have.attribute('h', '8');
+ expect(bus).dom.to.equalSnapshot();
+ });
});
describe('given a bay', () => {
@@ -371,7 +392,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle =
- sldEditor.shadowRoot!.querySelectorAll('g.bay a.handle')[1];
+ sldEditor.shadowRoot!.querySelectorAll('g.bay .handle')[1];
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('resizing')
@@ -388,7 +409,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle =
- sldEditor.shadowRoot!.querySelectorAll('g.bay a.handle')[1];
+ sldEditor.shadowRoot!.querySelectorAll('g.bay .handle')[1];
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('resizing')
@@ -405,7 +426,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
const moveHandle = sldEditor.shadowRoot!.querySelectorAll(
- 'g.voltagelevel > a.handle'
+ 'g.voltagelevel > .handle'
)[1];
moveHandle.dispatchEvent(new PointerEvent('click'));
expect(element)
@@ -423,7 +444,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
sldEditor
- .shadowRoot!.querySelector('g.bay a.handle')!
+ .shadowRoot!.querySelector('g.bay .handle')!
.dispatchEvent(new PointerEvent('click'));
expect(element)
.property('placing')
@@ -438,7 +459,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
sldEditor
- .shadowRoot!.querySelector('g.bay a.handle')!
+ .shadowRoot!.querySelector('g.bay .handle')!
.dispatchEvent(new PointerEvent('click'));
const bay = element.placing!;
expect(bay.parentElement).to.have.attribute('name', 'V1');
@@ -450,7 +471,7 @@ describe('Designer', () => {
expect(bay.parentElement).to.have.attribute('name', 'V2');
expect(bay).to.have.attribute('name', 'B2');
sldEditor
- .shadowRoot!.querySelector('g.bay a.handle')!
+ .shadowRoot!.querySelector('g.bay .handle')!
.dispatchEvent(new PointerEvent('click'));
await sendMouse({ type: 'click', position: [200, 200] });
expect(bay).to.have.attribute('esld:x', '5');
@@ -463,7 +484,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
sldEditor
- .shadowRoot!.querySelector('g.bay a.handle')!
+ .shadowRoot!.querySelector('g.bay .handle')!
.dispatchEvent(new PointerEvent('click'));
const bay = element.placing!;
const cNode = bay.querySelector('ConnectivityNode')!;
@@ -478,7 +499,7 @@ describe('Designer', () => {
const sldEditor =
element.shadowRoot!.querySelector('sld-editor')!;
sldEditor
- .shadowRoot!.querySelector('g.voltagelevel a.handle')!
+ .shadowRoot!.querySelector('g.voltagelevel .handle')!
.dispatchEvent(new PointerEvent('click'));
const bay = element.placing!.querySelector('Bay')!;
expect(bay).to.have.attribute('esld:x', '2');
@@ -501,6 +522,60 @@ describe('Designer', () => {
expect(equipment).to.have.attribute('x', '3');
expect(equipment).to.have.attribute('y', '3');
});
+
+ describe('with a sibling bus bar', () => {
+ beforeEach(async () => {
+ element
+ .shadowRoot!.querySelector('[label="Add Bus Bar"]')
+ ?.click();
+ await sendMouse({ type: 'click', position: [200, 200] });
+ await sendMouse({ type: 'click', position: [400, 400] });
+ });
+
+ it('allows the bay to overlap its sibling bus bar', async () => {
+ const sldEditor =
+ element.shadowRoot!.querySelector('sld-editor')!;
+ const moveHandle =
+ sldEditor.shadowRoot!.querySelectorAll(
+ 'g.bay .handle'
+ )[1];
+ moveHandle.dispatchEvent(new PointerEvent('click'));
+ expect(element)
+ .property('resizing')
+ .to.exist.and.to.have.property('tagName', 'Bay');
+ const bay = element.resizing!;
+ expect(bay).to.have.attribute('esld:w', '3');
+ expect(bay).to.have.attribute('esld:h', '3');
+ await sendMouse({ type: 'click', position: [400, 400] });
+ expect(bay).to.have.attribute('esld:w', '10');
+ expect(bay).to.have.attribute('esld:h', '9');
+ });
+
+ it('moves the bus bar on left click', async () => {
+ await sendMouse({
+ type: 'click',
+ position: middleOf(querySvg({ scl: '[name="L"]' })),
+ });
+ const bus = element.doc.querySelector('[name="BB1"]');
+ expect(bus).to.have.attribute('x', '5');
+ await sendMouse({ type: 'click', position: [150, 150] });
+ expect(bus).to.have.attribute('x', '3');
+ });
+
+ it('resizes the bus bar on middle mouse button click', async () => {
+ await sendMouse({
+ type: 'click',
+ button: 'middle',
+ position: middleOf(querySvg({ scl: '[name="L"]' })),
+ });
+ const bus = element.doc.querySelector('[name="BB1"]');
+ expect(bus).to.have.attribute('esld:w', '1');
+ expect(bus).to.have.attribute('h', '8');
+ await sendMouse({ type: 'click', position: [250, 150] });
+ expect(bus).to.have.attribute('esld:w', '3');
+ expect(bus).to.have.attribute('h', '1');
+ });
+ });
});
describe('given conducting equipment', () => {
@@ -1063,9 +1138,7 @@ describe('Designer', () => {
});
it('removes contained connectivity nodes when moving containers', async () => {
- querySvg({ svg: 'a.handle' }).dispatchEvent(
- new PointerEvent('click')
- );
+ querySvg({ svg: '.handle' }).dispatchEvent(new PointerEvent('click'));
await sendMouse({ type: 'click', position: [100, 150] });
expect(
element.doc.querySelectorAll('ConnectivityNode')
@@ -1075,7 +1148,7 @@ describe('Designer', () => {
it('removes connected connectivity nodes when moving containers', async () => {
querySvg({
scl: '[name="V2"]',
- svg: 'a.handle',
+ svg: '.handle',
}).dispatchEvent(new PointerEvent('click'));
expect(
element.doc.querySelectorAll('ConnectivityNode')
@@ -1090,7 +1163,7 @@ describe('Designer', () => {
const position = middleOf(
querySvg({
scl: '[name="V2"]',
- svg: 'a.handle',
+ svg: '.handle',
})
);
await sendMouse({ position, type: 'click' });
@@ -1104,6 +1177,114 @@ describe('Designer', () => {
expect(
element.doc.querySelectorAll('ConnectivityNode')
).to.have.lengthOf(1);
+ await expect(element.doc.documentElement).dom.to.equalSnapshot();
+ });
+
+ describe('and a bus bar', () => {
+ beforeEach(async () => {
+ element
+ .shadowRoot!.querySelector('[label="Add Bus Bar"]')
+ ?.click();
+ await sendMouse({ type: 'click', position: [430, 150] });
+ await sendMouse({ type: 'click', position: [430, 230] });
+ await sendMouse({
+ type: 'click',
+ position: middleOf(querySvg({ scl: '[name="L"]' })),
+ });
+ await sendMouse({ type: 'click', position: [450, 150] });
+ querySvg({ scl: '[type="VTR"]', svg: 'circle' }).dispatchEvent(
+ new PointerEvent('click')
+ );
+ await sendMouse({
+ type: 'click',
+ position: middleOf(querySvg({ scl: '[name="L"]' })),
+ });
+ });
+
+ it('keeps the bus bar when moving containers', async () => {
+ const position = middleOf(
+ querySvg({
+ scl: '[name="V2"] > [name="B1"]',
+ svg: '.handle',
+ })
+ );
+ expect(
+ element.doc
+ .querySelector('[name="L"]')
+ ?.querySelectorAll('Section')
+ ).to.have.lengthOf(3);
+ await sendMouse({ position, type: 'click' });
+ position[1] -= 40;
+ await sendMouse({ position, type: 'click' });
+ expect(
+ element.doc
+ .querySelector('[name="L"]')
+ ?.querySelectorAll('Section')
+ ).to.have.lengthOf(1);
+ await expect(element.doc.documentElement).dom.to.equalSnapshot();
+ });
+
+ it('opens a menu on right click', async () => {
+ querySvg({
+ scl: '[name="L"]',
+ svg: 'line[stroke="none"]',
+ }).dispatchEvent(new PointerEvent('contextmenu'));
+ await element.updateComplete;
+ expect(querySvg({ svg: 'menu' })).to.exist;
+ });
+
+ it('resizes the bus bar on first menu item select', async () => {
+ querySvg({
+ scl: '[name="L"]',
+ svg: 'line[stroke="none"]',
+ }).dispatchEvent(new PointerEvent('contextmenu'));
+ await element.updateComplete;
+ const sldEditor =
+ element.shadowRoot!.querySelector('sld-editor')!;
+ sldEditor.shadowRoot!.querySelector(
+ 'mwc-list-item:nth-of-type(1)'
+ )!.selected = true;
+ const bus = element.doc.querySelector('[name="BB1"]');
+ expect(bus).to.have.attribute('h', '3');
+ await sendMouse({ type: 'click', position: [450, 150] });
+ expect(bus).to.have.attribute('h', '2');
+ await expect(element.doc.documentElement).dom.to.equalSnapshot();
+ });
+
+ it('moves the bus bar on first menu item select', async () => {
+ querySvg({
+ scl: '[name="L"]',
+ svg: 'line[stroke="none"]',
+ }).dispatchEvent(new PointerEvent('contextmenu'));
+ await element.updateComplete;
+ const sldEditor =
+ element.shadowRoot!.querySelector('sld-editor')!;
+ sldEditor.shadowRoot!.querySelector(
+ 'mwc-list-item:nth-of-type(2)'
+ )!.selected = true;
+ const bus = element.doc.querySelector('[name="BB1"]');
+ expect(bus).to.have.attribute('y', '2');
+ await sendMouse({ type: 'click', position: [430, 400] });
+ expect(bus).to.have.attribute('y', '10');
+ await expect(element.doc.documentElement).dom.to.equalSnapshot();
+ });
+
+ it('removes the bus bar on third menu item select', async () => {
+ querySvg({
+ scl: '[name="L"]',
+ svg: 'line[stroke="none"]',
+ }).dispatchEvent(new PointerEvent('contextmenu'));
+ await element.updateComplete;
+ expect(element.doc.querySelector('[name="BB1"]')).to.exist;
+ const sldEditor =
+ element.shadowRoot!.querySelector('sld-editor')!;
+ sldEditor.shadowRoot!.querySelector(
+ 'mwc-list-item:nth-of-type(3)'
+ )!.selected = true;
+ await sldEditor.updateComplete;
+ expect(element.doc.querySelector('[name="BB1"]')).to.not.exist;
+ await expect(element.doc.documentElement).dom.to.equalSnapshot();
+ });
});
});
});
diff --git a/oscd-designer.ts b/oscd-designer.ts
index 84d4f33..e12590e 100644
--- a/oscd-designer.ts
+++ b/oscd-designer.ts
@@ -19,6 +19,7 @@ import {
ConnectEvent,
connectionStartPoints,
elementPath,
+ isBusBar,
PlaceEvent,
Point,
privType,
@@ -33,6 +34,33 @@ import {
xmlnsNs,
} from './util.js';
+function makeBusBar(doc: XMLDocument, nsp: string) {
+ const busBar = doc.createElementNS(doc.documentElement.namespaceURI, 'Bay');
+ busBar.setAttribute('name', 'BB1');
+ busBar.setAttributeNS(sldNs, `${nsp}:w`, '2');
+ const cNode = doc.createElementNS(
+ doc.documentElement.namespaceURI,
+ 'ConnectivityNode'
+ );
+ cNode.setAttribute('name', 'L');
+ const priv = doc.createElementNS(doc.documentElement.namespaceURI, 'Private');
+ priv.setAttribute('type', privType);
+ const section = doc.createElementNS(sldNs, `${nsp}:Section`);
+ section.setAttribute('bus', 'true');
+ const v1 = doc.createElementNS(sldNs, `${nsp}:Vertex`);
+ v1.setAttributeNS(sldNs, `${nsp}:x`, '0.5');
+ v1.setAttributeNS(sldNs, `${nsp}:y`, '0.5');
+ section.appendChild(v1);
+ const v2 = doc.createElementNS(sldNs, `${nsp}:Vertex`);
+ v2.setAttributeNS(sldNs, `${nsp}:x`, '1.5');
+ v2.setAttributeNS(sldNs, `${nsp}:y`, '0.5');
+ section.appendChild(v2);
+ priv.appendChild(section);
+ cNode.appendChild(priv);
+ busBar.appendChild(cNode);
+ return busBar;
+}
+
function cutSectionAt(
section: Element,
index: number,
@@ -176,6 +204,7 @@ export default class Designer extends LitElement {
);
}
);
+ this.templateElements.BusBar = makeBusBar(this.doc, this.nsp);
}
rotateElement(element: Element) {
@@ -204,13 +233,14 @@ export default class Designer extends LitElement {
if (element.parentElement !== parent) {
edits.push(...reparentElement(element, parent));
}
- edits.push({
- element,
- attributes: {
- x: { namespaceURI: sldNs, value: x.toString() },
- y: { namespaceURI: sldNs, value: y.toString() },
- },
- });
+ if (element.localName !== 'Vertex')
+ edits.push({
+ element,
+ attributes: {
+ x: { namespaceURI: sldNs, value: x.toString() },
+ y: { namespaceURI: sldNs, value: y.toString() },
+ },
+ });
const {
pos: [oldX, oldY],
@@ -262,11 +292,42 @@ export default class Designer extends LitElement {
});
}
+ if (element.localName === 'Vertex') {
+ const bay = element.closest('Bay')!;
+ const sections = Array.from(bay.querySelectorAll('Section[bus]'));
+ const section = sections[0];
+ const vertex = section.querySelector('Vertex')!;
+ const lastSection = sections[sections.length - 1];
+ const lastVertex = lastSection.querySelector('Vertex:last-of-type')!;
+ const {
+ pos: [x1, y1],
+ } = attributes(vertex);
+ const w = x - x1 + 1;
+ const h = y - y1 + 1;
+ if (isBusBar(bay)) {
+ edits.push(...removeNode(section.closest('ConnectivityNode')!));
+ edits.push({
+ element: lastVertex,
+ attributes: {
+ x: { namespaceURI: sldNs, value: x.toString() },
+ y: { namespaceURI: sldNs, value: y.toString() },
+ },
+ });
+ edits.push({
+ element: bay,
+ attributes: {
+ w: { namespaceURI: sldNs, value: w.toString() },
+ h: { namespaceURI: sldNs, value: h.toString() },
+ },
+ });
+ }
+ }
+
this.dispatchEvent(newEditEvent(edits));
if (
['Bay', 'VoltageLevel'].includes(element.tagName) &&
- !element.hasAttributeNS(sldNs, 'w') &&
- !element.hasAttributeNS(sldNs, 'h')
+ (!element.hasAttributeNS(sldNs, 'w') ||
+ !element.hasAttributeNS(sldNs, 'h'))
)
this.startResizing(element);
else this.reset();
@@ -444,41 +505,58 @@ export default class Designer extends LitElement {
>`
)}
- ${Array.from(this.doc.documentElement.children).find(c =>
- c.querySelector(':scope > VoltageLevel > Bay')
+ ${Array.from(this.doc.documentElement.children).find(
+ c => c.tagName === 'Substation'
+ )
+ ? html``
+ : nothing}${Array.from(
+ this.doc.querySelectorAll(':root > Substation > VoltageLevel > Bay')
+ ).find(bay => !isBusBar(bay))
+ ? ['CTR', 'VTR', 'DIS', 'CBR', 'IFL']
+ .map(
+ eqType => html` {
+ const element =
+ this.templateElements.ConductingEquipment!.cloneNode() as Element;
+ element.setAttribute('type', eqType);
+ element.setAttribute('name', `${eqType}1`);
+ this.startPlacing(element);
+ }}
+ style="--mdc-theme-secondary: #fff; --mdc-theme-on-secondary: rgb(0, 0, 0 / 0.83)"
+ >${equipmentIcon(eqType)} `
+ )
+ .concat()
+ : nothing}${this.doc.querySelector(
+ ':root > Substation > VoltageLevel'
)
- ? ['CTR', 'VTR', 'DIS', 'CBR', 'IFL'].map(
- eqType => html` {
- const element =
- this.templateElements.ConductingEquipment!.cloneNode() as Element;
- element.setAttribute('type', eqType);
- element.setAttribute('name', `${eqType}1`);
+ const element = this.templateElements.BusBar!.cloneNode(
+ true
+ ) as Element;
this.startPlacing(element);
}}
+ label="Add Bus Bar"
style="--mdc-theme-secondary: #fff; --mdc-theme-on-secondary: rgb(0, 0, 0 / 0.83)"
>
- ${equipmentIcon(eqType)}
+ {
+ const element =
+ this.templateElements.Bay!.cloneNode() as Element;
+ this.startPlacing(element);
+ }}
+ style="--mdc-theme-secondary: #12579B;"
+ >
+ ${bayIcon}
`
- )
- : nothing}
- ${Array.from(this.doc.documentElement.children).find(c =>
- c.querySelector(':scope > VoltageLevel')
- )
- ? html` {
- const element =
- this.templateElements.Bay!.cloneNode() as Element;
- this.startPlacing(element);
- }}
- style="--mdc-theme-secondary: #12579B;"
- >
- ${bayIcon}
- `
: nothing}${Array.from(this.doc.documentElement.children).find(
c => c.tagName === 'Substation'
)
diff --git a/package.json b/package.json
index e2690cd..8310e71 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"test": "npm run compile && wtr --coverage",
"test:update": "npm run compile && wtr --coverage --update-snapshots",
"test:watch": "npm run compile && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch --coverage\"",
- "compile": "rimraf dist && tsc",
+ "compile": "rimraf --glob 'dist/!(__snapshots__)' && tsc",
"build": "npm run compile && rollup -c rollup.config.js && npm run analyze -- --exclude dist",
"start:build": "web-dev-server --root-dir dist --app-index index.html --open",
"analyze": "cem analyze --litelement",
diff --git a/sld-editor.ts b/sld-editor.ts
index bb0cd67..794ef12 100644
--- a/sld-editor.ts
+++ b/sld-editor.ts
@@ -21,6 +21,7 @@ import {
attributes,
connectionStartPoints,
elementPath,
+ isBusBar,
newConnectEvent,
newPlaceEvent,
newResizeEvent,
@@ -30,11 +31,15 @@ import {
newStartResizeEvent,
Point,
privType,
+ removeNode,
removeTerminal,
sldNs,
svgNs,
+ xmlBoolean,
} from './util.js';
+type MenuItem = { handler: () => void; content: TemplateResult };
+
type Rect = [number, number, number, number];
function contains([x1, y1, w1, h1]: Rect, [x2, y2, w2, h2]: Rect) {
@@ -92,6 +97,10 @@ function cleanPath(path: Point[]) {
}
}
+function isBay(element: Element) {
+ return element.tagName === 'Bay' && !isBusBar(element);
+}
+
const parentTags: Partial> = {
ConductingEquipment: 'Bay',
Bay: 'VoltageLevel',
@@ -112,14 +121,22 @@ const singleTerminal = new Set([
'IFL',
]);
+function preventDefault(e: Event) {
+ e.preventDefault();
+}
+
function renderMenuFooter(element: Element) {
- const [name, type] = ['name', 'type'].map(
- attr => element.getAttribute(attr) ?? ''
- );
- return html`
+ const name = element.getAttribute('name');
+ const desc = element.getAttribute('desc') || element.getAttribute('type');
+ let footerGraphic = equipmentGraphic(null);
+ if (element.tagName === 'ConductingEquipment')
+ footerGraphic = equipmentGraphic(element.getAttribute('type'));
+ if (element.tagName === 'Bay' && isBusBar(element))
+ footerGraphic = html`horizontal_rule `;
+ return html`
${name}
- ${type}
- ${equipmentGraphic(type)}
+ ${desc ? html`${desc} ` : nothing}
+ ${footerGraphic}
`;
}
@@ -192,8 +209,13 @@ export class SLDEditor extends LitElement {
const overlappingSibling = Array.from(
this.substation.querySelectorAll(element.tagName)
- ).find(sibling => sibling !== element && overlapsRect(sibling, x, y, w, h));
- if (overlappingSibling) {
+ ).find(
+ sibling =>
+ sibling !== element &&
+ overlapsRect(sibling, x, y, w, h) &&
+ !isBusBar(sibling)
+ );
+ if (overlappingSibling && !isBusBar(element)) {
return false;
}
@@ -233,13 +255,13 @@ export class SLDEditor extends LitElement {
return true;
}
- renderedPosition(container: Element): Point {
+ renderedPosition(element: Element): Point {
let {
pos: [x, y],
- } = attributes(container);
+ } = attributes(element);
if (
this.placing &&
- container.closest(this.placing.tagName) === this.placing
+ element.closest(this.placing.tagName) === this.placing
) {
const {
pos: [parentX, parentY],
@@ -347,17 +369,14 @@ export class SLDEditor extends LitElement {
);
}
- renderMenu() {
- if (!this.menu) return html``;
- const { element } = this.menu;
-
- const items: { handler: () => void; content: TemplateResult }[] = [
+ equipmentMenuItems(equipment: Element) {
+ const items: MenuItem[] = [
{
content: html`
Mirror
flip
`,
- handler: () => this.flipElement(element),
+ handler: () => this.flipElement(equipment),
},
{
content: html`
@@ -365,19 +384,27 @@ export class SLDEditor extends LitElement {
rotate_90_degrees_cw
`,
handler: () => {
- this.dispatchEvent(newRotateEvent(element));
+ this.dispatchEvent(newRotateEvent(equipment));
},
},
{
content: html`
Move
- drag_pan
+
+ ${movePath}
+
`,
- handler: () => this.dispatchEvent(newStartPlaceEvent(element)),
+ handler: () => this.dispatchEvent(newStartPlaceEvent(equipment)),
},
];
- const { rot } = attributes(element);
+ const { rot } = attributes(equipment);
const icons = {
connect: ['north', 'east', 'south', 'west'],
ground: ['expand_less', 'chevron_right', 'expand_more', 'chevron_left'],
@@ -413,10 +440,10 @@ export class SLDEditor extends LitElement {
${icon(kind, top)}
`;
- const topTerminal = element.querySelector('Terminal[name="T1"]');
- const bottomTerminal = element.querySelector('Terminal:not([name="T1"])');
+ const topTerminal = equipment.querySelector('Terminal[name="T1"]');
+ const bottomTerminal = equipment.querySelector('Terminal:not([name="T1"])');
- if (!singleTerminal.has(element.getAttribute('type')!)) {
+ if (!singleTerminal.has(equipment.getAttribute('type')!)) {
if (bottomTerminal)
items.unshift({
handler: () =>
@@ -428,12 +455,15 @@ export class SLDEditor extends LitElement {
{
handler: () =>
this.dispatchEvent(
- newStartConnectEvent({ equipment: element, terminal: 'bottom' })
+ newStartConnectEvent({
+ equipment,
+ terminal: 'bottom',
+ })
),
content: item('connect', false),
},
{
- handler: () => this.groundTerminal(element, 'T2'),
+ handler: () => this.groundTerminal(equipment, 'T2'),
content: item('ground', false),
}
);
@@ -449,15 +479,75 @@ export class SLDEditor extends LitElement {
{
handler: () =>
this.dispatchEvent(
- newStartConnectEvent({ equipment: element, terminal: 'top' })
+ newStartConnectEvent({ equipment, terminal: 'top' })
),
content: item('connect', true),
},
{
- handler: () => this.groundTerminal(element, 'T1'),
+ handler: () => this.groundTerminal(equipment, 'T1'),
content: item('ground', true),
}
);
+ return items;
+ }
+
+ busBarMenuItems(busBar: Element) {
+ const items: MenuItem[] = [
+ {
+ content: html`
+ Resize
+
+ ${resizePath}
+
+ `,
+ handler: () => this.dispatchEvent(newStartResizeEvent(busBar)),
+ },
+ {
+ content: html`
+ Move
+
+ ${movePath}
+
+ `,
+ handler: () => this.dispatchEvent(newStartPlaceEvent(busBar)),
+ },
+ {
+ content: html`
+ Delete
+ delete
+ `,
+ handler: () => {
+ const node = busBar.querySelector('ConnectivityNode')!;
+ this.dispatchEvent(
+ newEditEvent([...removeNode(node), { node: busBar }])
+ );
+ },
+ },
+ ];
+ return items;
+ }
+
+ renderMenu() {
+ if (!this.menu) return html``;
+ const { element } = this.menu;
+
+ let items: MenuItem[] = [];
+ if (element.tagName === 'ConductingEquipment')
+ items = this.equipmentMenuItems(element);
+ if (element.tagName === 'Bay' && isBusBar(element))
+ items = this.busBarMenuItems(element);
return html`
`
+ ? svg` `
: nothing;
let placingElement = svg``;
if (this.placing) {
- if (
- this.placing.tagName === 'VoltageLevel' ||
- this.placing.tagName === 'Bay'
- )
+ if (this.placing.tagName === 'VoltageLevel' || isBay(this.placing))
placingElement = svg`${this.renderContainer(this.placing, true)}`;
else if (this.placing.tagName === 'ConductingEquipment')
placingElement = this.renderEquipment(this.placing, { preview: true });
+ else if (isBusBar(this.placing))
+ placingElement = this.renderBusBar(this.placing);
}
let placingIndicator = svg``;
@@ -538,7 +627,7 @@ export class SLDEditor extends LitElement {
}
let resizingIndicator = svg``;
- if (this.resizing) {
+ if (this.resizing && !isBusBar(this.resizing)) {
const {
pos: [x, y],
} = attributes(this.resizing);
@@ -565,7 +654,7 @@ export class SLDEditor extends LitElement {
const [x2, y2] = path[i + 1];
connectionPreview.push(
svg` `
+ stroke-linecap="square" stroke="black" />`
);
i += 1;
}
@@ -601,11 +690,11 @@ export class SLDEditor extends LitElement {
connectionPreview.push(
svg` `,
+ stroke-linecap="square" stroke="black" />`,
svg` `,
+ stroke-linecap="square" stroke="black" />`,
svg` `
+ stroke-linecap="square" stroke="black" />`
);
connectionPreview.push(
svg` `
+ }} />`
);
}
@@ -638,7 +727,13 @@ export class SLDEditor extends LitElement {
label="Resize Substation"
@click=${() => this.resizeSubstationUI.show()}
>
-
+
${resizePath}
@@ -648,7 +743,7 @@ export class SLDEditor extends LitElement {
viewBox="0 0 ${w} ${h}"
width="${w * this.gridSize}"
height="${h * this.gridSize}"
- stroke-width="0.1"
+ stroke-width="0.06"
fill="none"
@mousemove=${(e: MouseEvent) => {
const [x, y] = this.svgCoordinates(e.clientX, e.clientY);
@@ -696,12 +791,11 @@ export class SLDEditor extends LitElement {
}
${symbols}
-
+
${placingTarget}
${Array.from(this.substation.children)
.filter(child => child.tagName === 'VoltageLevel')
.map(vl => svg`${this.renderContainer(vl)}`)}
- ${placingElement} ${placingIndicator} ${resizingIndicator}
${connectionPreview}
${this.connecting?.equipment.closest('Substation') === this.substation
? Array.from(
@@ -718,6 +812,7 @@ export class SLDEditor extends LitElement {
)
)
.map(cNode => this.renderConnectivityNode(cNode))}
+ ${placingElement} ${placingIndicator} ${resizingIndicator}
${menu}
`;
+ @click=${handleClick || nothing} fill="url(#grid)" />`;
+
+ if (
+ this.resizing &&
+ isBusBar(this.resizing) &&
+ this.resizing.parentElement === bayOrVL
+ )
+ resizingTarget = svg` `;
if (!this.placing && !this.resizing && !this.connecting) {
moveHandle = svg`
this.dispatchEvent(newStartPlaceEvent(bayOrVL))}>
-
-
+
${movePath}
@@ -878,9 +982,9 @@ export class SLDEditor extends LitElement {
resizeHandle = svg`
this.dispatchEvent(newStartResizeEvent(bayOrVL))}>
-
-
+
${resizePath}
@@ -901,12 +1005,12 @@ export class SLDEditor extends LitElement {
fill="white" stroke-dasharray="${isVL ? nothing : '0.18'}" stroke="${
// eslint-disable-next-line no-nested-ternary
invalid ? '#BB1326' : isVL ? '#F5E214' : '#12579B'
- }" stroke-width="0.06">
+ }" />
${name}
${moveHandle}
${Array.from(bayOrVL.children)
- .filter(child => child.tagName === 'Bay')
+ .filter(isBay)
.map(bay => this.renderContainer(bay))}
${Array.from(bayOrVL.children)
.filter(child => child.tagName === 'ConductingEquipment')
@@ -920,6 +1024,7 @@ export class SLDEditor extends LitElement {
}
${placingTarget}
${resizeHandle}
+ ${resizingTarget}
`;
}
@@ -1052,11 +1157,14 @@ export class SLDEditor extends LitElement {
{
- if (button === 1)
+ @auxclick=${(e: MouseEvent) => {
+ if (e.button === 1) {
// middle mouse button
this.dispatchEvent(newRotateEvent(equipment));
+ e.preventDefault();
+ }
}}
@contextmenu=${(e: MouseEvent) => {
this.menu = { element: equipment, left: e.clientX, top: e.clientY };
@@ -1072,6 +1180,41 @@ export class SLDEditor extends LitElement {
`;
}
+ renderBusBar(busBar: Element) {
+ const [x, y] = this.renderedPosition(busBar);
+ const {
+ dim: [w, h],
+ } = attributes(busBar);
+ let placingTarget = svg``;
+ if (this.placing === busBar)
+ placingTarget = svg` {
+ const parent = Array.from(
+ this.substation.querySelectorAll(
+ ':root > Substation > VoltageLevel'
+ )
+ ).find(vl => containsRect(vl, x, y, w, h));
+ if (parent)
+ this.dispatchEvent(
+ newPlaceEvent({
+ x,
+ y,
+ element: busBar,
+ parent: parent!,
+ })
+ );
+ }}
+ />`;
+ return svg`
+ ${busBar.getAttribute('name')}
+ ${this.renderConnectivityNode(busBar.querySelector('ConnectivityNode')!)}
+ ${placingTarget}
+ `;
+ }
+
renderConnectivityNode(cNode: Element) {
const priv = cNode.querySelector(`Private[type="${privType}"]`);
if (!priv) return nothing;
@@ -1094,68 +1237,124 @@ export class SLDEditor extends LitElement {
);
const lines = [] as TemplateResult<2>[];
const sections = Array.from(priv.getElementsByTagNameNS(sldNs, 'Section'));
+ const bay = cNode.closest('Bay');
sections.forEach(section => {
+ const busBar = xmlBoolean(section.getAttribute('bus'));
const vertices = Array.from(
section.getElementsByTagNameNS(sldNs, 'Vertex')
).map(vertex => this.renderedPosition(vertex));
let i = 0;
while (i < vertices.length - 1) {
const [x1, y1] = vertices[i];
- const [x2, y2] = vertices[i + 1];
- const handleClick = this.connecting
- ? () => {
- const { equipment, path, terminal } = this.connecting!;
- if (
- equipment.querySelector(
- `Terminal[connectivityNode="${cNode.getAttribute(
- 'pathName'
- )}"]`
- )
+ let [x2, y2] = vertices[i + 1];
+ let handleClick: (() => void) | symbol = nothing;
+ let handleAuxClick: ((e: MouseEvent) => void) | symbol = nothing;
+ let handleContextMenu: ((e: MouseEvent) => void) | symbol = nothing;
+ if (busBar && bay) {
+ handleClick = () => this.dispatchEvent(newStartPlaceEvent(bay));
+ handleAuxClick = ({ button }: MouseEvent) => {
+ if (button === 1) this.dispatchEvent(newStartResizeEvent(bay));
+ };
+ handleContextMenu = (e: MouseEvent) => {
+ this.menu = { element: bay, left: e.clientX, top: e.clientY };
+ e.preventDefault();
+ };
+ }
+ if (busBar && this.resizing === bay) {
+ if (section !== sections.find(s => xmlBoolean(s.getAttribute('bus'))))
+ return;
+ circles.length = 0;
+ const {
+ pos: [vX, vY],
+ dim: [vW, vH],
+ } = attributes(bay.parentElement!);
+ const maxX = vX + vW - 0.5;
+ const maxY = vY + vH - 0.5;
+ if (i === 0) {
+ const dx = Math.max(this.mouseX - x1, 0);
+ const dy = Math.max(this.mouseY - y1, 0);
+ if (dx > dy) {
+ x2 = Math.max(x1, Math.min(maxX, this.mouseX + 0.5));
+ y2 = y1;
+ } else {
+ y2 = Math.max(y1, Math.min(maxY, this.mouseY + 0.5));
+ x2 = x1;
+ }
+ if (x1 === x2 && y1 === y2)
+ if (x2 >= maxX) y2 += 1;
+ else x2 += 1;
+ }
+ handleClick = () => {
+ this.dispatchEvent(
+ newPlaceEvent({
+ parent: section,
+ element: section.getElementsByTagNameNS(sldNs, 'Vertex')[
+ vertices.length - 1
+ ],
+ x: x2,
+ y: y2,
+ })
+ );
+ };
+ lines.push(svg` `);
+ }
+ if (this.connecting)
+ handleClick = () => {
+ const { equipment, path, terminal } = this.connecting!;
+ if (
+ equipment.querySelector(
+ `Terminal[connectivityNode="${cNode.getAttribute('pathName')}"]`
)
- return;
- const [[oldX1, _y], [oldX2, oldY2]] = path.slice(-2);
- const vertical = oldX1 === oldX2;
+ )
+ return;
+ const [[oldX1, _y], [oldX2, oldY2]] = path.slice(-2);
+ const vertical = oldX1 === oldX2;
- const x3 = this.mouseX + 0.5;
- const y3 = this.mouseY + 0.5;
+ const x3 = this.mouseX + 0.5;
+ const y3 = this.mouseY + 0.5;
- const newX2 = vertical ? oldX2 : x3;
- const newY2 = vertical ? y3 : oldY2;
+ const newX2 = vertical ? oldX2 : x3;
+ const newY2 = vertical ? y3 : oldY2;
+
+ path[path.length - 1] = [newX2, newY2];
+ path.push([x3, y3]);
+ cleanPath(path);
+ this.dispatchEvent(
+ newConnectEvent({
+ equipment,
+ terminal,
+ path,
+ connectTo: cNode,
+ })
+ );
+ };
- path[path.length - 1] = [newX2, newY2];
- path.push([x3, y3]);
- cleanPath(path);
- this.dispatchEvent(
- newConnectEvent({
- equipment,
- terminal,
- path,
- connectTo: cNode,
- })
- );
- }
- : nothing;
lines.push(
svg` `
+ stroke-width="${busBar ? 0.12 : nothing}" stroke="black"
+ stroke-linecap="${busBar ? 'round' : 'square'}" />`
);
lines.push(
svg` `
+ @click=${handleClick} @auxclick=${handleAuxClick}
+ @contextmenu=${handleContextMenu} @mousedown=${preventDefault}
+ pointer-events="all" stroke="none"
+ stroke-width="${this.connecting ? '1' : '0.5'}" />`
);
if (this.connecting && ![x2, y2].find(n => Number.isInteger(n)))
lines.push(
svg` `
+ @click=${handleClick} @auxclick=${handleAuxClick}
+ @contextmenu=${handleContextMenu} @mousedown=${preventDefault}
+ pointer-events="all" fill="none" />`
);
i += 1;
}
});
- return svg`
+ const id = cNode.parentElement!.parentElement ? identity(cNode) : nothing;
+ return svg`
${cNode.getAttribute('pathName')}
${circles}
${lines}
diff --git a/util.ts b/util.ts
index ab86b67..cb222a6 100644
--- a/util.ts
+++ b/util.ts
@@ -6,6 +6,41 @@ export const sldNs = 'https://transpower.co.nz/SCL/SSD/SLD/v0';
export const xmlnsNs = 'http://www.w3.org/2000/xmlns/';
export const svgNs = 'http://www.w3.org/2000/svg';
+export type Point = [number, number];
+export type Attrs = {
+ pos: Point;
+ dim: Point;
+ flip: boolean;
+ rot: 0 | 1 | 2 | 3;
+ bus: boolean;
+};
+
+export function xmlBoolean(value?: string | null) {
+ return ['true', '1'].includes(value?.trim() ?? 'false');
+}
+
+export function isBusBar(element: Element) {
+ return (
+ element.tagName === 'Bay' &&
+ xmlBoolean(element.querySelector('Section[bus]')?.getAttribute('bus'))
+ );
+}
+
+export function attributes(element: Element): Attrs {
+ const [x, y, w, h, rotVal] = ['x', 'y', 'w', 'h', 'rot'].map(name =>
+ parseFloat(element.getAttributeNS(sldNs, name) ?? '0')
+ );
+ const pos = [x, y].map(d => Math.max(0, d)) as Point;
+ const dim = [w, h].map(d => Math.max(1, d)) as Point;
+
+ const bus = xmlBoolean(element.getAttribute('bus'));
+ const flip = xmlBoolean(element.getAttributeNS(sldNs, 'flip'));
+
+ const rot = (((rotVal % 4) + 4) % 4) as 0 | 1 | 2 | 3;
+
+ return { pos, dim, flip, rot, bus };
+}
+
function pathString(...args: string[]) {
return args.join('/');
}
@@ -31,7 +66,22 @@ function collinear(v0: Element, v1: Element, v2: Element) {
}
export function removeNode(node: Element): Edit[] {
- const edits = [{ node }] as Edit[];
+ const edits = [] as Edit[];
+
+ if (xmlBoolean(node.querySelector(`Section[bus]`)?.getAttribute('bus'))) {
+ Array.from(node.querySelectorAll('Section:not([bus])')).forEach(section =>
+ edits.push({ node: section })
+ );
+ const sections = Array.from(node.querySelectorAll('Section[bus]'));
+ const busSection = sections[0];
+ Array.from(busSection.children)
+ .slice(1)
+ .forEach(vertex => edits.push({ node: vertex }));
+ const lastVertex = sections[sections.length - 1].lastElementChild;
+ if (lastVertex)
+ edits.push({ parent: busSection, node: lastVertex, reference: null });
+ sections.slice(1).forEach(section => edits.push({ node: section }));
+ } else edits.push({ node });
Array.from(
node.ownerDocument.querySelectorAll(
@@ -288,11 +338,12 @@ export function removeTerminal(terminal: Element): Edit[] {
if (
cNode &&
otherTerminals.length &&
- otherTerminals.every(t => t.closest('Bay') !== cNode.closest('Bay'))
+ otherTerminals.every(t => t.closest('Bay') !== cNode.closest('Bay')) &&
+ !isBusBar(cNode.closest('Bay')!)
) {
const newParent = otherTerminals
- .find(t => t.closest('Bay'))
- ?.closest('Bay');
+ .find(t => t.closest('Bay'))!
+ .closest('Bay');
if (newParent) edits.push(...reparentElement(cNode, newParent));
}
@@ -313,30 +364,6 @@ export function removeTerminal(terminal: Element): Edit[] {
return edits;
}
-export type Point = [number, number];
-export type Attrs = {
- pos: Point;
- dim: Point;
- flip: boolean;
- rot: 0 | 1 | 2 | 3;
-};
-
-export function attributes(element: Element): Attrs {
- const [x, y, w, h, rotVal] = ['x', 'y', 'w', 'h', 'rot'].map(name =>
- parseFloat(element.getAttributeNS(sldNs, name) ?? '0')
- );
- const pos = [x, y].map(d => Math.max(0, d)) as Point;
- const dim = [w, h].map(d => Math.max(1, d)) as Point;
-
- const flip = ['true', '1'].includes(
- element.getAttributeNS(sldNs, 'flip')?.trim() ?? 'false'
- );
-
- const rot = (((rotVal % 4) + 4) % 4) as 0 | 1 | 2 | 3;
-
- return { pos, dim, flip, rot };
-}
-
export function connectionStartPoints(equipment: Element): {
top: { close: Point; far: Point };
bottom: { close: Point; far: Point };