feat: allow to edit the print layout

Merge branch 'movable-print'
main
Yanick Champoux 2022-04-10 18:24:40 -04:00
commit 381d497a15
19 changed files with 432 additions and 135 deletions

View File

@ -1 +1,2 @@
export const browser = true;
export const dev = true;

1
fake/app/paths.js Normal file
View File

@ -0,0 +1 @@
export const base = "";

View File

@ -18,9 +18,6 @@
"@sveltejs/adapter-static": "^1.0.0-next.28",
"@sveltejs/kit": "^1.0.0-next.288",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.38",
"@vitebook/client": "^0.23.2",
"@vitebook/core": "^0.23.2",
"@vitebook/theme-default": "^0.23.2",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.4.0",
"eslint-plugin-svelte3": "^3.4.1",
@ -28,13 +25,12 @@
"prettier-plugin-svelte": "^2.6.0",
"standard-version": "^9.3.2",
"storybook-builder-vite": "0.1.21",
"svelte": "^3.46.4",
"vite": "^2.7.0"
"svelte": "^3.46.4"
},
"dependencies": {
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-svelte-csf": "^1.1.0",
"@storybook/svelte": "^6.4.19",
"@storybook/svelte": "^6.4.21",
"@sveltejs/adapter-node": "^1.0.0-next.0",
"chota": "^0.8.0",
"lodash": "^4.17.21",

View File

@ -1,4 +1,12 @@
<div>
<div
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "hull", translate });
}}
style:transform={hull?.uiTransform}
>
{#each rows as row, i (i)}
<div class="row">
{#each row as threshold, j (j)}
@ -13,10 +21,18 @@
</div>
<script>
import { base } from '$app/paths';
import { base } from "$app/paths";
import { getContext } from "svelte";
import { movable } from "../../MainSystems/movable.js";
export let shipMass = 0;
export let rating = 0;
export let advanced = false;
export let hull = {};
export let isMovable = false;
const ship = getContext("ship");
let nbr_rows;
$: nbr_rows = advanced ? 3 : 4;
@ -50,9 +66,9 @@
</script>
<style>
.row {
.row {
margin-bottom: 0.5em;
}
}
.cell {
display: inline-block;
margin-right: 0.5em;

View File

@ -1,6 +1,8 @@
<div>
<Armour armour={structure.armour} />
<Integrity
{isMovable}
hull={structure?.hull}
rating={structure.hull.rating}
advanced={structure.hull.advanced}
{ship_mass}
@ -13,4 +15,5 @@
export let structure = {};
export let ship_mass = 0;
export let isMovable = false;
</script>

View File

@ -1,28 +1,33 @@
<div class="main_systems">
{#if ftl !== "none"}
<img
bind:this={targetFTL}
class="ftl"
src="{base}/icons/ftl-drive.svg"
alt="ftl drive"
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "ftl", translate });
}}
style:transform={ftl?.uiTransform}
/>
{#if movable}
<Movable target={targetFTL} />
{/if}
{/if}
{#if engine > 0}
<div
bind:this={targetEngine}
class="thrust"
style="background-image: url({base}/icons/standard-drive.svg);"
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "drive", translate });
}}
style:transform={drive?.uiTransform}
>
{engine}
</div>
{#if movable}
<Movable target={targetEngine} />
{/if}
{/if}
<img
@ -30,20 +35,36 @@
src="{base}/icons/internal-systems.svg"
alt="internal systems"
bind:this={targetInternal}
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "internalSystems", translate });
}}
style:transform={structure?.uiTransform}
/>
{#if movable}
<Movable target={targetInternal} />
{/if}
</div>
<script>
import { getContext } from "svelte";
import { base } from "$app/paths";
import Movable from "./Movable.svelte";
import { movable } from "./movable.js";
export let ftl = "none";
export let engine = 0;
export let movable = false;
export let isMovable = false;
export let structure = {};
export let drive = {};
let internalTranslate = "translate(50px,50px)";
const ship = getContext("ship");
let frame = {
translate: [0, 0],
};
let targetFTL;
let targetInternal;

View File

@ -0,0 +1,64 @@
import VanillaMoveable, { PROPERTIES, EVENTS } from "moveable";
import { camelize, isUndefined } from "@daybrush/utils";
function createMoveable(node, options) {
let translate = [0, 0];
options = {
originDraggable: true,
originRelative: true,
draggable: true,
throttleDrag: 0,
zoom: 1,
origin: false,
onDrag(e) {
translate = e.beforeTranslate;
node.dispatchEvent(new CustomEvent("translate", { detail: translate }));
},
target: node,
...options,
};
const moveable = new VanillaMoveable(document.body, options);
EVENTS.forEach((name) => {
const onName = camelize(`on ${name}`);
moveable.on(name, (e) => {
const result = options[onName] && options[onName](e);
const result2 = node.dispatchEvent(new CustomEvent(name, { detail: e }));
return !isUndefined(result)
? result
: !isUndefined(result2)
? result2
: undefined;
});
});
return moveable;
}
export function movable(node, options) {
let moveable = options.disabled ? undefined : createMoveable(options);
const destroy = () => {
if (!moveable) return;
moveable.destroy();
moveable = undefined;
};
const update = async (params) => {
if (params.disabled) {
destroy();
} else {
if (!moveable) {
moveable = createMoveable(node, params);
}
}
};
return {
destroy,
update,
};
}

View File

@ -1,15 +1,16 @@
<Meta title="Output/Print" component={Print} argTypes={{
ship: {
type: 'object',
defaultValue: sample
}
}} />
<Meta
title="Output/Print"
component={Print}
argTypes={{
isMovable: { defaultValue: false },
}}
/>
<Story name="Primary" args={{}} />
<Template let:args>
<div style="width: 50em">
<Print ship={sample}/>
<div style="width: 50em; positive: relative;">
<Print ship={$shipState} {...args} />
</div>
</Template>
@ -17,7 +18,16 @@
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
import { action } from "@storybook/addon-actions";
import sample from './sample.js';
import { setContext } from "svelte";
import Print from './index.svelte';
import sample from "./sample.js";
import shipStore from "$lib/store/ship.js";
const ship = shipStore(sample);
setContext("ship", ship);
const shipState = ship.state;
import Print from "./index.svelte";
</script>

View File

@ -1,14 +1,27 @@
<div>
{#each range(1,firecons) as firecon}
<div
style:transform={uiTransform}
use:movable={{
disabled: !isMovable,
ship,
system: "firecons",
}}
>
{#each range(1, stations) as firecon}
<img class="firecon" src="{base}/icons/firecon.svg" alt="firecon" />
{/each}
</div>
<script>
import { base } from '$app/paths';
import {range} from "$lib/utils.js";
import { base } from "$app/paths";
import { range } from "$lib/utils.js";
import { getContext } from "svelte";
import { movable } from "../../movable.js";
export let firecons = 0;
export let stations = 0;
export let isMovable = false;
export let uiTransform = "";
const ship = getContext("ship");
</script>
<style>

View File

@ -1,18 +1,33 @@
<div>
{#each range(1,standard) as i}
<div
use:movable={{
disabled: !isMovable,
}}
on:translate={({ detail: translate }) => {
ship.dispatch.setUITransform({ system: "screens", translate });
}}
style:transform={uiTransform}
>
{#each range(1, standard) as i}
<img src="{base}/icons/screen.svg" alt="screen" />
{/each}
{#each range(1,advanced) as i}
{#each range(1, advanced) as i}
<img src="{base}/icons/screen-advanced.svg" alt="advanced screen" />
{/each}
</div>
<script>
import { base } from '$app/paths';
import {range} from "$lib/utils.js";
import { base } from "$app/paths";
import { range } from "$lib/utils.js";
import { getContext } from "svelte";
import { movable } from "../../MainSystems/movable.js";
export let standard = 0;
export let advanced = 0;
export let uiTransform = "";
export let isMovable = false;
const ship = getContext("ship");
</script>
<style>

View File

@ -1,7 +1,7 @@
<div>
<Firecons {firecons} />
<Firecons {isMovable} {...firecons} />
<Screens {...screens} />
<Screens {isMovable} {...screens} />
</div>
<script>
@ -10,6 +10,7 @@
export let firecons = 0;
export let screens = {};
export let isMovable = false;
</script>
<style>

View File

@ -1,4 +1,11 @@
<div>
<div
style:transform={uiTransform}
use:movable={{
disabled: !isMovable,
ship,
system: ["weapon", id],
}}
>
<Arcs selected={arcs} size="40">
<text x="50%" y="50%">
{weaponClass}
@ -7,9 +14,15 @@
</div>
<script>
import { getContext } from "svelte";
import { movable } from "../../movable.js";
import Arcs from "$lib/components/ShipEdit/Weaponry/Weapon/Arcs.svelte";
export let weaponClass = 1;
export let arcs = [];
export let uiTransform = "";
export let isMovable = false;
export let id = -1;
const ship = getContext("ship");
</script>
<style>

View File

@ -1,17 +1,22 @@
<div class="weapons">
<div class="beams">
{#each beams as beam}
<Beam {...beam} />
<Beam {isMovable} {...beam} />
{/each}
</div>
</div>
<script>
import Beam from "./Beam/index.svelte";
import { getContext } from "svelte";
import { movable } from "../movable.js";
export let weapons = [];
export let isMovable = false;
let beams = [];
$: beams = weapons.filter(({ type }) => type === "beam");
const ship = getContext("ship");
</script>
<style>

View File

@ -1,9 +1,8 @@
<div class="notice">
<label>
<input type="checkbox" bind:checked={movable} /> enable wiggletron (<i
>alpha feature</i
>)
<input type="checkbox" bind:checked={isMovable} /> edit layout
</label>
<button class="button error" on:click={resetLayout}>reset layout</button>
</div>
<div class="print-output">
@ -15,26 +14,34 @@
/>
<div class="section-2">
<Hull structure={ship.structure} shipMass={ship.identification.mass} />
<Hull
structure={ship.structure}
shipMass={ship.identification.mass}
{isMovable}
/>
<Systems
firecons={ship.weaponry.firecons.nbr}
{isMovable}
firecons={ship.weaponry.firecons}
screens={ship.structure.screens}
/>
</div>
<Weapons weapons={ship.weaponry.weapons} />
<Weapons {isMovable} weapons={ship.weaponry.weapons} />
<MainSystems
{movable}
{isMovable}
ftl={ship?.propulsion?.ftl}
engine={ship?.propulsion?.drive?.rating}
drive={ship?.propulsion?.drive}
structure={ship?.structure}
/>
</div>
<div class="notice">Printing this page will only prints the ship sheet.</div>
<script>
import { getContext } from "svelte";
import Identification from "./Identification/index.svelte";
import MainSystems from "./MainSystems/index.svelte";
import Hull from "./Hull/index.svelte";
@ -42,7 +49,14 @@
import Systems from "./Systems/index.svelte";
export let ship = {};
let movable = false;
export let isMovable = false;
const { dispatch } = getContext("ship");
const resetLayout = () => {
isMovable = false;
dispatch.resetLayout();
};
</script>
<style>
@ -64,6 +78,10 @@
font-style: italic;
margin-top: 1em;
text-align: right;
display: flex;
justify-content: end;
align-items: center;
gap: 3em;
}
@media print {

View File

@ -0,0 +1,76 @@
import VanillaMoveable, { PROPERTIES, EVENTS } from "moveable";
import { camelize, isUndefined } from "@daybrush/utils";
function createMoveable(node, options) {
let translate = [0, 0];
let ship = options.ship;
let system = options.system;
delete options.ship;
delete options.system;
if (Array.isArray(system)) {
system = { system: system[0], systemId: system[1] };
} else {
system = { system };
}
options = {
originDraggable: true,
originRelative: true,
draggable: true,
throttleDrag: 0,
zoom: 1,
origin: false,
onDrag(e) {
translate = e.beforeTranslate;
node.dispatchEvent(new CustomEvent("translate", { detail: translate }));
ship.dispatch.setUITransform({ ...system, translate });
},
target: node,
...options,
};
const moveable = new VanillaMoveable(document.body, options);
EVENTS.forEach((name) => {
const onName = camelize(`on ${name}`);
moveable.on(name, (e) => {
const result = options[onName] && options[onName](e);
const result2 = node.dispatchEvent(new CustomEvent(name, { detail: e }));
return !isUndefined(result)
? result
: !isUndefined(result2)
? result2
: undefined;
});
});
return moveable;
}
export function movable(node, options) {
let moveable = options.disabled ? undefined : createMoveable(options);
const destroy = () => {
if (!moveable) return;
moveable.destroy();
moveable = undefined;
};
const update = async (params) => {
if (params.disabled) {
destroy();
} else {
if (!moveable) {
moveable = createMoveable(node, params);
}
}
};
return {
destroy,
update,
};
}

View File

@ -11,48 +11,96 @@ import weaponry from "./weaponry/index.js";
import { screensReqsReaction } from "./structure/screens.js";
const dux = new Updux({
subduxes: {
identification,
propulsion,
structure,
carrier,
weaponry,
},
initial: {
reqs: { cost: 0, mass: 10, usedMass: 0 },
},
actions: {
setShipReqs: null,
},
subduxes: {
identification,
propulsion,
structure,
carrier,
weaponry,
},
initial: {
reqs: { cost: 0, mass: 10, usedMass: 0 },
},
actions: {
setShipReqs: null,
setUITransform: null,
resetLayout: null,
},
});
function resetUITransform(thing) {
if (typeof thing !== "object") return thing;
return u.map(
(v, k) => (k === "uiTransform" ? "" : resetUITransform(v)),
thing
);
}
dux.setMutation("resetLayout", () => resetUITransform);
dux.setMutation("setShipMass", (mass) => u({ reqs: { mass } }));
dux.setMutation('setShipReqs', reqs => u({reqs}));
dux.setMutation("setShipReqs", (reqs) => u({ reqs }));
dux.setMutation("setUITransform", ({ system, systemId, translate }) => {
const transform = translate
? `translate(${translate[0]}px,${translate[1]}px)`
: "";
switch (system) {
case "firecons":
return u.updateIn("weaponry.firecons.uiTransform", transform);
case "weapon":
return u.updateIn(
"weaponry.weapons",
u.map(u.if(({ id }) => id === systemId, u({ uiTransform: transform })))
);
case "screens":
return u.updateIn("structure.screens.uiTransform", transform);
case "hull":
return u.updateIn("structure.hull.uiTransform", transform);
case "internalSystems":
const path = "structure.uiTransform";
return u.updateIn(path, transform);
case "ftl":
return u.updateIn("propulsion.ftl.uiTransform", transform);
case "drive":
return u.updateIn("propulsion.drive.uiTransform", transform);
default:
return (state) => state;
}
});
dux.addReaction(calculateDriveReqs);
dux.addReaction(ftlReqsReaction);
dux.addReaction(screensReqsReaction);
dux.addReaction( (store) => (state) => {
let cost = 0;
let mass = 0;
dux.addReaction((store) => (state) => {
let cost = 0;
let mass = 0;
let subsystems = Object.values(state);
let subsystems = Object.values(state);
while(subsystems.length>0) {
const subsystem = subsystems.shift();
if( typeof subsystem !== 'object' ) continue;
while (subsystems.length > 0) {
const subsystem = subsystems.shift();
if (typeof subsystem !== "object") continue;
if( subsystem.reqs ) {
cost += subsystem.reqs.cost;
mass += subsystem.reqs.mass;
}
subsystems.push( ...Object.values(subsystem));
if (subsystem.reqs) {
cost += subsystem.reqs.cost;
mass += subsystem.reqs.mass;
}
store.dispatch.setShipReqs({cost,usedMass: mass});
subsystems.push(...Object.values(subsystem));
}
store.dispatch.setShipReqs({ cost, usedMass: mass });
});
export default dux;

View File

@ -10,35 +10,32 @@ const dux = new Updux({
subduxes: { reqs },
initial: {
type: "none",
uiTransform: "",
},
actions: {
setFtl: null,
setFtlReqs: null,
setFtl: null,
setFtlReqs: null,
},
});
export default dux;
dux.setMutation( 'setFtl', type => u({type}) );
dux.setMutation( 'setFtlReqs', reqs => u({reqs}) );
dux.setMutation("setFtl", (type) => u({ type }));
dux.setMutation("setFtlReqs", (reqs) => u({ reqs }));
export function calcFtlReqs(type,shipMass) {
if(type==="none") return { cost: 0, mass: 0 };
export function calcFtlReqs(type, shipMass) {
if (type === "none") return { cost: 0, mass: 0 };
const mass = Math.ceil(shipMass / 10);
const mass = Math.ceil(shipMass / 10);
return {
mass,
cost: mass * ( type === 'advanced' ? 3 : 2 ),
}
return {
mass,
cost: mass * (type === "advanced" ? 3 : 2),
};
}
// needs to be at the top level
export const ftlReqsReaction = store =>
export const ftlReqsReaction = (store) =>
createSelector(
[
(ship) => ship.propulsion.ftl.type,
(ship) => ship.reqs.mass,
],
(type,shipMass) =>
store.dispatch.setFtlReqs(calcFtlReqs(type,shipMass))
);
[(ship) => ship.propulsion.ftl.type, (ship) => ship.reqs.mass],
(type, shipMass) => store.dispatch.setFtlReqs(calcFtlReqs(type, shipMass))
);

View File

@ -1,13 +1,15 @@
import { Updux } from 'updux';
import { Updux } from "updux";
import hull from './hull.js';
import screens from './screens.js';
import cargo from './cargo.js';
import armor from './armor.js';
import streamlining from './streamlining.js';
import hull from "./hull.js";
import screens from "./screens.js";
import cargo from "./cargo.js";
import armor from "./armor.js";
import streamlining from "./streamlining.js";
const dux = new Updux({
subduxes: { hull, screens, cargo, streamlining, armor }
subduxes: { hull, screens, cargo, streamlining, armor },
initial: {
uiTransform: "",
},
});
export default dux;

View File

@ -8,36 +8,33 @@ import { initial } from "lodash";
let composeEnhancers = compose;
if (dev && browser && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
}
export default () => {
export default (initialState = undefined) => {
if (browser) {
const i = localStorage.getItem("ship");
let initialState = undefined;
if (i) initialState = JSON.parse(localStorage.getItem("ship"));
}
if( browser ) {
const i =localStorage.getItem('ship');
const duxStore = shipDux.createStore(initialState, (mw) =>
composeEnhancers(applyMiddleware(mw))
);
if(i) initialState = JSON.parse(localStorage.getItem('ship'));
}
const duxStore = shipDux.createStore(initialState, (mw) =>
composeEnhancers(applyMiddleware(mw))
);
let previous;
const state = readable(duxStore.getState(), (set) => {
duxStore.subscribe(() => {
if (previous === duxStore.getState()) return;
previous = duxStore.getState();
set(previous);
if( browser ) localStorage.setItem('ship', JSON.stringify(previous));
});
let previous;
const state = readable(duxStore.getState(), (set) => {
duxStore.subscribe(() => {
if (previous === duxStore.getState()) return;
previous = duxStore.getState();
set(previous);
if (browser) localStorage.setItem("ship", JSON.stringify(previous));
});
});
return {
dispatch: duxStore.dispatch,
state,
shipMass: derived(state, (state) => state.reqs.mass),
};
return {
dispatch: duxStore.dispatch,
state,
shipMass: derived(state, (state) => state.reqs.mass),
};
};