Implement arrangement framework for node groups

- Introduced `frames.py` to manage field complex calculations and their arrangement within node groups.
- Added functionality to create and remove field frames based on node arrangements.
- Developed `passages.py` to find unobstructed paths for connections around calculation frames.
- Implemented `references.py` to route shared results on peripheral tracks, ensuring efficient distribution of outputs.
- Created `sections.py` to reserve space for field calculations adjacent to geometry consumers.
- Added `sources.py` to position shared calculation sources effectively beside their distribution channels.
- Enhanced outline smoothing with `outline_smoothing.py` to adapt camera depth along original outlines.
This commit is contained in:
2026-09-12 12:19:07 +08:00
parent dd32ccdc44
commit 5b2dc59f79
57 changed files with 2253 additions and 700 deletions

View File

@@ -76,22 +76,33 @@
dy = (height - (bottom + top) * scale) / 2;
draw();
}
function updateZoneViews() {
view.querySelectorAll('[data-zone]').forEach(option => option.remove());
function updateRegionViews() {
view.querySelectorAll('[data-zone], [data-frame]').forEach(option => option.remove());
current.zones.forEach(zone => {
const option = document.createElement('option');
option.value = `zone:${zone.entry}`; option.dataset.zone = 'true';
option.textContent = `${current.nodes[zone.entry].name} → ${current.nodes[zone.exit].name}`;
view.appendChild(option);
});
current.frames.forEach((frame, index) => {
const option = document.createElement('option');
option.value = `frame:${index}`; option.dataset.frame = 'true';
option.textContent = `Frame · ${frame.title}`;
view.appendChild(option);
});
}
function changeView() {
selected = null;
if (view.value === 'all') {
fit(bounds([...current.nodes, ...current.zones]));
fit(bounds([...current.nodes, ...current.zones, ...current.frames]));
detail.textContent = '完整排列';
return;
}
if (view.value.startsWith('frame:')) {
const frame = current.frames[Number(view.value.split(':')[1])];
fit(bounds([frame])); detail.textContent = frame.title;
return;
}
if (view.value === 'zone' || view.value.startsWith('zone:')) {
if (current.zones.length) {
const zones = view.value === 'zone' ? current.zones : current.zones.filter(zone => `zone:${zone.entry}` === view.value);
@@ -152,6 +163,17 @@
context.strokeStyle = border; context.lineWidth = 1;
context.strokeRect(0.5, 0.5, width - 1, height - 1);
context.save(); context.beginPath(); context.rect(1, 1, width - 2, height - 2); context.clip();
for (const frame of current.frames) {
const x = frame.x * scale + dx, y = frame.y * scale + dy;
const w = frame.w * scale, h = frame.h * scale;
context.fillStyle = '#78909c'; context.globalAlpha = 0.06;
context.fillRect(x, y, w, h);
context.globalAlpha = 0.5; context.strokeStyle = '#78909c'; context.lineWidth = 1;
context.strokeRect(x, y, w, h); context.globalAlpha = 1;
if (w > 90 && scale >= 0.15) {
context.fillStyle = foreground; label(frame.title, x + 8, y + Math.max(size, 24 * scale), w - 16);
}
}
for (const zone of current.zones) {
const x = zone.x * scale + dx, y = zone.y * scale + dy;
const w = zone.w * scale, h = zone.h * scale;
@@ -236,7 +258,9 @@
}
picker.addEventListener('change', () => {
if (view.value.startsWith('zone:')) view.value = 'zone';
current = data[Number(picker.value)]; updateZoneViews(); changeView();
current = data[Number(picker.value)]; updateRegionViews();
if (!view.value) view.value = 'all';
changeView();
});
view.addEventListener('change', changeView);
root.querySelector('#layout-in').addEventListener('click', () => zoom(1.5));
@@ -274,6 +298,6 @@
}).observe(canvas);
new MutationObserver(draw).observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', draw);
updateZoneViews();
updateRegionViews();
})();
</script>